feat: introduce audio administration, reel generation, and enhanced analytics with new database schema and API routes.

This commit is contained in:
Michael committed 2026-03-02 13:11:37 +01:00
1 parent b31a45549a
commit 04373d2881
3 files changed
+72 -39

No files matched your search

+18 -10
View File
@@ -32,9 +32,11 @@ export default function AudioAdmin() {
}); });
const uploadMutation = useMutation({ const uploadMutation = useMutation({
mutationFn: async (file: File) => { mutationFn: async (files: File[]) => {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); for (const file of files) {
formData.append("files", file);
}
const res = await fetch("/api/audio-tracks", { const res = await fetch("/api/audio-tracks", {
method: "POST", method: "POST",
@@ -48,11 +50,15 @@ export default function AudioAdmin() {
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["/api/audio-tracks"] }); queryClient.invalidateQueries({ queryKey: ["/api/audio-tracks"] });
const successCount = data.results?.filter((r: any) => r.success).length ?? 0;
const failCount = data.results?.filter((r: any) => !r.success).length ?? 0;
toast({ toast({
title: "Succès", title: "Upload terminé",
description: "La piste audio a été ajoutée à la bibliothèque", description: failCount > 0
? `${successCount} fichier(s) ajouté(s), ${failCount} échec(s)`
: `${successCount} fichier(s) ajouté(s) à la bibliothèque`,
}); });
}, },
onError: (error: Error) => { onError: (error: Error) => {
@@ -94,20 +100,21 @@ export default function AudioAdmin() {
}); });
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const files = Array.from(e.target.files || []);
if (!file) return; if (files.length === 0) return;
if (!file.type.startsWith("audio/")) { const invalidFiles = files.filter(f => !f.type.startsWith("audio/"));
if (invalidFiles.length > 0) {
toast({ toast({
title: "Format invalide", title: "Format invalide",
description: "Veuillez sélectionner un fichier audio (MP3, WAV, etc.)", description: "Certains fichiers ne sont pas des fichiers audio (MP3, WAV, etc.)",
variant: "destructive", variant: "destructive",
}); });
return; return;
} }
setIsUploading(true); setIsUploading(true);
uploadMutation.mutate(file); uploadMutation.mutate(files);
// Reset input // Reset input
if (e.target) { if (e.target) {
@@ -173,6 +180,7 @@ export default function AudioAdmin() {
id="audio-upload" id="audio-upload"
className="hidden" className="hidden"
accept="audio/*" accept="audio/*"
multiple
onChange={handleFileUpload} onChange={handleFileUpload}
disabled={isUploading} disabled={isUploading}
/> />
+13
View File
@@ -117,6 +117,19 @@ export async function migrate() {
ADD COLUMN IF NOT EXISTS "page_engagement" integer DEFAULT 0; ADD COLUMN IF NOT EXISTS "page_engagement" integer DEFAULT 0;
`); `);
// audio_tracks (bibliothèque audio interne)
await client.query(`
CREATE TABLE IF NOT EXISTS "audio_tracks" (
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" varchar REFERENCES "users"("id") ON DELETE cascade,
"title" text NOT NULL,
"file_name" text NOT NULL,
"url" text NOT NULL,
"duration" integer DEFAULT 0,
"created_at" timestamp DEFAULT now()
);
`);
console.log("[Migration] Safe migration completed."); console.log("[Migration] Safe migration completed.");
} catch (error) { } catch (error) {
console.error("[Migration] Error during migration:", error); console.error("[Migration] Error during migration:", error);
+41 -29
View File
@@ -977,48 +977,60 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
// Audio Tracks Management (Admin Only) // Audio Tracks Management (Admin Only) - Multi-file upload
app.post("/api/audio-tracks", requireAdmin, audioUpload.single("file"), async (req, res) => { app.post("/api/audio-tracks", requireAdmin, audioUpload.array("files", 20), async (req, res) => {
try { try {
if (!req.file) { const files = req.files as Express.Multer.File[];
if (!files || files.length === 0) {
return res.status(400).json({ error: "No audio file uploaded" }); return res.status(400).json({ error: "No audio file uploaded" });
} }
console.log(`🎵 Processing audio upload: ${req.file.originalname} (${(req.file.size / 1024 / 1024).toFixed(2)} MB)`);
const user = req.user as User; const user = req.user as User;
const results = [];
// Upload to Cloudinary using file path (streamed from disk) as resource_type: "video" (audio is categorized as video in cloudinary) for (const file of files) {
const uploadResult = await cloudinaryService.uploadMedia( try {
req.file.path, console.log(`🎵 Processing audio upload: ${file.originalname} (${(file.size / 1024 / 1024).toFixed(2)} MB)`);
req.file.originalname,
user.id,
"video"
);
// Clean up temp file immediately after upload to free disk space // Upload to Cloudinary; audio is categorized as "video" in Cloudinary
await fs.promises.unlink(req.file.path).catch(err => console.error("Failed to cleanup temp audio file:", err)); const uploadResult = await cloudinaryService.uploadMedia(
file.path,
file.originalname,
user.id,
"video"
);
const title = req.body.title || req.file.originalname.replace(/\.[^/.]+$/, ""); // Use provided title or fallback to filename w/o ext // Clean up temp file immediately after upload
await fs.promises.unlink(file.path).catch(err => console.error("Failed to cleanup temp audio file:", err));
const track = await storage.createAudioTrack({ const title = file.originalname.replace(/\.[^/.]+$/, "");
userId: user.id,
title: title,
fileName: req.file.originalname,
url: uploadResult.originalUrl,
duration: 0, // We could extract duration, but 0 is default
});
res.json(track); const track = await storage.createAudioTrack({
} catch (error) { userId: user.id,
console.error("Error uploading audio track:", error); title: title,
fileName: file.originalname,
url: uploadResult.originalUrl,
duration: 0,
});
// Attempt cleanup on error results.push({ success: true, track });
if (req.file && req.file.path) { } catch (fileError) {
await fs.promises.unlink(req.file.path).catch(() => { }); // Clean up on per-file error
await fs.promises.unlink(file.path).catch(() => { });
results.push({ success: false, fileName: file.originalname, error: fileError instanceof Error ? fileError.message : "Upload failed" });
}
} }
const errorMessage = error instanceof Error ? error.message : "Failed to upload audio track"; res.json({ results });
} catch (error) {
console.error("Error uploading audio tracks:", error);
// Cleanup all remaining temp files
if (req.files) {
for (const file of req.files as Express.Multer.File[]) {
await fs.promises.unlink(file.path).catch(() => { });
}
}
const errorMessage = error instanceof Error ? error.message : "Failed to upload audio tracks";
res.status(500).json({ error: errorMessage }); res.status(500).json({ error: errorMessage });
} }
}); });