mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
414 lines
14 KiB
TypeScript
414 lines
14 KiB
TypeScript
/**
|
|
* Routes API pour les Reels Facebook et la musique Jamendo
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import type { User, Media, SocialPage } from '@shared/schema';
|
|
import { storage } from '../storage';
|
|
import { freeSoundService, type MusicTrack } from '../services/freesound';
|
|
import { ffmpegService } from '../services/ffmpeg';
|
|
import { facebookService } from '../services/facebook';
|
|
import { cloudinaryService } from '../services/cloudinary';
|
|
import { openRouterService } from '../services/openrouter';
|
|
|
|
export const reelsRouter = Router();
|
|
|
|
// ============================================
|
|
// ROUTES MUSIQUE JAMENDO
|
|
// ============================================
|
|
|
|
/**
|
|
* Recherche de musiques par durée
|
|
* GET /api/music/search?minDuration=10&maxDuration=60&genre=pop&limit=10
|
|
*/
|
|
reelsRouter.get('/music/search', async (req: Request, res: Response) => {
|
|
try {
|
|
const {
|
|
minDuration = '10',
|
|
maxDuration = '120',
|
|
genre,
|
|
limit = '10',
|
|
offset = '0',
|
|
search,
|
|
} = req.query;
|
|
|
|
const tracks = await freeSoundService.searchMusicByDuration({
|
|
minDuration: parseInt(minDuration as string),
|
|
maxDuration: parseInt(maxDuration as string),
|
|
genre: genre as string | undefined,
|
|
limit: parseInt(limit as string),
|
|
offset: parseInt(offset as string),
|
|
search: search as string | undefined,
|
|
});
|
|
|
|
res.json({
|
|
tracks,
|
|
pagination: {
|
|
offset: parseInt(offset as string),
|
|
limit: parseInt(limit as string),
|
|
hasMore: tracks.length === parseInt(limit as string),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Error searching music:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la recherche de musique' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Voir plus de musiques (pagination)
|
|
* GET /api/music/more?offset=10&minDuration=10&maxDuration=60
|
|
*/
|
|
reelsRouter.get('/music/more', async (req: Request, res: Response) => {
|
|
try {
|
|
const {
|
|
minDuration = '10',
|
|
maxDuration = '120',
|
|
genre,
|
|
limit = '10',
|
|
offset = '0',
|
|
search,
|
|
} = req.query;
|
|
|
|
const tracks = await freeSoundService.searchMusicByDuration({
|
|
minDuration: parseInt(minDuration as string),
|
|
maxDuration: parseInt(maxDuration as string),
|
|
genre: genre as string | undefined,
|
|
limit: parseInt(limit as string),
|
|
offset: parseInt(offset as string),
|
|
search: search as string | undefined,
|
|
});
|
|
|
|
res.json({
|
|
tracks,
|
|
pagination: {
|
|
offset: parseInt(offset as string),
|
|
limit: parseInt(limit as string),
|
|
hasMore: tracks.length === parseInt(limit as string),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Error loading more music:', error);
|
|
res.status(500).json({ error: 'Erreur lors du chargement des musiques' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Obtenir les tracks populaires
|
|
* GET /api/music/popular
|
|
*/
|
|
reelsRouter.get('/music/popular', async (req: Request, res: Response) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit as string) || 10;
|
|
const tracks = await freeSoundService.getPopularTracks(limit);
|
|
res.json({ tracks });
|
|
} catch (error) {
|
|
console.error('❌ Error fetching popular music:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la récupération des musiques populaires' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Détails d'un track
|
|
* GET /api/music/:trackId
|
|
*/
|
|
reelsRouter.get('/music/:trackId', async (req: Request, res: Response) => {
|
|
try {
|
|
const { trackId } = req.params;
|
|
const track = await freeSoundService.getMusicDetails(trackId);
|
|
|
|
if (!track) {
|
|
return res.status(404).json({ error: 'Musique non trouvée' });
|
|
}
|
|
|
|
res.json(track);
|
|
} catch (error) {
|
|
console.error('❌ Error fetching music details:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la récupération des détails' });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// ROUTES REELS
|
|
// ============================================
|
|
|
|
/**
|
|
* Générer des textes IA pour un Reel (utilise le même service que les posts)
|
|
* POST /api/reels/generate-text
|
|
*/
|
|
reelsRouter.post('/reels/generate-text', async (req: Request, res: Response) => {
|
|
try {
|
|
const user = req.user as User;
|
|
const { productInfo, model } = req.body;
|
|
|
|
if (!productInfo) {
|
|
return res.status(400).json({ error: 'Informations produit requises' });
|
|
}
|
|
|
|
// Convertir la chaîne de texte simple en objet ProductInfo
|
|
// Si l'utilisateur envoie une chaîne, on l'utilise comme description du produit
|
|
const productInfoObject = typeof productInfo === 'string'
|
|
? {
|
|
name: productInfo.trim(), // Utiliser le texte comme nom
|
|
description: productInfo.trim(), // Et comme description
|
|
}
|
|
: productInfo;
|
|
|
|
const generatedTexts = await openRouterService.generatePostText(productInfoObject, user.id, model);
|
|
|
|
// Sauvegarder la génération
|
|
await storage.createAiGeneration({
|
|
userId: user.id,
|
|
productInfo: productInfoObject,
|
|
generatedTexts,
|
|
});
|
|
|
|
res.json({ variants: generatedTexts });
|
|
} catch (error) {
|
|
console.error('❌ Error generating Reel text:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la génération du texte' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Prévisualiser un Reel (traitement sans publication)
|
|
* POST /api/reels/preview
|
|
*/
|
|
reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
|
try {
|
|
const user = req.user as User;
|
|
const {
|
|
videoMediaId,
|
|
musicTrackId,
|
|
musicUrl,
|
|
overlayText,
|
|
wordDuration = 0.6,
|
|
fontSize = 60,
|
|
musicVolume = 0.25,
|
|
} = req.body;
|
|
|
|
// Récupérer le média vidéo
|
|
const media = await storage.getMediaById(videoMediaId);
|
|
if (!media) {
|
|
return res.status(404).json({ error: 'Vidéo non trouvée' });
|
|
}
|
|
|
|
if (media.type !== 'video') {
|
|
return res.status(400).json({ error: 'Le média doit être une vidéo' });
|
|
}
|
|
|
|
// Récupérer l'URL de la musique si trackId fourni
|
|
let finalMusicUrl = musicUrl;
|
|
if (musicTrackId && !musicUrl) {
|
|
const track = await freeSoundService.getMusicDetails(musicTrackId);
|
|
if (track) {
|
|
finalMusicUrl = track.downloadUrl;
|
|
}
|
|
}
|
|
|
|
// Traiter la vidéo via FFmpeg
|
|
const result = await ffmpegService.processReelFromUrl(media.originalUrl, {
|
|
text: overlayText,
|
|
musicUrl: finalMusicUrl,
|
|
wordDuration,
|
|
fontSize,
|
|
musicVolume,
|
|
});
|
|
|
|
if (!result.success) {
|
|
return res.status(500).json({ error: result.error || 'Erreur de traitement vidéo' });
|
|
}
|
|
|
|
// Retourner la vidéo en base64 pour prévisualisation
|
|
res.json({
|
|
success: true,
|
|
videoBase64: result.videoBase64,
|
|
duration: result.duration,
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Error previewing Reel:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la prévisualisation du Reel' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Créer et publier un Reel
|
|
* POST /api/reels
|
|
*/
|
|
reelsRouter.post('/reels', async (req: Request, res: Response) => {
|
|
try {
|
|
const user = req.user as User;
|
|
const {
|
|
videoMediaId,
|
|
musicTrackId,
|
|
musicUrl,
|
|
overlayText,
|
|
description,
|
|
pageIds,
|
|
scheduledFor,
|
|
wordDuration = 0.6,
|
|
fontSize = 60,
|
|
musicVolume = 0.25,
|
|
} = req.body;
|
|
|
|
// Validation
|
|
if (!videoMediaId) {
|
|
return res.status(400).json({ error: 'Vidéo requise' });
|
|
}
|
|
|
|
if (!pageIds || pageIds.length === 0) {
|
|
return res.status(400).json({ error: 'Au moins une page requise' });
|
|
}
|
|
|
|
// Récupérer le média vidéo
|
|
const media = await storage.getMediaById(videoMediaId);
|
|
if (!media) {
|
|
return res.status(404).json({ error: 'Vidéo non trouvée' });
|
|
}
|
|
|
|
if (media.type !== 'video') {
|
|
return res.status(400).json({ error: 'Le média doit être une vidéo' });
|
|
}
|
|
|
|
// Récupérer l'URL de la musique si trackId fourni
|
|
let finalMusicUrl = musicUrl;
|
|
if (musicTrackId && !musicUrl) {
|
|
const track = await freeSoundService.getMusicDetails(musicTrackId);
|
|
if (track) {
|
|
finalMusicUrl = track.downloadUrl;
|
|
}
|
|
}
|
|
|
|
console.log('🎬 Processing Reel:', {
|
|
videoMediaId,
|
|
hasMusic: !!finalMusicUrl,
|
|
hasText: !!overlayText,
|
|
pageCount: pageIds.length,
|
|
});
|
|
|
|
// Traiter la vidéo via FFmpeg
|
|
const ffmpegResult = await ffmpegService.processReelFromUrl(media.originalUrl, {
|
|
text: overlayText,
|
|
musicUrl: finalMusicUrl,
|
|
wordDuration,
|
|
fontSize,
|
|
musicVolume,
|
|
});
|
|
|
|
if (!ffmpegResult.success || !ffmpegResult.videoBase64) {
|
|
return res.status(500).json({
|
|
error: ffmpegResult.error || 'Erreur de traitement vidéo',
|
|
});
|
|
}
|
|
|
|
// Upload la vidéo traitée sur Cloudinary
|
|
const videoBuffer = Buffer.from(ffmpegResult.videoBase64, 'base64');
|
|
const cloudinaryResult = await cloudinaryService.uploadMedia(
|
|
videoBuffer,
|
|
`reel-${Date.now()}.mp4`,
|
|
user.id,
|
|
'video/mp4'
|
|
);
|
|
|
|
console.log('✅ Reel uploaded to Cloudinary:', cloudinaryResult.originalUrl);
|
|
|
|
// Créer le post en base
|
|
const post = await storage.createPost({
|
|
userId: user.id,
|
|
content: description || overlayText || '',
|
|
aiGenerated: 'false',
|
|
status: scheduledFor ? 'scheduled' : 'draft',
|
|
scheduledFor: scheduledFor ? new Date(scheduledFor) : undefined,
|
|
});
|
|
|
|
// Publier sur chaque page sélectionnée
|
|
const results: { pageId: string; success: boolean; reelId?: string; error?: string }[] = [];
|
|
|
|
for (const pageId of pageIds) {
|
|
try {
|
|
const page = await storage.getSocialPage(pageId);
|
|
if (!page) {
|
|
results.push({ pageId, success: false, error: 'Page non trouvée' });
|
|
continue;
|
|
}
|
|
|
|
if (page.platform !== 'facebook') {
|
|
results.push({ pageId, success: false, error: 'Seules les pages Facebook sont supportées pour les Reels' });
|
|
continue;
|
|
}
|
|
|
|
// Créer l'entrée scheduled_post
|
|
const scheduledPost = await storage.createScheduledPost({
|
|
postId: post.id,
|
|
pageId: page.id,
|
|
postType: 'reel',
|
|
scheduledAt: scheduledFor ? new Date(scheduledFor) : new Date(),
|
|
});
|
|
|
|
// Si pas de planification, publier immédiatement
|
|
if (!scheduledFor) {
|
|
const reelId = await facebookService.publishReel(
|
|
page,
|
|
cloudinaryResult.originalUrl,
|
|
description
|
|
);
|
|
|
|
// Mettre à jour le scheduled_post avec l'ID externe
|
|
await storage.updateScheduledPost(scheduledPost.id, {
|
|
publishedAt: new Date(),
|
|
externalPostId: reelId,
|
|
});
|
|
|
|
results.push({ pageId, success: true, reelId });
|
|
} else {
|
|
results.push({ pageId, success: true, reelId: 'scheduled' });
|
|
}
|
|
} catch (pageError) {
|
|
console.error(`❌ Error publishing Reel to page ${pageId}:`, pageError);
|
|
results.push({
|
|
pageId,
|
|
success: false,
|
|
error: pageError instanceof Error ? pageError.message : 'Erreur inconnue',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Mettre à jour le statut du post
|
|
const allSuccess = results.every(r => r.success);
|
|
await storage.updatePost(post.id, {
|
|
status: scheduledFor ? 'scheduled' : (allSuccess ? 'published' : 'failed'),
|
|
});
|
|
|
|
res.json({
|
|
success: allSuccess,
|
|
postId: post.id,
|
|
results,
|
|
videoUrl: cloudinaryResult.originalUrl,
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Error creating Reel:', error);
|
|
res.status(500).json({
|
|
error: error instanceof Error ? error.message : 'Erreur lors de la création du Reel',
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Obtenir la configuration des services Reels
|
|
* GET /api/reels/config
|
|
*/
|
|
reelsRouter.get('/reels/config', async (req: Request, res: Response) => {
|
|
try {
|
|
const jamendoConfigured = await freeSoundService.testConnection().catch(() => false);
|
|
const ffmpegConfigured = await ffmpegService.healthCheck().catch(() => false);
|
|
|
|
res.json({
|
|
jamendo: { configured: jamendoConfigured },
|
|
ffmpeg: { configured: ffmpegConfigured },
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Error fetching Reels config:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la récupération de la configuration' });
|
|
}
|
|
});
|