diff --git a/client/src/pages/audio-admin.tsx b/client/src/pages/audio-admin.tsx index 5816164..7397f3f 100644 --- a/client/src/pages/audio-admin.tsx +++ b/client/src/pages/audio-admin.tsx @@ -32,9 +32,11 @@ export default function AudioAdmin() { }); const uploadMutation = useMutation({ - mutationFn: async (file: File) => { + mutationFn: async (files: File[]) => { const formData = new FormData(); - formData.append("file", file); + for (const file of files) { + formData.append("files", file); + } const res = await fetch("/api/audio-tracks", { method: "POST", @@ -48,11 +50,15 @@ export default function AudioAdmin() { return res.json(); }, - onSuccess: () => { + onSuccess: (data) => { 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({ - title: "Succès", - description: "La piste audio a été ajoutée à la bibliothèque", + title: "Upload terminé", + description: failCount > 0 + ? `${successCount} fichier(s) ajouté(s), ${failCount} échec(s)` + : `${successCount} fichier(s) ajouté(s) à la bibliothèque`, }); }, onError: (error: Error) => { @@ -94,20 +100,21 @@ export default function AudioAdmin() { }); const handleFileUpload = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; + const files = Array.from(e.target.files || []); + if (files.length === 0) return; - if (!file.type.startsWith("audio/")) { + const invalidFiles = files.filter(f => !f.type.startsWith("audio/")); + if (invalidFiles.length > 0) { toast({ 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", }); return; } setIsUploading(true); - uploadMutation.mutate(file); + uploadMutation.mutate(files); // Reset input if (e.target) { @@ -173,6 +180,7 @@ export default function AudioAdmin() { id="audio-upload" className="hidden" accept="audio/*" + multiple onChange={handleFileUpload} disabled={isUploading} /> diff --git a/server/migrate.ts b/server/migrate.ts index 5fb1a84..a050c26 100644 --- a/server/migrate.ts +++ b/server/migrate.ts @@ -117,6 +117,19 @@ export async function migrate() { 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."); } catch (error) { console.error("[Migration] Error during migration:", error); diff --git a/server/routes.ts b/server/routes.ts index c8274fd..4658dbc 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -977,48 +977,60 @@ export async function registerRoutes(app: Express): Promise { } }); - // Audio Tracks Management (Admin Only) - app.post("/api/audio-tracks", requireAdmin, audioUpload.single("file"), async (req, res) => { + // Audio Tracks Management (Admin Only) - Multi-file upload + app.post("/api/audio-tracks", requireAdmin, audioUpload.array("files", 20), async (req, res) => { 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" }); } - console.log(`🎵 Processing audio upload: ${req.file.originalname} (${(req.file.size / 1024 / 1024).toFixed(2)} MB)`); - 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) - const uploadResult = await cloudinaryService.uploadMedia( - req.file.path, - req.file.originalname, - user.id, - "video" - ); + for (const file of files) { + try { + console.log(`🎵 Processing audio upload: ${file.originalname} (${(file.size / 1024 / 1024).toFixed(2)} MB)`); - // Clean up temp file immediately after upload to free disk space - await fs.promises.unlink(req.file.path).catch(err => console.error("Failed to cleanup temp audio file:", err)); + // Upload to Cloudinary; audio is categorized as "video" in Cloudinary + 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({ - userId: user.id, - title: title, - fileName: req.file.originalname, - url: uploadResult.originalUrl, - duration: 0, // We could extract duration, but 0 is default - }); + const title = file.originalname.replace(/\.[^/.]+$/, ""); - res.json(track); - } catch (error) { - console.error("Error uploading audio track:", error); + const track = await storage.createAudioTrack({ + userId: user.id, + title: title, + fileName: file.originalname, + url: uploadResult.originalUrl, + duration: 0, + }); - // Attempt cleanup on error - if (req.file && req.file.path) { - await fs.promises.unlink(req.file.path).catch(() => { }); + results.push({ success: true, track }); + } catch (fileError) { + // 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 }); } });