feat: Implement a new FastAPI service for video processing with FFmpeg, TTS, and subtitle generation, including font management and diagnostic endpoints.

This commit is contained in:
Michael committed 2026-02-03 15:24:10 +01:00
1 parent 838d168239
commit 1f7d0d1675
8 files changed
+494 -56

No files matched your search

+3 -4
View File
@@ -183,10 +183,9 @@ app.use((req, res, next) => {
// Initialiser l'utilisateur admin par défaut avant de démarrer le serveur
await ensureAdminUserExists();
// Configurer le service FreeSound pour les Reels
const freeSoundClientId = process.env.FREESOUND_CLIENT_ID || 'uTn5gEjjJRzPvyGP1eAN';
const freeSoundClientSecret = process.env.FREESOUND_CLIENT_SECRET || 'QGUgFJFcDwXPfVOflMTXBBJ8C5l4UuKf4t9KAYBX';
freeSoundService.configure(freeSoundClientId, freeSoundClientSecret);
// Configurer le service Jamendo pour les Reels
const jamendoClientId = process.env.JAMENDO_CLIENT_ID || '5b6bba49';
jamendoService.configure(jamendoClientId);
// Configurer le service FFmpeg pour le traitement vidéo (si configuré)
if (process.env.FFMPEG_API_URL && process.env.FFMPEG_API_KEY) {
+18
View File
@@ -63,6 +63,24 @@ export async function migrate() {
ADD COLUMN IF NOT EXISTS "last_token_check" timestamp;
`);
// music_favorites
await client.query(`
CREATE TABLE IF NOT EXISTS "music_favorites" (
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"track_id" text NOT NULL,
"title" text NOT NULL,
"artist" text NOT NULL,
"album_name" text,
"duration" integer NOT NULL,
"preview_url" text NOT NULL,
"download_url" text NOT NULL,
"image_url" text,
"license" text,
"created_at" timestamp DEFAULT now()
);
`);
console.log("[Migration] Safe migration completed.");
} catch (error) {
console.error("[Migration] Error during migration:", error);
+118 -8
View File
@@ -5,7 +5,7 @@
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 { jamendoService, type MusicTrack } from '../services/jamendo';
import { ffmpegService } from '../services/ffmpeg';
import { facebookService } from '../services/facebook';
import { cloudinaryService } from '../services/cloudinary';
@@ -32,7 +32,7 @@ reelsRouter.get('/music/search', async (req: Request, res: Response) => {
search,
} = req.query;
const tracks = await freeSoundService.searchMusicByDuration({
const tracks = await jamendoService.searchMusicByDuration({
minDuration: parseInt(minDuration as string),
maxDuration: parseInt(maxDuration as string),
genre: genre as string | undefined,
@@ -70,7 +70,7 @@ reelsRouter.get('/music/more', async (req: Request, res: Response) => {
search,
} = req.query;
const tracks = await freeSoundService.searchMusicByDuration({
const tracks = await jamendoService.searchMusicByDuration({
minDuration: parseInt(minDuration as string),
maxDuration: parseInt(maxDuration as string),
genre: genre as string | undefined,
@@ -100,7 +100,7 @@ reelsRouter.get('/music/more', async (req: Request, res: Response) => {
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);
const tracks = await jamendoService.getPopularTracks(limit);
res.json({ tracks });
} catch (error) {
console.error('❌ Error fetching popular music:', error);
@@ -108,6 +108,116 @@ reelsRouter.get('/music/popular', async (req: Request, res: Response) => {
}
});
// ============================================
// ROUTES FAVORIS MUSIQUE
// ============================================
/**
* Obtenir les favoris de l'utilisateur
* GET /api/music/favorites
*/
reelsRouter.get('/music/favorites', async (req: Request, res: Response) => {
try {
const user = req.user as User;
const favorites = await storage.getMusicFavorites(user.id);
// Convertir en format MusicTrack pour le client
const tracks: MusicTrack[] = favorites.map(f => ({
id: f.trackId,
title: f.title,
artist: f.artist,
albumName: f.albumName || '',
duration: f.duration,
previewUrl: f.previewUrl,
downloadUrl: f.downloadUrl,
imageUrl: f.imageUrl || '',
license: f.license || '',
}));
res.json({ tracks, isFavorites: true });
} catch (error) {
console.error('❌ Error fetching music favorites:', error);
res.status(500).json({ error: 'Erreur lors de la récupération des favoris' });
}
});
/**
* Vérifier si un track est en favori
* GET /api/music/favorites/check/:trackId
*/
reelsRouter.get('/music/favorites/check/:trackId', async (req: Request, res: Response) => {
try {
const user = req.user as User;
const { trackId } = req.params;
const isFavorite = await storage.isMusicFavorite(user.id, trackId);
res.json({ isFavorite });
} catch (error) {
console.error('❌ Error checking music favorite:', error);
res.status(500).json({ error: 'Erreur lors de la vérification du favori' });
}
});
/**
* Ajouter un favori
* POST /api/music/favorites
*/
reelsRouter.post('/music/favorites', async (req: Request, res: Response) => {
try {
const user = req.user as User;
const { trackId, title, artist, albumName, duration, previewUrl, downloadUrl, imageUrl, license } = req.body;
if (!trackId || !title) {
return res.status(400).json({ error: 'trackId et title requis' });
}
// Vérifier si déjà en favori
const alreadyFavorite = await storage.isMusicFavorite(user.id, trackId);
if (alreadyFavorite) {
return res.json({ success: true, message: 'Déjà en favori' });
}
await storage.addMusicFavorite({
userId: user.id,
trackId,
title,
artist: artist || '',
albumName: albumName || null,
duration: duration || 0,
previewUrl: previewUrl || '',
downloadUrl: downloadUrl || '',
imageUrl: imageUrl || null,
license: license || null,
});
res.json({ success: true });
} catch (error) {
console.error('❌ Error adding music favorite:', error);
res.status(500).json({ error: 'Erreur lors de l\'ajout du favori' });
}
});
/**
* Supprimer un favori
* DELETE /api/music/favorites/:trackId
*/
reelsRouter.delete('/music/favorites/:trackId', async (req: Request, res: Response) => {
try {
const user = req.user as User;
const { trackId } = req.params;
await storage.removeMusicFavorite(user.id, trackId);
res.json({ success: true });
} catch (error) {
console.error('❌ Error removing music favorite:', error);
res.status(500).json({ error: 'Erreur lors de la suppression du favori' });
}
});
// ============================================
// ROUTE DÉTAILS TRACK (après les routes /music/favorites et /music/popular)
// ============================================
/**
* Détails d'un track
* GET /api/music/:trackId
@@ -115,7 +225,7 @@ reelsRouter.get('/music/popular', async (req: Request, res: Response) => {
reelsRouter.get('/music/:trackId', async (req: Request, res: Response) => {
try {
const { trackId } = req.params;
const track = await freeSoundService.getMusicDetails(trackId);
const track = await jamendoService.getMusicDetails(trackId);
if (!track) {
return res.status(404).json({ error: 'Musique non trouvée' });
@@ -204,7 +314,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
// Récupérer l'URL de la musique si trackId fourni
let finalMusicUrl = musicUrl;
if (musicTrackId && !musicUrl) {
const track = await freeSoundService.getMusicDetails(musicTrackId);
const track = await jamendoService.getMusicDetails(musicTrackId);
if (track) {
finalMusicUrl = track.downloadUrl;
}
@@ -309,7 +419,7 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
// Récupérer l'URL de la musique si trackId fourni
let finalMusicUrl = musicUrl;
if (musicTrackId && !musicUrl) {
const track = await freeSoundService.getMusicDetails(musicTrackId);
const track = await jamendoService.getMusicDetails(musicTrackId);
if (track) {
finalMusicUrl = track.downloadUrl;
}
@@ -460,7 +570,7 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
*/
reelsRouter.get('/reels/config', async (req: Request, res: Response) => {
try {
const jamendoConfigured = await freeSoundService.testConnection().catch(() => false);
const jamendoConfigured = await jamendoService.testConnection().catch(() => false);
const ffmpegConfigured = await ffmpegService.healthCheck().catch(() => false);
res.json({
+40
View File
@@ -28,6 +28,9 @@ import {
type InsertOpenrouterConfig,
type UserPagePermission,
type InsertUserPagePermission,
musicFavorites,
type MusicFavorite,
type InsertMusicFavorite,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, gte, lte, desc, asc, isNull, inArray } from "drizzle-orm";
@@ -98,6 +101,12 @@ export interface IStorage {
deletePagePermission(userId: string, pageId: string): Promise<void>;
deleteAllUserPagePermissions(userId: string): Promise<void>;
getUserAccessiblePages(userId: string): Promise<SocialPage[]>;
// Music Favorites
getMusicFavorites(userId: string): Promise<MusicFavorite[]>;
addMusicFavorite(favorite: InsertMusicFavorite): Promise<MusicFavorite>;
removeMusicFavorite(userId: string, trackId: string): Promise<void>;
isMusicFavorite(userId: string, trackId: string): Promise<boolean>;
}
export class DatabaseStorage implements IStorage {
@@ -449,6 +458,37 @@ export class DatabaseStorage implements IStorage {
return permissions.map(p => p.social_pages);
}
// Music Favorites
async getMusicFavorites(userId: string): Promise<MusicFavorite[]> {
return db.select().from(musicFavorites)
.where(eq(musicFavorites.userId, userId))
.orderBy(desc(musicFavorites.createdAt));
}
async addMusicFavorite(favorite: InsertMusicFavorite): Promise<MusicFavorite> {
const [created] = await db.insert(musicFavorites).values(favorite).returning();
return created;
}
async removeMusicFavorite(userId: string, trackId: string): Promise<void> {
await db.delete(musicFavorites).where(
and(
eq(musicFavorites.userId, userId),
eq(musicFavorites.trackId, trackId)
)
);
}
async isMusicFavorite(userId: string, trackId: string): Promise<boolean> {
const [fav] = await db.select().from(musicFavorites).where(
and(
eq(musicFavorites.userId, userId),
eq(musicFavorites.trackId, trackId)
)
);
return !!fav;
}
}
export const storage = new DatabaseStorage();