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({
|
||||
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<HTMLInputElement>) => {
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
+41
-29
@@ -977,48 +977,60 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user