From 0e9622b5ebb7c04f0c61b2ab13abc4c0dfd32580 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Mon, 2 Mar 2026 11:33:22 +0100 Subject: [PATCH] feat: Implement internal MP3 management and logo overlay functionality for Reels. --- client/src/App.tsx | 6 + client/src/components/sidebar.tsx | 2 +- client/src/pages/audio-admin.tsx | 275 ++++++++++++++++++++++++ client/src/pages/mobile/audio-admin.tsx | 2 + client/src/pages/new-reel.tsx | 66 ++++++ ffmpeg-service/main.py | 24 +++ server/routes.ts | 123 +++++++++++ server/routes/reels.ts | 58 ++++- server/services/ffmpeg.ts | 5 + server/storage.ts | 28 +++ shared/schema.ts | 29 +++ task.md | 22 +- 12 files changed, 632 insertions(+), 8 deletions(-) create mode 100644 client/src/pages/audio-admin.tsx create mode 100644 client/src/pages/mobile/audio-admin.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 061c14e..63111bf 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -60,6 +60,11 @@ const SqlAdmin = createResponsiveRoute( () => import("@/pages/mobile/sql") ); +const AudioAdmin = createResponsiveRoute( + () => import("@/pages/audio-admin"), + () => import("@/pages/mobile/audio-admin") +); + const UsersAdmin = createResponsiveRoute( () => import("@/pages/users-admin"), () => import("@/pages/mobile/users-admin") @@ -134,6 +139,7 @@ function Router() { {() => } {() => } {() => } + {() => } {() => } {() => } diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx index c63103c..3599ff8 100644 --- a/client/src/components/sidebar.tsx +++ b/client/src/components/sidebar.tsx @@ -1,4 +1,4 @@ -import { Home, PlusCircle, Calendar, Images, Users, Bot, Clock, Settings, Database, UserCog, LogOut, ChevronLeft, ChevronRight, Wand2, BarChart3, Video } from "lucide-react"; +import { Home, PlusCircle, Calendar, Images, Users, Bot, Clock, Settings, Database, UserCog, LogOut, ChevronLeft, ChevronRight, Wand2, BarChart3, Video, Music } from "lucide-react"; import { Link, useLocation } from "wouter"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; diff --git a/client/src/pages/audio-admin.tsx b/client/src/pages/audio-admin.tsx new file mode 100644 index 0000000..0f3fb62 --- /dev/null +++ b/client/src/pages/audio-admin.tsx @@ -0,0 +1,275 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import Sidebar from "@/components/sidebar"; +import TopBar from "@/components/topbar"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/hooks/use-toast"; +import { Trash2, Music, Upload, Loader2, Play, Pause } from "lucide-react"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; + +interface AudioTrack { + id: number; + userId: number; + title: string; + url: string; + duration: number | null; + createdAt: string; +} + +export default function AudioAdmin() { + const [sidebarOpen, setSidebarOpen] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [playingTrackId, setPlayingTrackId] = useState(null); + const [audioElement, setAudioElement] = useState(null); + + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const { data: tracks = [], isLoading } = useQuery({ + queryKey: ["/api/audio-tracks"], + }); + + const uploadMutation = useMutation({ + mutationFn: async (file: File) => { + const formData = new FormData(); + formData.append("audio", file); + + const res = await fetch("/api/audio-tracks", { + method: "POST", + body: formData, + }); + + if (!res.ok) { + const error = await res.json(); + throw new Error(error.error || "Erreur lors de l'upload"); + } + + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/audio-tracks"] }); + toast({ + title: "Succès", + description: "La piste audio a été ajoutée à la bibliothèque", + }); + }, + onError: (error: Error) => { + toast({ + title: "Erreur", + description: error.message, + variant: "destructive", + }); + }, + onSettled: () => { + setIsUploading(false); + } + }); + + const deleteMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await fetch(`/api/audio-tracks/${id}`, { + method: "DELETE", + }); + + if (!res.ok) { + throw new Error("Erreur lors de la suppression"); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/audio-tracks"] }); + toast({ + title: "Succès", + description: "La piste audio a été supprimée", + }); + }, + onError: (error: Error) => { + toast({ + title: "Erreur", + description: error.message, + variant: "destructive", + }); + } + }); + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (!file.type.startsWith("audio/")) { + toast({ + title: "Format invalide", + description: "Veuillez sélectionner un fichier audio (MP3, WAV, etc.)", + variant: "destructive", + }); + return; + } + + setIsUploading(true); + uploadMutation.mutate(file); + + // Reset input + if (e.target) { + e.target.value = ""; + } + }; + + const togglePlay = (track: AudioTrack) => { + if (playingTrackId === track.id) { + audioElement?.pause(); + setPlayingTrackId(null); + } else { + if (audioElement) { + audioElement.pause(); + } + + const newAudio = new Audio(track.url); + newAudio.play(); + + newAudio.onended = () => { + setPlayingTrackId(null); + }; + + setAudioElement(newAudio); + setPlayingTrackId(track.id); + } + }; + + return ( +
+ {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + +
+ setSidebarOpen(false)} /> +
+ +
+ setSidebarOpen(!sidebarOpen)} /> + +
+
+
+

+ + Bibliothèque Audio +

+

+ Gérez les musiques disponibles pour la création de Reels +

+
+ +
+ + +
+
+ + + + Musiques ({tracks.length}) + + Ces pistes pourront être sélectionnées par les utilisateurs lors de la création d'un nouveau Reel. + + + + {isLoading ? ( +
+ +
+ ) : tracks.length === 0 ? ( +
+ +

Aucune musique

+

+ Vous n'avez pas encore ajouté de piste audio. Les musiques ajoutées ici seront utilisables pour les Reels. +

+
+ ) : ( +
+ {tracks.map((track) => ( +
+
+
+ +
+
+

{track.title}

+

+ {format(new Date(track.createdAt), "dd MMM yyyy", { locale: fr })} +

+
+
+ +
+ + + +
+
+ ))} +
+ )} +
+
+
+
+
+ ); +} diff --git a/client/src/pages/mobile/audio-admin.tsx b/client/src/pages/mobile/audio-admin.tsx new file mode 100644 index 0000000..e6e268c --- /dev/null +++ b/client/src/pages/mobile/audio-admin.tsx @@ -0,0 +1,2 @@ +import AudioAdmin from "../audio-admin"; +export default AudioAdmin; diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx index b1d9a28..0f47d78 100644 --- a/client/src/pages/new-reel.tsx +++ b/client/src/pages/new-reel.tsx @@ -97,6 +97,24 @@ export default function NewReel() { // État pour stocker les musiques chargées manuellement const [loadedTracks, setLoadedTracks] = useState([]); + // --- Added for Internal Audio Tracks --- + const { data: internalTracksResponse = [], isLoading: internalTracksLoading } = useQuery({ + queryKey: ['/api/audio-tracks'], + }); + + const internalTracks: MusicTrack[] = (internalTracksResponse || []).map(t => ({ + id: `internal_${t.id}`, + title: t.title, + artist: "Bibliothèque Interne", + albumName: "Upload", + duration: t.duration || 0, + previewUrl: t.url, + downloadUrl: t.url, + imageUrl: "", + license: "Internal" + })); + // --- End Internal Tracks --- + // Recherche de musiques par durée const { data: musicData, isLoading: musicLoading, refetch: refetchMusic } = useQuery<{ tracks: MusicTrack[] }>({ queryKey: ['/api/music/search', selectedVideo?.id, musicOffset], @@ -539,6 +557,54 @@ export default function NewReel() {