Reels : programmer la publication d'une vidéo déjà montée

Nouvelle page « Programmer un Reel » (/reel/schedule) : envoi ou choix
d'une vidéo, description, pages Facebook/TikTok, date et heure. Le Reel
est publié tel quel par le planificateur, sans passer par le générateur.

- POST /api/posts valide les Reels (une vidéo, pages Facebook/TikTok)
- vignette extraite à l'envoi des vidéos pour l'historique

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tFkVXQDATYfw5aFtjQahi
This commit is contained in:
Claude committed 2026-09-29 13:16:06 +00:00
1 parent 2c732c11b9
commit 188706f4dc
5 files changed
+335 -4

No files matched your search

+27 -2
View File
@@ -21,6 +21,7 @@ import { externalRouter } from "./routes/external";
import { tiktokRouter } from "./routes/tiktok";
import { facebookRouter } from "./routes/facebook";
import { legalRouter } from "./routes/legal";
import { createVideoThumbnail } from "./services/thumbnail";
import { insertAudioTrackSchema } from "@shared/schema";
import * as musicMetadata from "music-metadata";
@@ -721,13 +722,20 @@ export async function registerRoutes(app: Express): Promise<Server> {
req.file.mimetype
);
// Détection basique basée sur le mimetype ou l'extension
const isVideo = req.file.mimetype.startsWith("video/") || /\.(mp4|mov|avi|mkv|webm)$/i.test(req.file.originalname);
// Vignette extraite tant que la vidéo est sur le disque : elle survit à la
// suppression du fichier après publication (historique illustré)
const thumbnailUrl = isVideo ? await createVideoThumbnail(req.file.path) : null;
// 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 file:", err));
const mediaItem = await storage.createMedia({
userId,
// Détection basique basée sur le mimetype ou le résultat Cloudinary
type: req.file.mimetype.startsWith("video/") || req.file.originalname.match(/\.(mp4|mov|avi|mkv)$/i) ? "video" : "image",
type: isVideo ? "video" : "image",
thumbnailUrl,
cloudinaryPublicId: uploadResult.publicId,
originalUrl: uploadResult.originalUrl,
facebookFeedUrl: uploadResult.facebookFeedUrl,
@@ -1297,6 +1305,23 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(400).json({ error: "Les stories nécessitent au moins un média (image ou vidéo)" });
}
// Reel déjà monté : une seule vidéo, publiée telle quelle sur Facebook ou TikTok
if (postType === 'reel') {
if (finalMediaItems.length !== 1) {
return res.status(400).json({ error: "Un Reel nécessite exactement une vidéo" });
}
const reelMedia = await storage.getMediaById(finalMediaItems[0].mediaId);
if (!reelMedia || reelMedia.type !== 'video' || (user.role !== 'admin' && reelMedia.userId !== userId)) {
return res.status(400).json({ error: "Le média d'un Reel doit être une de vos vidéos" });
}
for (const pageId of Array.isArray(pageIds) ? pageIds : []) {
const page = await storage.getSocialPage(pageId);
if (!page || (page.platform !== 'facebook' && page.platform !== 'tiktok')) {
return res.status(400).json({ error: "Les Reels ne peuvent être publiés que sur Facebook ou TikTok" });
}
}
}
// Security: Verify user has access to all specified pages (unless admin)
if (user.role !== 'admin' && pageIds && Array.isArray(pageIds) && pageIds.length > 0) {
const accessiblePages = await storage.getUserAccessiblePages(userId);