feat: Implement dynamic Freesound configuration (DB, API, UI)

This commit is contained in:
Michael committed 2026-02-03 15:45:08 +01:00
1 parent 1f7d0d1675
commit 62562d37f1
5 files changed
+240 -2

No files matched your search

+104
View File
@@ -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() {
</CardContent>
</Card>
<Card className="rounded-2xl border-border/50 shadow-lg">
<CardHeader className="p-6">
<CardTitle className="flex items-center gap-2">
<SettingsIcon className="w-5 h-5" />
Configuration Freesound (Musique)
</CardTitle>
<CardDescription>
Configurez l'API Freesound pour la recherche de musique dans les Reels
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="freesoundClientId">Client ID</Label>
<Input
id="freesoundClientId"
type="text"
value={freesoundClientId}
onChange={(e) => setFreesoundClientId(e.target.value)}
placeholder="Votre Client ID Freesound"
data-testid="input-freesound-client-id"
/>
<p className="text-xs text-muted-foreground">
Disponible dans vos paramètres d'API Freesound
</p>
</div>
<div className="space-y-2">
<Label htmlFor="freesoundClientSecret">Client Secret</Label>
<Input
id="freesoundClientSecret"
type="password"
value={freesoundClientSecret}
onChange={(e) => setFreesoundClientSecret(e.target.value)}
placeholder={hasExistingFreesoundConfig ? "••••••••" : "Votre Client Secret"}
data-testid="input-freesound-client-secret"
/>
<p className="text-xs text-muted-foreground">
{hasExistingFreesoundConfig ? (
<span className="text-green-600 dark:text-green-400">✓ Configuré (Laissez vide pour conserver)</span>
) : (
"Votre clé secrète ne sera jamais exposée"
)}
</p>
</div>
<Button
onClick={() => saveFreesoundMutation.mutate()}
disabled={saveFreesoundMutation.isPending || !freesoundClientId}
data-testid="button-save-freesound"
className="w-full"
>
{saveFreesoundMutation.isPending ? "Enregistrement..." : "Enregistrer Freesound"}
</Button>
</CardContent>
</Card>
<Card className="rounded-2xl border-border/50 shadow-lg">
<CardHeader className="p-6">
<CardTitle className="flex items-center gap-2">
+28
View File
@@ -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);
+56 -2
View File
@@ -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<Server> {
// 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);
+27
View File
@@ -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<MusicFavorite>;
removeMusicFavorite(userId: string, trackId: string): Promise<void>;
isMusicFavorite(userId: string, trackId: string): Promise<boolean>;
// Freesound Config
getFreesoundConfig(userId: string): Promise<FreesoundConfig | undefined>;
createFreesoundConfig(config: InsertFreesoundConfig): Promise<FreesoundConfig>;
updateFreesoundConfig(userId: string, config: Partial<InsertFreesoundConfig>): Promise<FreesoundConfig>;
}
export class DatabaseStorage implements IStorage {
@@ -489,6 +497,25 @@ export class DatabaseStorage implements IStorage {
);
return !!fav;
}
// Freesound Config
async getFreesoundConfig(userId: string): Promise<FreesoundConfig | undefined> {
const [config] = await db.select().from(freesoundConfig).where(eq(freesoundConfig.userId, userId));
return config || undefined;
}
async createFreesoundConfig(config: InsertFreesoundConfig): Promise<FreesoundConfig> {
const [newConfig] = await db.insert(freesoundConfig).values(config).returning();
return newConfig;
}
async updateFreesoundConfig(userId: string, config: Partial<InsertFreesoundConfig>): Promise<FreesoundConfig> {
const [updated] = await db.update(freesoundConfig)
.set({ ...config, updatedAt: new Date() })
.where(eq(freesoundConfig.userId, userId))
.returning();
return updated;
}
}
export const storage = new DatabaseStorage();
+25
View File
@@ -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<typeof insertPostMediaSchema>;
export type MusicFavorite = typeof musicFavorites.$inferSelect;
export type InsertMusicFavorite = z.infer<typeof insertMusicFavoriteSchema>;
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<typeof insertFreesoundConfigSchema>;
export type UpdateFreesoundConfig = z.infer<typeof updateFreesoundConfigSchema>;