const express = require('express');
const { google } = require('googleapis');
const path = require('path');
const app = express();
const PORT = 3005;
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';
const OPENAI_API_KEY = 'sk-proj-YOUR_OPENAI_API_KEY_HERE';
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 });
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
async function ensureValidToken() {
const access = await oauth2Client.getAccessToken();
if (!access?.token) throw new Error('Token invalide');
}
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();
}
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);
}
});
app.get('/api/my-videos', async (req, res) => {
try {
await ensureValidToken();
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: [] });
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: [] });
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);
}
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
});
}
});
app.put('/api/videos/:videoId/title', async (req, res) => {
try {
await ensureValidToken();
const { videoId } = req.params;
const { title } = req.body;
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' });
}
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;
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
});
}
});
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 = [];
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