#!/usr/bin/env python3 # -*- coding: utf-8 -*- import requests import mysql.connector from datetime import datetime from urllib.parse import quote import time import re import json import os import sys import signal import logging import random from typing import Dict, Optional, List from dotenv import load_dotenv from pathlib import Path # Charger les variables d'environnement load_dotenv() # Configuration du logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('dual_blog_articles.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) class AIModelManager: """Gestionnaire multi-modèles AI avec fallback automatique""" def __init__(self, api_keys: Dict = None): self.api_keys = api_keys or {} # Configuration des URLs self.pollinations_url = "https://text.pollinations.ai" # Configuration Grok (xAI) self.grok_config = { 'url': 'https://api.x.ai/v1/chat/completions', 'model': 'grok-2-latest' } # Configuration OpenRouter self.openrouter_config = { 'url': 'https://openrouter.ai/api/v1/chat/completions', 'models': [ 'mistralai/mistral-7b-instruct:free', 'meta-llama/llama-3-8b-instruct:free', 'google/gemma-2-9b-it:free', 'microsoft/phi-3-mini-128k-instruct:free' ] } logger.info("✅ Gestionnaire multi-modèles initialisé") def call_pollinations(self, prompt: str, timeout: int = 120) -> Optional[str]: """Appeler Pollinations.ai""" try: encoded_prompt = quote(prompt) url = f"{self.pollinations_url}/{encoded_prompt}" response = requests.get(url, timeout=timeout) if response.status_code == 200: content = response.text.strip() if len(content) > 200: logger.info(f"✅ Pollinations: succès ({len(content)} caractères)") return content elif response.status_code == 502: logger.warning("⚠️ Pollinations: HTTP 502 (Bad Gateway)") elif response.status_code == 403: logger.warning("⚠️ Pollinations: HTTP 403 (Forbidden)") except Exception as e: logger.error(f"❌ Pollinations: {str(e)}") return None def call_grok(self, prompt: str, timeout: int = 120) -> Optional[str]: """Appeler Grok API""" if not self.api_keys.get('grok'): return None try: headers = { 'Authorization': f'Bearer {self.api_keys["grok"]}', 'Content-Type': 'application/json' } data = { 'model': self.grok_config['model'], 'messages': [ {'role': 'system', 'content': 'Vous êtes un expert en rédaction d\'articles professionnels.'}, {'role': 'user', 'content': prompt} ], 'temperature': 0.7, 'max_tokens': 4000 } response = requests.post( self.grok_config['url'], headers=headers, json=data, timeout=timeout ) if response.status_code == 200: result = response.json() content = result['choices'][0]['message']['content'].strip() logger.info(f"✅ Grok: succès ({len(content)} caractères)") return content except Exception as e: logger.error(f"❌ Grok: {str(e)}") return None def call_openrouter(self, prompt: str, timeout: int = 120) -> Optional[str]: """Appeler OpenRouter API""" if not self.api_keys.get('openrouter'): return None for model in self.openrouter_config['models']: try: headers = { 'Authorization': f'Bearer {self.api_keys["openrouter"]}', 'Content-Type': 'application/json' } data = { 'model': model, 'messages': [ {'role': 'system', 'content': 'Vous êtes un expert en rédaction SEO.'}, {'role': 'user', 'content': prompt} ], 'temperature': 0.7, 'max_tokens': 4000 } response = requests.post( self.openrouter_config['url'], headers=headers, json=data, timeout=timeout ) if response.status_code == 200: result = response.json() content = result['choices'][0]['message']['content'].strip() logger.info(f"✅ OpenRouter ({model}): succès ({len(content)} caractères)") return content except Exception as e: logger.error(f"❌ OpenRouter ({model}): {str(e)}") continue return None def generate_article(self, titre: str, max_retries: int = 2) -> Dict: """Générer un article avec fallback""" prompt = self.build_prompt(titre) models_order = [ ('pollinations', self.call_pollinations), ('grok', self.call_grok), ('openrouter', self.call_openrouter) ] for attempt in range(max_retries): for model_name, model_func in models_order: logger.info(f"🔄 Tentative {attempt + 1}/{max_retries} avec {model_name}") content = model_func(prompt) if content and len(content) > 200: return { "titre": titre, "contenu": content, "modele": model_name, "date_creation": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "statut": "succès" } if attempt < max_retries - 1: wait_time = (attempt + 1) * 10 logger.info(f"⏸️ Attente de {wait_time} secondes...") time.sleep(wait_time) return { "titre": titre, "contenu": "", "modele": "aucun", "statut": "erreur" } def build_prompt(self, titre: str) -> str: """Construire le prompt""" return f"""Rédiger un article complet et professionnel pour un blog emploi en Suisse. Sujet : {titre} Instructions : - 800-1000 mots minimum - Structure: Introduction, développement en sections, conclusion percutante - Utiliser des sous-titres (h2, h3, h4) - Ton informatif, engageant et expert - Inclure des conseils pratiques et des exemples concrets - Style: blog professionnel avec storytelling - Optimisé SEO avec mots-clés pertinents - Langue: français de Suisse Rédiger l'article :""" class DualBlogPostGenerator: """Générateur d'articles pour deux sites en alternance""" def __init__(self, api_keys: Dict = None): self.api_keys = api_keys self.ai_manager = AIModelManager(api_keys) # Configuration des deux bases de données self.sites_config = { 'emploisuisse': { 'name': 'EmploiSuisse', 'db_config': { 'host': '127.0.0.1', 'user': 'roseiujr_emploisuisse', 'password': '19Sh@hrazad', 'database': 'roseiujr_emploisuisse' }, 'table': 'news', 'fields': { 'title': 'title', 'slug': 'slug', 'content': 'content', 'excerpt': 'excerpt', 'image': 'featured_image', 'category': 'category', 'author': 'author', 'status': 'status', 'published_at': 'published_at' }, 'default_image': 'uploads/news/actualites.png', 'author': 'EmploiSuisse Bot' }, 'chjobs': { 'name': 'CH-Jobs', 'db_config': { 'host': '127.0.0.1', 'user': 'adminsql', 'password': '19Sh@hrazad.', 'database': 'roseiujr_ch-jobs' }, 'table': 'blog_posts', 'fields': { 'title': 'title', 'slug': 'slug', 'content': 'content', 'excerpt': 'excerpt', 'category': 'category', 'author': 'author', 'status': 'is_published', 'published_at': 'published_at' }, 'default_image': None, 'author': 'CH-Jobs Bot' } } # Fichier pour suivre la dernière exécution self.state_file = '/tmp/dual_blog_state.json' self.stats = { 'pollinations': 0, 'grok': 0, 'openrouter': 0, 'errors': 0, 'emploisuisse_posts': 0, 'chjobs_posts': 0 } self.load_state() def load_state(self): """Charger l'état de la dernière exécution""" if os.path.exists(self.state_file): try: with open(self.state_file, 'r') as f: state = json.load(f) self.last_site = state.get('last_site', 'chjobs') # Commencer par chjobs logger.info(f"📋 Dernier site publié: {self.last_site}") except: self.last_site = 'chjobs' else: self.last_site = 'chjobs' self.save_state() def save_state(self): """Sauvegarder l'état""" with open(self.state_file, 'w') as f: json.dump({'last_site': self.last_site, 'timestamp': datetime.now().isoformat()}, f) def get_next_site(self) -> str: """Déterminer quel site publier (alternance)""" next_site = 'emploisuisse' if self.last_site == 'chjobs' else 'chjobs' logger.info(f"🔄 Alternance: {self.last_site} → {next_site}") return next_site def connect_db(self, site_config: Dict): """Connexion à une base de données""" try: db_config = site_config['db_config'] connection = mysql.connector.connect( host=db_config['host'], user=db_config['user'], password=db_config['password'], database=db_config['database'], charset='utf8mb4' ) logger.info(f"✅ Connecté à {site_config['name']} ({db_config['database']})") return connection except Exception as e: logger.error(f"❌ Erreur connexion {site_config['name']}: {str(e)}") return None def create_slug(self, titre: str) -> str: """Créer un slug SEO-friendly""" slug = titre.lower() slug = re.sub(r'[éèêë]', 'e', slug) slug = re.sub(r'[àâä]', 'a', slug) slug = re.sub(r'[ôö]', 'o', slug) slug = re.sub(r'[îï]', 'i', slug) slug = re.sub(r'[ùûü]', 'u', slug) slug = re.sub(r'[ç]', 'c', slug) slug = re.sub(r'[^a-z0-9\s-]', '', slug) slug = re.sub(r'[\s-]+', '-', slug) return slug.strip('-') def format_html_content(self, content: str, titre: str, site_name: str) -> str: """Formater le contenu en HTML selon le site""" # Style différent selon le site if site_name == 'EmploiSuisse': # Style emploisuisse avec classes spécifiques html = f'
\n' html += f'
\n' else: # Style ch-jobs html = f'
\n' # Ajouter le titre h1 html += f"

{titre}

\n\n" # Convertir les paragraphes paragraphs = content.split('\n\n') for para in paragraphs: para = para.strip() if not para: continue # Détecter les sous-titres if para.startswith('## '): html += f"

{para[3:]}

\n\n" elif para.startswith('### '): html += f"

{para[4:]}

\n\n" elif para.startswith('#### '): html += f"

{para[5:]}

\n\n" elif para.startswith('- ') or para.startswith('* '): # Liste à puces items = para.split('\n') html += "
    \n" for item in items: if item.startswith('- ') or item.startswith('* '): html += f"
  • {item[2:]}
  • \n" html += "
\n\n" elif re.match(r'^\d+\.', para): # Liste numérotée items = para.split('\n') html += "
    \n" for item in items: if re.match(r'^\d+\.', item): text = re.sub(r'^\d+\.\s*', '', item) html += f"
  1. {text}
  2. \n" html += "
\n\n" else: html += f"

{para}

\n\n" # Fermeture des divs if site_name == 'EmploiSuisse': html += '
\n
' else: html += '' return html def generate_excerpt(self, content: str, max_length: int = 160) -> str: """Générer un extrait""" text = re.sub(r'<[^>]+>', '', content) excerpt = text[:max_length] if len(text) > max_length: excerpt += "..." return excerpt def insert_article_emploisuisse(self, connection, article_data: Dict) -> Optional[int]: """Insérer un article dans la table news de emploisuisse""" try: cursor = connection.cursor() query = """ INSERT INTO news (title, slug, excerpt, content, featured_image, category, tags, author, status, views, meta_title, meta_description, meta_keywords, published_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ values = ( article_data['title'], article_data['slug'], article_data['excerpt'], article_data['content'], article_data['featured_image'], article_data['category'], article_data['tags'], article_data['author'], article_data['status'], 0, # views article_data['title'], article_data['meta_description'], article_data['tags'], article_data['published_at'] ) cursor.execute(query, values) connection.commit() article_id = cursor.lastrowid cursor.close() return article_id except Exception as e: logger.error(f"❌ Erreur insertion emploisuisse: {str(e)}") return None def insert_article_chjobs(self, connection, article_data: Dict) -> Optional[int]: """Insérer un article dans la table blog_posts de ch-jobs""" try: cursor = connection.cursor() query = """ INSERT INTO blog_posts (title, slug, content, excerpt, category, author, is_published, published_at, views, meta_description) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ values = ( article_data['title'], article_data['slug'], article_data['content'], article_data['excerpt'], article_data['category'], article_data['author'], 1, # is_published article_data['published_at'], 0, # views article_data['meta_description'] ) cursor.execute(query, values) connection.commit() article_id = cursor.lastrowid cursor.close() return article_id except Exception as e: logger.error(f"❌ Erreur insertion ch-jobs: {str(e)}") return None def generate_and_publish_article(self, site_key: str, titre: str, category: str = "Emploi") -> bool: """Générer et publier un article sur un site spécifique""" site_config = self.sites_config[site_key] logger.info(f"\n📝 Publication sur {site_config['name']}") logger.info(f" Sujet: {titre[:50]}...") # Générer l'article result = self.ai_manager.generate_article(titre) if result['statut'] != 'succès' or not result['contenu']: logger.error(f"❌ Échec génération pour {site_config['name']}") self.stats['errors'] += 1 return False # Mettre à jour les stats modele = result.get('modele', 'unknown') self.stats[modele] = self.stats.get(modele, 0) + 1 # Formater le contenu content_html = self.format_html_content(result['contenu'], titre, site_config['name']) excerpt = self.generate_excerpt(content_html) # Générer meta description et tags meta_desc = excerpt[:150] if len(excerpt) > 50 else f"Découvrez nos conseils pour {titre[:100]}" tags = self.extract_tags(titre) # Préparer les données selon le site article_data = { 'title': titre, 'slug': self.create_slug(titre), 'content': content_html, 'excerpt': excerpt, 'category': category, 'meta_description': meta_desc, 'author': site_config['author'], 'published_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S") } # Ajouter les champs spécifiques if site_key == 'emploisuisse': article_data['featured_image'] = site_config['default_image'] article_data['tags'] = tags article_data['status'] = 'published' # Connexion à la base connection = self.connect_db(site_config) if not connection: return False # Insertion selon le site try: if site_key == 'emploisuisse': article_id = self.insert_article_emploisuisse(connection, article_data) else: article_id = self.insert_article_chjobs(connection, article_data) if article_id: logger.info(f"✅ Article #{article_id} publié sur {site_config['name']} (via {modele})") self.stats[f'{site_key}_posts'] += 1 return True else: logger.error(f"❌ Échec insertion sur {site_config['name']}") return False finally: connection.close() def extract_tags(self, titre: str) -> str: """Extraire des tags du titre""" mots_vides = ['le', 'la', 'les', 'un', 'une', 'des', 'de', 'du', 'en', 'et', 'pour', 'dans', 'sur'] mots = re.sub(r'[^\w\s]', '', titre.lower()).split() tags = [m for m in mots if m not in mots_vides and len(m) > 3] return ', '.join(tags[:5]) def get_topics(self) -> List[Dict]: """Liste des sujets d'articles à générer""" return [ {'title': 'Comment négocier son salaire en Suisse : le guide complet 2025', 'category': 'Salaires'}, {'title': 'Les secteurs qui recrutent le plus en Suisse cette année', 'category': 'Marché emploi'}, {'title': 'CV Suisse : les erreurs qui coûtent des entretiens', 'category': 'Conseils CV'}, {'title': 'Travailler comme frontalier français : avantages et pièges', 'category': 'Frontaliers'}, {'title': 'Les compétences tech les plus demandées en Suisse en 2025', 'category': 'Tech'}, {'title': 'Permis de travail Suisse : guide complet pour les étrangers', 'category': 'Administratif'}, {'title': 'Entretien d\'embauche en Suisse : les questions pièges', 'category': 'Entretiens'}, {'title': 'Fiscalité pour frontaliers : optimisez vos revenus', 'category': 'Fiscalité'} ] def run_execution(self): """Exécuter une session (publie sur un site différent à chaque fois)""" # Déterminer le site pour cette exécution target_site = self.get_next_site() logger.info(f"\n{'='*70}") logger.info(f"🚀 NOUVELLE EXÉCUTION - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") logger.info(f"🎯 Site cible: {self.sites_config[target_site]['name']}") logger.info(f"{'='*70}") # Sélectionner un sujet aléatoire topics = self.get_topics() selected_topic = random.choice(topics) logger.info(f"📝 Sujet sélectionné: {selected_topic['title']}") # Générer et publier l'article success = self.generate_and_publish_article( target_site, selected_topic['title'], selected_topic['category'] ) # Mettre à jour l'état pour la prochaine exécution if success: self.last_site = target_site self.save_state() # Afficher les statistiques logger.info(f"\n📊 STATISTIQUES SESSION") logger.info(f"{'='*40}") logger.info(f"📝 Article publié sur: {self.sites_config[target_site]['name']}") logger.info(f"✅ Succès: {success}") logger.info(f"🤖 Modèle utilisé: {self.ai_manager.__class__.__name__}") return success class DualBlogScheduler: """Scheduler pour exécution sur deux sites en alternance""" def __init__(self): self.pid_file = "/tmp/dual_blog_scheduler.pid" self.running = True self.setup_signal_handlers() def setup_signal_handlers(self): signal.signal(signal.SIGTERM, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler) def signal_handler(self, signum, frame): logger.info("📡 Arrêt du scheduler...") self.running = False self.cleanup() sys.exit(0) def cleanup(self): if os.path.exists(self.pid_file): os.remove(self.pid_file) def write_pid(self): with open(self.pid_file, 'w') as f: f.write(str(os.getpid())) def run_once(self): """Exécuter une session""" generator = DualBlogPostGenerator() return generator.run_execution() def run_scheduler(self): """Exécuter toutes les 30 minutes""" self.write_pid() logger.info("="*70) logger.info("🕐 DUAL BLOG SCHEDULER DÉMARRÉ") logger.info("📡 Exécution toutes les 30 minutes") logger.info("🔄 Alternance automatique entre:") logger.info(" • EmploiSuisse (roseiujr_emploisuisse)") logger.info(" • CH-Jobs (roseiujr_ch-jobs)") logger.info("="*70) execution_count = 0 while self.running: try: execution_count += 1 logger.info(f"\n{'🔄'*35}") logger.info(f"EXÉCUTION #{execution_count}") logger.info(f"{'🔄'*35}") self.run_once() logger.info("\n⏳ Prochaine exécution dans 30 minutes...") for _ in range(1800): if not self.running: break time.sleep(1) except Exception as e: logger.error(f"❌ Erreur dans la boucle: {str(e)}") import traceback logger.error(traceback.format_exc()) time.sleep(60) def setup_config(): """Configuration initiale et tests""" logger.info("🔧 Configuration du système dual-blog...") # Test des connexions aux deux bases sites = { 'emploisuisse': { 'host': '127.0.0.1', 'user': 'roseiujr_emploisuisse', 'password': '19Sh@hrazad', 'database': 'roseiujr_emploisuisse' }, 'chjobs': { 'host': '127.0.0.1', 'user': 'adminsql', 'password': '19Sh@hrazad.', 'database': 'roseiujr_ch-jobs' } } for site_name, config in sites.items(): try: conn = mysql.connector.connect(**config) cursor = conn.cursor() if site_name == 'emploisuisse': cursor.execute("SHOW TABLES LIKE 'news'") table = cursor.fetchone() if table: logger.info(f"✅ {site_name}: table news trouvée") else: logger.warning(f"⚠️ {site_name}: table news non trouvée") else: cursor.execute("SHOW TABLES LIKE 'blog_posts'") table = cursor.fetchone() if table: logger.info(f"✅ {site_name}: table blog_posts trouvée") else: logger.warning(f"⚠️ {site_name}: table blog_posts non trouvée") cursor.close() conn.close() except Exception as e: logger.error(f"❌ {site_name}: {str(e)}") # Créer fichier .env si nécessaire if not os.path.exists('.env'): with open('.env', 'w') as f: f.write("""# Clés API pour les modèles alternatifs GROK_API_KEY= OPENROUTER_API_KEY= # Obtenez une clé API gratuite: # OpenRouter: https://openrouter.ai/keys # Grok: https://console.x.ai """) logger.info("✅ Fichier .env créé") logger.info("✅ Configuration terminée") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description='Générateur d\'articles pour deux sites en alternance') parser.add_argument('--mode', choices=['scheduler', 'once', 'setup', 'test'], default='scheduler') parser.add_argument('--site', choices=['emploisuisse', 'chjobs'], help='Forcer la publication sur un site spécifique') args = parser.parse_args() if args.mode == 'setup': setup_config() sys.exit(0) if args.mode == 'test': logger.info("🧪 TEST MODE - Publication unique") generator = DualBlogPostGenerator() if args.site: logger.info(f"Forçage sur le site: {args.site}") generator.last_site = 'chjobs' if args.site == 'emploisuisse' else 'emploisuisse' generator.run_execution() sys.exit(0) if args.mode == 'scheduler': logger.info("🎯 DUAL BLOG GENERATOR") logger.info("📡 Fonctionnalités:") logger.info(" ✅ Publication alternée sur 2 sites") logger.info(" ✅ Fallback AI (Pollinations/Grok/OpenRouter)") logger.info(" ✅ Exécution toutes les 30 minutes") logger.info(" ✅ Mode nohup compatible") scheduler = DualBlogScheduler() # Si un site est forcé, on modifie l'état initial if args.site: logger.info(f"⚠️ Mode forcé: démarrage avec {args.site}") state_file = '/tmp/dual_blog_state.json' if os.path.exists(state_file): os.remove(state_file) with open(state_file, 'w') as f: json.dump({'last_site': 'chjobs' if args.site == 'emploisuisse' else 'emploisuisse', 'timestamp': datetime.now().isoformat()}, f) logger.info("\n💡 Commandes utiles:") logger.info(" Démarrer: nohup python3 script.py --mode scheduler > dual_blog.log 2>&1 &") logger.info(" Surveiller: tail -f dual_blog_articles.log") logger.info(" Arrêter: kill $(cat /tmp/dual_blog_scheduler.pid)") logger.info(" Tester: python3 script.py --mode test") scheduler.run_scheduler() elif args.mode == 'once': scheduler = DualBlogScheduler() scheduler.run_once()