🎬 Code Final - Gestionnaire YouTube + IA

Application complète avec masquage des tokens API

📱 Interface Frontend
HTML + CSS + JavaScript
🎨 Interface utilisateur complète avec design moderne, gestion des états, animations et intégration IA.
index.html
<!DOCTYPE html> <html lang="fr"> <head> <meta charset="UTF-8"> <title>🎬 Gestion des titres YouTube (local)</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> /* ===== Design System moderne ===== */ :root { --bg: #0b1020; --bg-elev: #0f152a; --card: #121a34; --accent: #6ea8ff; --text: #e9ecf8; --success: #4ade80; --warning: #fbbf24; --shadow: 0 10px 30px rgba(0,0,0,.35); --radius: 14px; } body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; color: var(--text); background: radial-gradient(1200px 600px at 10% -10%, rgba(139,92,246,.20), transparent), var(--bg); } .container { max-width: 1100px; margin: 32px auto; padding: 0 20px; } h1 { background: linear-gradient(90deg, var(--accent), #8b5cf6); -webkit-background-clip: text; color: transparent; font-size: clamp(26px, 4vw, 38px); } /* ===== Composants UI ===== */ .toolbar { display: flex; gap: 10px; padding: 12px; background: var(--bg-elev); border-radius: var(--radius); box-shadow: var(--shadow); } button { padding: 10px 14px; border: 0; border-radius: 10px; cursor: pointer; transition: transform .08s ease; } .primary { background: linear-gradient(135deg, var(--accent), #a0c2ff); color: #07132a; } .success { background: linear-gradient(135deg, var(--success), #6ee76e); } .card { background: var(--card); border: 1px solid #1e2a55; border-radius: var(--radius); padding: 16px; transition: transform .2s ease; animation: fadeIn .35s ease; } .card:hover { transform: translateY(-3px); } #list { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 14px; } @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } </style> </head> <body> <div class="container"> <h1>🎬 Gestion des titres YouTube</h1> <p class="subtitle">Renommez rapidement vos vidéos avec l'IA</p> <div class="toolbar"> <button class="primary" onclick="load()">📺 Charger les vidéos</button> <input id="q" type="search" placeholder="Filtrer par titre…" oninput="filterRows()"> </div> <!-- Panneau d'amélioration IA --> <div class="enhancement-panel"> <h3>🚀 Amélioration automatique par IA</h3> <div class="enhancement-options"> <select id="titleStyle"> <option value="engaging">Engageant (émojis, questions)</option> <option value="professional">Professionnel</option> <option value="clickbait">Accrocheur</option> </select> <select id="contentCategory"> <option value="tech">Technologie</option> <option value="gaming">Gaming</option> <option value="lifestyle">Lifestyle</option> </select> </div> <div class="row"> <button class="success" onclick="previewEnhancements()">👀 Aperçu IA</button> <button class="primary" onclick="enhanceAllTitles()">✨ Appliquer tout</button> </div> </div> <div id="list"></div> </div> <script> // ===== Variables globales ===== let rows = []; let filtered = []; let originalTitles = {}; let enhancedTitles = {}; // ===== Fonctions utilitaires ===== function formatViews(n) { n = Number(n || 0); if (n >= 1e6) return (n/1e6).toFixed(1) + 'M'; if (n >= 1e3) return (n/1e3).toFixed(1) + 'K'; return n.toString(); } // ===== Chargement des vidéos ===== async function load() { const list = document.getElementById('list'); list.innerHTML = '<div>🔄 Chargement...</div>'; try { const res = await fetch('/api/my-videos'); const data = await res.json(); if (data.success) { rows = data.videos || []; filtered = rows; // Sauvegarder les titres originaux originalTitles = {}; rows.forEach(v => originalTitles[v.id] = v.title); render(); } } catch (e) { list.innerHTML = '❌ Erreur: ' + e.message; } } // ===== Rendu de l'interface ===== function render() { const list = document.getElementById('list'); list.innerHTML = ''; filtered.forEach(v => { const hasPreview = enhancedTitles[v.id]; const isChanged = hasPreview && enhancedTitles[v.id] !== originalTitles[v.id]; const card = document.createElement('div'); card.className = 'card'; card.innerHTML = ` <div class="row"> <span class="badge">${formatViews(v.views)} vues</span> <span class="badge">${v.privacyStatus}</span> </div> <div class="row"> <input class="title ${isChanged ? 'changed' : ''}" id="t-${v.id}" value="${hasPreview ? enhancedTitles[v.id] : v.title}"> <button class="primary" onclick="save('${v.id}')">💾 Sauver</button> </div> ${isChanged ? `<div class="preview">Original: "${originalTitles[v.id]}"</div>` : ''} `; list.appendChild(card); }); } // ===== Filtrage des vidéos ===== function filterRows() { const q = document.getElementById('q').value.toLowerCase(); filtered = rows.filter(r => r.title.toLowerCase().includes(q)); render(); } // ===== Sauvegarde d'un titre ===== async function save(id) { const el = document.getElementById('t-' + id); const title = el.value.trim(); if (!title) return alert('Titre vide'); if (title.length > 100) return alert('Max 100 caractères'); try { const r = await fetch(`/api/videos/${id}/title`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title}) }); const out = await r.json(); if (out.success) { const video = rows.find(v => v.id === id); if (video) video.title = title; alert('✅ Titre enregistré'); } else { alert('❌ ' + (out.error || 'Erreur inconnue')); } } catch (e) { alert('❌ Erreur réseau: ' + e.message); } } // ===== Amélioration par IA ===== async function previewEnhancements() { if (!rows.length) { return alert('⚠️ Chargez d\'abord vos vidéos'); } const style = document.getElementById('titleStyle').value; const category = document.getElementById('contentCategory').value; try { const response = await fetch('/api/enhance-titles', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ videos: filtered.length > 0 ? filtered : rows, style, category, language: 'fr' }) }); const result = await response.json(); if (result.success) { enhancedTitles = result.enhancedTitles || {}; render(); alert(`✅ ${result.processed} titres améliorés par l'IA !`); } else { alert('❌ ' + result.error); } } catch (error) { alert('❌ Erreur IA: ' + error.message); } } async function enhanceAllTitles() { if (!Object.keys(enhancedTitles).length) { return alert('⚠️ Générez d\'abord un aperçu'); } if (!confirm('Appliquer ces améliorations ?')) return; const videosToUpdate = Object.keys(enhancedTitles); let successful = 0; for (const videoId of videosToUpdate) { try { const response = await fetch(`/api/videos/${videoId}/title`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title: enhancedTitles[videoId]}) }); const result = await response.json(); if (result.success) successful++; // Délai pour éviter le rate limiting await new Promise(resolve => setTimeout(resolve, 200)); } catch (e) { console.error('Erreur pour ' + videoId, e); } } alert(`✅ ${successful}/${videosToUpdate.length} titres mis à jour !`); enhancedTitles = {}; render(); } </script> </body> </html>
⚙️ Serveur Backend
Node.js + Express
🚀 Serveur complet avec YouTube API, OAuth 2.0, et intégration ChatGPT pour l'amélioration des titres.
server.js
const express = require('express'); const { google } = require('googleapis'); const path = require('path'); const app = express(); const PORT = 3005; /* ====== CONFIGURATION (À PERSONNALISER) ====== */ const CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com'; const CLIENT_SECRET = 'YOUR_GOOGLE_CLIENT_SECRET'; const REDIRECT_URI = 'http://localhost:3005'; const REFRESH_TOKEN = 'YOUR_GOOGLE_REFRESH_TOKEN'; // 🔑 CLÉ API OPENAI (À CONFIGURER) const OPENAI_API_KEY = 'sk-proj-YOUR_OPENAI_API_KEY_HERE'; /* ====== OAuth & YouTube API ====== */ const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); if (REFRESH_TOKEN) oauth2Client.setCredentials({ refresh_token: REFRESH_TOKEN }); const youtube = google.youtube({ version: 'v3', auth: oauth2Client }); /* ====== Middlewares ====== */ app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); /* ====== Helpers ====== */ async function ensureValidToken() { const access = await oauth2Client.getAccessToken(); if (!access?.token) throw new Error('Token invalide'); } /* ====== CHATGPT INTEGRATION ====== */ async function callChatGPT(prompt) { try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'gpt-3.5-turbo', messages: [ { role: 'system', content: 'Tu es expert en optimisation YouTube. Crée des titres accrocheurs, SEO, max 100 caractères.' }, { role: 'user', content: prompt } ], max_tokens: 100, temperature: 0.7 }) }); if (!response.ok) { throw new Error(`OpenAI Error: ${response.status}`); } const data = await response.json(); return data.choices[0]?.message?.content?.trim() || ''; } catch (error) { console.error('❌ ChatGPT Error:', error); throw error; } } function buildPrompt(originalTitle, style, category, language, views) { const styleDesc = { engaging: 'engageant avec émojis et questions', professional: 'professionnel et informatif', clickbait: 'accrocheur mais éthique' }; return ` Améliore ce titre YouTube : "${originalTitle}" Contraintes : - Maximum 100 caractères - Style : ${styleDesc[style] || 'engageant'} - Catégorie : ${category} - Langue : ${language} - ${views} vues actuellement Le nouveau titre doit : 1. Être plus accrocheur 2. Inclure mots-clés SEO 3. Susciter curiosité 4. Rester fidèle au contenu Réponds uniquement avec le nouveau titre. `.trim(); } /* ====== ROUTES OAUTH ====== */ const SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']; app.get('/auth', (req, res) => { const url = oauth2Client.generateAuthUrl({ access_type: 'offline', prompt: 'consent', scope: SCOPES }); res.redirect(url); }); app.get('/oauth2callback', async (req, res) => { try { const { code } = req.query; const { tokens } = await oauth2Client.getToken(code); oauth2Client.setCredentials(tokens); res.send(`<h2>✅ Autorisation réussie</h2> <pre>${JSON.stringify(tokens, null, 2)}</pre> <p>Copiez le refresh_token dans votre configuration</p>`); } catch (e) { res.status(500).send('Erreur OAuth: ' + e.message); } }); /* ====== API ROUTES ====== */ /** GET /api/my-videos - Récupère toutes les vidéos */ app.get('/api/my-videos', async (req, res) => { try { await ensureValidToken(); // 1. Récupérer la playlist "uploads" const ch = await youtube.channels.list({ part: 'contentDetails', mine: true }); const items = ch.data.items || []; if (!items.length) return res.json({ success: true, videos: [] }); const uploadsId = items[0].contentDetails?.relatedPlaylists?.uploads; if (!uploadsId) return res.json({ success: true, videos: [] }); // 2. Récupérer tous les IDs de vidéos let videoIds = []; let nextPageToken = ''; do { const pl = await youtube.playlistItems.list({ part: 'contentDetails', playlistId: uploadsId, maxResults: 50, pageToken: nextPageToken }); const batch = (pl.data.items || []) .map(it => it.contentDetails?.videoId) .filter(Boolean); videoIds.push(...batch); nextPageToken = pl.data.nextPageToken; } while (nextPageToken); if (!videoIds.length) return res.json({ success: true, videos: [] }); // 3. Récupérer les détails par chunks de 50 const detailed = []; for (let i = 0; i < videoIds.length; i += 50) { const chunk = videoIds.slice(i, i + 50); const vd = await youtube.videos.list({ part: 'snippet,statistics,status', id: chunk.join(',') }); if (vd.data.items?.length) detailed.push(...vd.data.items); } // 4. Formater les données const videos = detailed.map(v => ({ id: v.id, title: v.snippet?.title || '', views: Number(v.statistics?.viewCount || 0), privacyStatus: v.status?.privacyStatus || 'public' })); res.json({ success: true, total: videos.length, videos }); } catch (error) { console.error('❌ /api/my-videos:', error); res.status(500).json({ success: false, error: error.message }); } }); /** PUT /api/videos/:videoId/title - Met à jour un titre */ app.put('/api/videos/:videoId/title', async (req, res) => { try { await ensureValidToken(); const { videoId } = req.params; const { title } = req.body; // Validation if (!title?.trim()) { return res.status(400).json({ success: false, error: 'Titre vide' }); } if (title.length > 100) { return res.status(400).json({ success: false, error: 'Max 100 caractères' }); } // Récupérer les infos actuelles const vr = await youtube.videos.list({ part: 'snippet', id: videoId }); if (!vr.data.items?.length) { return res.status(404).json({ success: false, error: 'Vidéo non trouvée' }); } const snippet = vr.data.items[0].snippet; const oldTitle = snippet.title; // Mise à jour await youtube.videos.update({ part: 'snippet', requestBody: { id: videoId, snippet: { ...snippet, title: title.trim() } } }); res.json({ success: true, message: 'Titre modifié', oldTitle, newTitle: title.trim(), videoId }); } catch (error) { console.error('❌ PUT /api/videos/title:', error); res.status(500).json({ success: false, error: error.message }); } }); /** POST /api/enhance-titles - Améliore les titres avec ChatGPT */ app.post('/api/enhance-titles', async (req, res) => { try { if (!OPENAI_API_KEY || OPENAI_API_KEY.includes('YOUR_')) { return res.status(400).json({ success: false, error: 'Clé OpenAI non configurée' }); } const { videos, style = 'engaging', category = 'auto', language = 'fr' } = req.body; if (!videos || !Array.isArray(videos)) { return res.status(400).json({ success: false, error: 'Paramètre "videos" requis' }); } const enhancedTitles = {}; const errors = []; // Traiter chaque vidéo for (const video of videos) { try { console.log(`🤖 Amélioration: "${video.title}"`); const prompt = buildPrompt(video.title, style, category, language, video.views); const enhancedTitle = await