mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
feat: introduce audio administration, reel generation, and enhanced analytics with new database schema and API routes.
This commit is contained in:
1 parent
b31a45549a
commit
04373d2881
3 files changed
+72
-39
No files matched your search
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
@@ -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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user