diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index 1090e68..3392345 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -56,9 +56,59 @@ export default function Settings() { const logoPublicId = (cloudinaryConfig as any).logoPublicId; setLogoPreview(`https://res.cloudinary.com/${cloudName}/image/upload/${logoPublicId}`); } + if ((cloudinaryConfig as any).logoPublicId) { + const cloudName = (cloudinaryConfig as any).cloudName; + const logoPublicId = (cloudinaryConfig as any).logoPublicId; + setLogoPreview(`https://res.cloudinary.com/${cloudName}/image/upload/${logoPublicId}`); + } } }, [cloudinaryConfig]); + const { data: freesoundConfig } = useQuery({ + queryKey: ['/api/freesound/config'], + }); + + const [freesoundClientId, setFreesoundClientId] = useState(""); + const [freesoundClientSecret, setFreesoundClientSecret] = useState(""); + + useEffect(() => { + if (freesoundConfig) { + setFreesoundClientId((freesoundConfig as any).clientId || ""); + setFreesoundClientSecret((freesoundConfig as any).clientSecret || ""); // Usually masked + } + }, [freesoundConfig]); + + const hasExistingFreesoundConfig = !!freesoundConfig; + + const saveFreesoundMutation = useMutation({ + mutationFn: () => { + const payload: any = { + clientId: freesoundClientId, + }; + + if (freesoundClientSecret && freesoundClientSecret.trim() !== "" && freesoundClientSecret !== "********") { + payload.clientSecret = freesoundClientSecret; + } + + return apiRequest('POST', '/api/freesound/config', payload); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/freesound/config'] }); + toast({ + title: "Configuration sauvegardée", + description: "Vos paramètres Freesound ont été enregistrés", + }); + // On garde l'affichage tel quel ou on remet masked si retourné + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible de sauvegarder la configuration Freesound", + variant: "destructive", + }); + }, + }); + useEffect(() => { if (openrouterConfig) { setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-3.5-sonnet"); @@ -459,6 +509,60 @@ export default function Settings() { + + + + + Configuration Freesound (Musique) + + + Configurez l'API Freesound pour la recherche de musique dans les Reels + + + +
+ + setFreesoundClientId(e.target.value)} + placeholder="Votre Client ID Freesound" + data-testid="input-freesound-client-id" + /> +

+ Disponible dans vos paramètres d'API Freesound +

+
+
+ + setFreesoundClientSecret(e.target.value)} + placeholder={hasExistingFreesoundConfig ? "••••••••" : "Votre Client Secret"} + data-testid="input-freesound-client-secret" + /> +

+ {hasExistingFreesoundConfig ? ( + ✓ Configuré (Laissez vide pour conserver) + ) : ( + "Votre clé secrète ne sera jamais exposée" + )} +

+
+ +
+
+ diff --git a/server/index.ts b/server/index.ts index fa5c47b..27b03ab 100644 --- a/server/index.ts +++ b/server/index.ts @@ -187,6 +187,34 @@ app.use((req, res, next) => { const jamendoClientId = process.env.JAMENDO_CLIENT_ID || '5b6bba49'; jamendoService.configure(jamendoClientId); + // Configurer le service FreeSound pour les Reels + // 1. Essayer de charger depuis la DB (suppose un utilisateur admin ou premier utilisateur) + // Note: Dans un contexte multi-utilisateur, cela devrait être fait par requête. + // Pour l'instant, on charge la config du premier user trouvé ou via variable d'env. + + let freesoundConfigured = false; + try { + const { storage } = await import("./storage"); + const allUsers = await storage.getAllUsers(); + if (allUsers.length > 0) { + const dbConfig = await storage.getFreesoundConfig(allUsers[0].id); + if (dbConfig && dbConfig.clientId && dbConfig.clientSecret) { + freeSoundService.configure(dbConfig.clientId, dbConfig.clientSecret); + freesoundConfigured = true; + log('🎵 FreeSound configured from Database'); + } + } + } catch (err) { + console.warn('Failed to load Freesound config from DB:', err); + } + + if (!freesoundConfigured && process.env.FREESOUND_CLIENT_ID && process.env.FREESOUND_CLIENT_SECRET) { + freeSoundService.configure(process.env.FREESOUND_CLIENT_ID, process.env.FREESOUND_CLIENT_SECRET); + log('🎵 FreeSound configured from Environment Variables'); + } else if (!freesoundConfigured) { + log('⚠️ FreeSound credentials not found - music search will be disabled'); + } + // Configurer le service FFmpeg pour le traitement vidéo (si configuré) if (process.env.FFMPEG_API_URL && process.env.FFMPEG_API_KEY) { ffmpegService.configure(process.env.FFMPEG_API_URL, process.env.FFMPEG_API_KEY); diff --git a/server/routes.ts b/server/routes.ts index 6a1bf93..315fdb2 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -10,8 +10,9 @@ import passport from "./auth"; import { z } from "zod"; import { openRouterService } from "./services/openrouter"; import { cloudinaryService } from "./services/cloudinary"; -import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema"; -import type { User, InsertUser, ScheduledPost } from "@shared/schema"; +import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertFreesoundConfigSchema, updateFreesoundConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema"; +import type { User, InsertUser, ScheduledPost, FreesoundConfig } from "@shared/schema"; +import { freeSoundService } from "./services/freesound"; import { analyticsRouter } from "./routes/analytics"; import { reelsRouter } from "./routes/reels"; @@ -346,6 +347,59 @@ export async function registerRoutes(app: Express): Promise { // Analytics Routes app.use("/api/analytics", requireAuth, analyticsRouter); + // Freesound Configuration Routes + app.get("/api/freesound/config", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const config = await storage.getFreesoundConfig(user.id); + if (!config) { + return res.json(null); + } + return res.json({ + ...config, + clientSecret: config.clientSecret ? "********" : "", + }); + } catch (error) { + console.error("Error fetching Freesound config:", error); + res.status(500).json({ error: "Failed to fetch configuration" }); + } + }); + + app.post("/api/freesound/config", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const data = updateFreesoundConfigSchema.parse(req.body); + + let config = await storage.getFreesoundConfig(user.id); + const updateData: any = { ...data }; + + if (!updateData.clientSecret || updateData.clientSecret === "********") { + delete updateData.clientSecret; + } + + if (config) { + config = await storage.updateFreesoundConfig(user.id, updateData); + } else { + if (!data.clientId || !data.clientSecret) { + return res.status(400).json({ error: "Client ID and Secret are required" }); + } + config = await storage.createFreesoundConfig({ + ...data as any, + userId: user.id + }); + } + + if (config && config.clientId && config.clientSecret) { + freeSoundService.configure(config.clientId, config.clientSecret); + } + + res.json({ success: true }); + } catch (error) { + console.error("Error saving Freesound config:", error); + res.status(500).json({ error: "Failed to save configuration" }); + } + }); + // Reels & Music Routes app.use("/api", requireAuth, reelsRouter); diff --git a/server/storage.ts b/server/storage.ts index e51af0e..c560a97 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -31,6 +31,9 @@ import { musicFavorites, type MusicFavorite, type InsertMusicFavorite, + freesoundConfig, + type FreesoundConfig, + type InsertFreesoundConfig, } from "@shared/schema"; import { db } from "./db"; import { eq, and, gte, lte, desc, asc, isNull, inArray } from "drizzle-orm"; @@ -107,6 +110,11 @@ export interface IStorage { addMusicFavorite(favorite: InsertMusicFavorite): Promise; removeMusicFavorite(userId: string, trackId: string): Promise; isMusicFavorite(userId: string, trackId: string): Promise; + + // Freesound Config + getFreesoundConfig(userId: string): Promise; + createFreesoundConfig(config: InsertFreesoundConfig): Promise; + updateFreesoundConfig(userId: string, config: Partial): Promise; } export class DatabaseStorage implements IStorage { @@ -489,6 +497,25 @@ export class DatabaseStorage implements IStorage { ); return !!fav; } + + // Freesound Config + async getFreesoundConfig(userId: string): Promise { + const [config] = await db.select().from(freesoundConfig).where(eq(freesoundConfig.userId, userId)); + return config || undefined; + } + + async createFreesoundConfig(config: InsertFreesoundConfig): Promise { + const [newConfig] = await db.insert(freesoundConfig).values(config).returning(); + return newConfig; + } + + async updateFreesoundConfig(userId: string, config: Partial): Promise { + const [updated] = await db.update(freesoundConfig) + .set({ ...config, updatedAt: new Date() }) + .where(eq(freesoundConfig.userId, userId)) + .returning(); + return updated; + } } export const storage = new DatabaseStorage(); diff --git a/shared/schema.ts b/shared/schema.ts index d93a9f0..4ecfeec 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -131,6 +131,15 @@ export const openrouterConfig = pgTable("openrouter_config", { updatedAt: timestamp("updated_at").defaultNow(), }); +export const freesoundConfig = pgTable("freesound_config", { + id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), + userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }), + clientId: text("client_id").notNull(), + clientSecret: text("client_secret").notNull(), + createdAt: timestamp("created_at").defaultNow(), + updatedAt: timestamp("updated_at").defaultNow(), +}); + export const media = pgTable("media", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), @@ -250,6 +259,7 @@ export const usersRelations = relations(users, ({ many, one }) => ({ pagePermissions: many(userPagePermissions), cloudinaryConfig: one(cloudinaryConfig), openrouterConfig: one(openrouterConfig), + freesoundConfig: one(freesoundConfig), })); export const cloudinaryConfigRelations = relations(cloudinaryConfig, ({ one }) => ({ @@ -425,4 +435,19 @@ export type InsertPostMedia = z.infer; export type MusicFavorite = typeof musicFavorites.$inferSelect; export type InsertMusicFavorite = z.infer; +export const insertFreesoundConfigSchema = createInsertSchema(freesoundConfig).omit({ + id: true, + createdAt: true, + updatedAt: true, +}); + +export const updateFreesoundConfigSchema = insertFreesoundConfigSchema.partial({ + clientId: true, + clientSecret: true, +}); + +export type FreesoundConfig = typeof freesoundConfig.$inferSelect; +export type InsertFreesoundConfig = z.infer; +export type UpdateFreesoundConfig = z.infer; +