diff --git a/server/index.ts b/server/index.ts index cdfac1e..24c7abe 100644 --- a/server/index.ts +++ b/server/index.ts @@ -13,6 +13,7 @@ import { ensureAdminUserExists } from "./init-admin"; import { startTokenCron } from "./cron"; import { migrate } from "./migrate"; import { jamendoService } from "./services/jamendo"; +import { freeSoundService } from "./services/freesound"; import { ffmpegService } from "./services/ffmpeg"; const app = express(); @@ -181,9 +182,10 @@ app.use((req, res, next) => { // Initialiser l'utilisateur admin par défaut avant de démarrer le serveur await ensureAdminUserExists(); - // Configurer le service Jamendo pour les Reels - const jamendoClientId = process.env.JAMENDO_CLIENT_ID || 'b42d9f71'; // Demo client ID - jamendoService.configure(jamendoClientId); + // 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 FFmpeg pour le traitement vidéo (si configuré) if (process.env.FFMPEG_API_URL && process.env.FFMPEG_API_KEY) { diff --git a/server/routes/reels.ts b/server/routes/reels.ts index 5ece076..c77d2f1 100644 --- a/server/routes/reels.ts +++ b/server/routes/reels.ts @@ -5,7 +5,7 @@ import { Router, Request, Response } from 'express'; import type { User, Media, SocialPage } from '@shared/schema'; import { storage } from '../storage'; -import { jamendoService, type MusicTrack } from '../services/jamendo'; +import { freeSoundService, type MusicTrack } from '../services/freesound'; 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 jamendoService.searchMusicByDuration({ + const tracks = await freeSoundService.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 jamendoService.searchMusicByDuration({ + const tracks = await freeSoundService.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 jamendoService.getPopularTracks(limit); + const tracks = await freeSoundService.getPopularTracks(limit); res.json({ tracks }); } catch (error) { console.error('❌ Error fetching popular music:', error); @@ -115,7 +115,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 jamendoService.getMusicDetails(trackId); + const track = await freeSoundService.getMusicDetails(trackId); if (!track) { return res.status(404).json({ error: 'Musique non trouvée' }); @@ -200,7 +200,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 jamendoService.getMusicDetails(musicTrackId); + const track = await freeSoundService.getMusicDetails(musicTrackId); if (track) { finalMusicUrl = track.downloadUrl; } @@ -273,7 +273,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 jamendoService.getMusicDetails(musicTrackId); + const track = await freeSoundService.getMusicDetails(musicTrackId); if (track) { finalMusicUrl = track.downloadUrl; } @@ -399,7 +399,7 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => { */ reelsRouter.get('/reels/config', async (req: Request, res: Response) => { try { - const jamendoConfigured = await jamendoService.testConnection().catch(() => false); + const jamendoConfigured = await freeSoundService.testConnection().catch(() => false); const ffmpegConfigured = await ffmpegService.healthCheck().catch(() => false); res.json({ diff --git a/server/services/freesound.ts b/server/services/freesound.ts new file mode 100644 index 0000000..e1e694b --- /dev/null +++ b/server/services/freesound.ts @@ -0,0 +1,251 @@ +/** + * FreeSound Music API Service + * + * Intégration avec l'API FreeSound pour rechercher et récupérer des sons + * libres de droits pour les Reels. + * + * Documentation: https://freesound.org/docs/api/ + */ + +interface FreeSoundResult { + id: number; + name: string; + duration: number; + username: string; + tags: string[]; + license: string; + previews: { + 'preview-hq-mp3': string; + 'preview-lq-mp3': string; + 'preview-hq-ogg': string; + 'preview-lq-ogg': string; + }; + images: { + waveform_m: string; + waveform_l: string; + spectral_m: string; + spectral_l: string; + }; + download?: string; +} + +interface FreeSoundSearchResponse { + count: number; + next: string | null; + previous: string | null; + results: FreeSoundResult[]; +} + +export interface MusicTrack { + id: string; + title: string; + artist: string; + albumName: string; + duration: number; // en secondes + previewUrl: string; // URL audio streaming + downloadUrl: string; // URL téléchargement + imageUrl: string; // Waveform image + license: string; +} + +interface MusicSearchParams { + minDuration?: number; // Durée min en secondes + maxDuration?: number; // Durée max en secondes + genre?: string; // Genre musical / tag + limit?: number; // Nombre de résultats (default: 10) + offset?: number; // Offset pour pagination + search?: string; // Recherche texte libre +} + +interface FreeSoundConfig { + clientId: string; + clientSecret: string; +} + +const FREESOUND_API_BASE = 'https://freesound.org/apiv2'; + +export class FreeSoundService { + private config: FreeSoundConfig | null = null; + + /** + * Configure le service avec les credentials FreeSound + */ + configure(clientId: string, clientSecret: string): void { + this.config = { clientId, clientSecret }; + console.log('🎵 FreeSound Service configured'); + } + + /** + * Vérifie que le service est configuré + */ + private ensureConfigured(): FreeSoundConfig { + if (!this.config) { + throw new Error('FreeSound Service not configured. Call configure() first.'); + } + return this.config; + } + + /** + * Convertit un résultat FreeSound en format MusicTrack unifié + */ + private mapResult(result: FreeSoundResult): MusicTrack { + return { + id: result.id.toString(), + title: result.name, + artist: result.username, + albumName: 'FreeSound', + duration: Math.round(result.duration), + previewUrl: result.previews['preview-hq-mp3'] || result.previews['preview-lq-mp3'], + downloadUrl: result.previews['preview-hq-mp3'], // On utilise le preview HQ + imageUrl: result.images?.waveform_m || '', + license: result.license, + }; + } + + /** + * Recherche des musiques par durée + * + * @param params - Paramètres de recherche + * @returns Liste de tracks correspondants + */ + async searchMusicByDuration(params: MusicSearchParams = {}): Promise { + const config = this.ensureConfigured(); + + const { + minDuration = 10, + maxDuration = 120, + genre, + limit = 10, + offset = 0, + search, + } = params; + + // Construction du filtre de durée + const durationFilter = `duration:[${minDuration} TO ${maxDuration}]`; + + // Recherche de musiques/sons musicaux + let query = search || 'music'; + if (genre) { + query = `${query} ${genre}`; + } + + const queryParams = new URLSearchParams({ + token: config.clientSecret, // FreeSound utilise le token dans l'URL pour les requêtes simples + query: query, + filter: durationFilter, + page_size: limit.toString(), + page: Math.floor(offset / limit + 1).toString(), + fields: 'id,name,duration,username,tags,license,previews,images', + sort: 'rating_desc', + }); + + const url = `${FREESOUND_API_BASE}/search/text/?${queryParams.toString()}`; + + console.log('🎵 Searching FreeSound:', { + query, + duration: `${minDuration}-${maxDuration}s`, + limit, + offset, + }); + + try { + const response = await fetch(url); + + if (!response.ok) { + const errorText = await response.text(); + console.error('FreeSound API error:', response.status, errorText); + throw new Error(`FreeSound API error: ${response.status}`); + } + + const data = await response.json() as FreeSoundSearchResponse; + + const tracks = data.results.map(result => this.mapResult(result)); + + console.log(`✅ Found ${tracks.length} tracks on FreeSound`); + return tracks; + + } catch (error) { + console.error('❌ FreeSound search error:', error); + throw error; + } + } + + /** + * Récupère les détails d'un son par son ID + */ + async getMusicDetails(trackId: string): Promise { + const config = this.ensureConfigured(); + + const url = `${FREESOUND_API_BASE}/sounds/${trackId}/?token=${config.clientSecret}&fields=id,name,duration,username,tags,license,previews,images`; + + try { + const response = await fetch(url); + + if (!response.ok) { + console.error('FreeSound API error:', response.status); + return null; + } + + const result = await response.json() as FreeSoundResult; + return this.mapResult(result); + + } catch (error) { + console.error('❌ FreeSound getMusicDetails error:', error); + return null; + } + } + + /** + * Récupère des sons populaires (utilisé comme fallback ou découverte) + */ + async getPopularTracks(limit: number = 10): Promise { + const config = this.ensureConfigured(); + + const queryParams = new URLSearchParams({ + token: config.clientSecret, + query: 'music loop beat', + filter: 'duration:[15 TO 90]', + page_size: limit.toString(), + fields: 'id,name,duration,username,tags,license,previews,images', + sort: 'downloads_desc', + }); + + const url = `${FREESOUND_API_BASE}/search/text/?${queryParams.toString()}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`FreeSound API error: ${response.status}`); + } + + const data = await response.json() as FreeSoundSearchResponse; + return data.results.map(result => this.mapResult(result)); + + } catch (error) { + console.error('❌ FreeSound getPopularTracks error:', error); + return []; + } + } + + /** + * Retourne l'URL de téléchargement/preview + */ + getDownloadUrl(track: MusicTrack): string { + return track.downloadUrl; + } + + /** + * Vérifie la validité de la configuration + */ + async testConnection(): Promise { + try { + const tracks = await this.getPopularTracks(1); + return tracks.length > 0; + } catch { + return false; + } + } +} + +export const freeSoundService = new FreeSoundService();