mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
feat: Replace Jamendo with FreeSound API for Reel music
This commit is contained in:
1 parent
c6ecb195ef
commit
973bf5186c
3 files changed
+264
-11
No files matched your search
+5
-3
@@ -13,6 +13,7 @@ import { ensureAdminUserExists } from "./init-admin";
|
|||||||
import { startTokenCron } from "./cron";
|
import { startTokenCron } from "./cron";
|
||||||
import { migrate } from "./migrate";
|
import { migrate } from "./migrate";
|
||||||
import { jamendoService } from "./services/jamendo";
|
import { jamendoService } from "./services/jamendo";
|
||||||
|
import { freeSoundService } from "./services/freesound";
|
||||||
import { ffmpegService } from "./services/ffmpeg";
|
import { ffmpegService } from "./services/ffmpeg";
|
||||||
|
|
||||||
const app = express();
|
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
|
// Initialiser l'utilisateur admin par défaut avant de démarrer le serveur
|
||||||
await ensureAdminUserExists();
|
await ensureAdminUserExists();
|
||||||
|
|
||||||
// Configurer le service Jamendo pour les Reels
|
// Configurer le service FreeSound pour les Reels
|
||||||
const jamendoClientId = process.env.JAMENDO_CLIENT_ID || 'b42d9f71'; // Demo client ID
|
const freeSoundClientId = process.env.FREESOUND_CLIENT_ID || 'uTn5gEjjJRzPvyGP1eAN';
|
||||||
jamendoService.configure(jamendoClientId);
|
const freeSoundClientSecret = process.env.FREESOUND_CLIENT_SECRET || 'QGUgFJFcDwXPfVOflMTXBBJ8C5l4UuKf4t9KAYBX';
|
||||||
|
freeSoundService.configure(freeSoundClientId, freeSoundClientSecret);
|
||||||
|
|
||||||
// Configurer le service FFmpeg pour le traitement vidéo (si configuré)
|
// Configurer le service FFmpeg pour le traitement vidéo (si configuré)
|
||||||
if (process.env.FFMPEG_API_URL && process.env.FFMPEG_API_KEY) {
|
if (process.env.FFMPEG_API_URL && process.env.FFMPEG_API_KEY) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import type { User, Media, SocialPage } from '@shared/schema';
|
import type { User, Media, SocialPage } from '@shared/schema';
|
||||||
import { storage } from '../storage';
|
import { storage } from '../storage';
|
||||||
import { jamendoService, type MusicTrack } from '../services/jamendo';
|
import { freeSoundService, type MusicTrack } from '../services/freesound';
|
||||||
import { ffmpegService } from '../services/ffmpeg';
|
import { ffmpegService } from '../services/ffmpeg';
|
||||||
import { facebookService } from '../services/facebook';
|
import { facebookService } from '../services/facebook';
|
||||||
import { cloudinaryService } from '../services/cloudinary';
|
import { cloudinaryService } from '../services/cloudinary';
|
||||||
@@ -32,7 +32,7 @@ reelsRouter.get('/music/search', async (req: Request, res: Response) => {
|
|||||||
search,
|
search,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const tracks = await jamendoService.searchMusicByDuration({
|
const tracks = await freeSoundService.searchMusicByDuration({
|
||||||
minDuration: parseInt(minDuration as string),
|
minDuration: parseInt(minDuration as string),
|
||||||
maxDuration: parseInt(maxDuration as string),
|
maxDuration: parseInt(maxDuration as string),
|
||||||
genre: genre as string | undefined,
|
genre: genre as string | undefined,
|
||||||
@@ -70,7 +70,7 @@ reelsRouter.get('/music/more', async (req: Request, res: Response) => {
|
|||||||
search,
|
search,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const tracks = await jamendoService.searchMusicByDuration({
|
const tracks = await freeSoundService.searchMusicByDuration({
|
||||||
minDuration: parseInt(minDuration as string),
|
minDuration: parseInt(minDuration as string),
|
||||||
maxDuration: parseInt(maxDuration as string),
|
maxDuration: parseInt(maxDuration as string),
|
||||||
genre: genre as string | undefined,
|
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) => {
|
reelsRouter.get('/music/popular', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const limit = parseInt(req.query.limit as string) || 10;
|
const limit = parseInt(req.query.limit as string) || 10;
|
||||||
const tracks = await jamendoService.getPopularTracks(limit);
|
const tracks = await freeSoundService.getPopularTracks(limit);
|
||||||
res.json({ tracks });
|
res.json({ tracks });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error fetching popular music:', 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) => {
|
reelsRouter.get('/music/:trackId', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { trackId } = req.params;
|
const { trackId } = req.params;
|
||||||
const track = await jamendoService.getMusicDetails(trackId);
|
const track = await freeSoundService.getMusicDetails(trackId);
|
||||||
|
|
||||||
if (!track) {
|
if (!track) {
|
||||||
return res.status(404).json({ error: 'Musique non trouvée' });
|
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
|
// Récupérer l'URL de la musique si trackId fourni
|
||||||
let finalMusicUrl = musicUrl;
|
let finalMusicUrl = musicUrl;
|
||||||
if (musicTrackId && !musicUrl) {
|
if (musicTrackId && !musicUrl) {
|
||||||
const track = await jamendoService.getMusicDetails(musicTrackId);
|
const track = await freeSoundService.getMusicDetails(musicTrackId);
|
||||||
if (track) {
|
if (track) {
|
||||||
finalMusicUrl = track.downloadUrl;
|
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
|
// Récupérer l'URL de la musique si trackId fourni
|
||||||
let finalMusicUrl = musicUrl;
|
let finalMusicUrl = musicUrl;
|
||||||
if (musicTrackId && !musicUrl) {
|
if (musicTrackId && !musicUrl) {
|
||||||
const track = await jamendoService.getMusicDetails(musicTrackId);
|
const track = await freeSoundService.getMusicDetails(musicTrackId);
|
||||||
if (track) {
|
if (track) {
|
||||||
finalMusicUrl = track.downloadUrl;
|
finalMusicUrl = track.downloadUrl;
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
reelsRouter.get('/reels/config', async (req: Request, res: Response) => {
|
reelsRouter.get('/reels/config', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const jamendoConfigured = await jamendoService.testConnection().catch(() => false);
|
const jamendoConfigured = await freeSoundService.testConnection().catch(() => false);
|
||||||
const ffmpegConfigured = await ffmpegService.healthCheck().catch(() => false);
|
const ffmpegConfigured = await ffmpegService.healthCheck().catch(() => false);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -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<MusicTrack[]> {
|
||||||
|
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<MusicTrack | null> {
|
||||||
|
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<MusicTrack[]> {
|
||||||
|
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<boolean> {
|
||||||
|
try {
|
||||||
|
const tracks = await this.getPopularTracks(1);
|
||||||
|
return tracks.length > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const freeSoundService = new FreeSoundService();
|
||||||
Reference in new issue
Block a user