mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 23:18:12 +02:00
feat(reels): file d'attente persistante des rendus (reel_jobs)
L'ancienne file comptait les posts « processing » : un redémarrage pendant un rendu la bloquait définitivement. Les rendus passent désormais par une table reel_jobs, réservée avec FOR UPDATE SKIP LOCKED, avec heartbeat et reprise des jobs interrompus au démarrage (2 tentatives maximum). - Pipeline vidéo et pipeline images (Remotion) extraits des routes vers server/services/reels/, les routes ne font que valider (zod) et mettre en file - Rendus d'images suivis en base au lieu d'une Map en mémoire - storeName conservé pour les jobs mis en attente - Publication Facebook en binaire : l'URL relative /uploads/... transmise auparavant n'était pas téléchargeable par Facebook - Job en échec si toutes les pages échouent ou si la voix demandée manque (le service Python renseigne enfin tts_error) - La voix choisie sur la page images est réellement utilisée - sync-info mesure la voix Gemini avec sa clé - Timeouts sur les appels au service FFmpeg, vignettes via execFile, /debug-ffmpeg protégé par la clé API, plus de voix anglaise en secours Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
This commit is contained in:
19 files changed
+1071
-901
No files matched your search
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Enregistrement et publication d'un Reel rendu, commun aux Reels vidéo et
|
||||
* images.
|
||||
*
|
||||
* La vidéo est toujours envoyée en binaire : les fichiers vivent sous
|
||||
* /uploads, une URL relative que Facebook ne peut pas télécharger (c'était le
|
||||
* cas de l'ancien `publishReel(originalUrl)`).
|
||||
*/
|
||||
|
||||
import type { Media } from "@shared/schema";
|
||||
import { storage } from "../../storage";
|
||||
import { minioService } from "../minio";
|
||||
import { createVideoThumbnail } from "../thumbnail";
|
||||
import { facebookService } from "../facebook";
|
||||
import { tiktokService } from "../tiktok";
|
||||
|
||||
export interface PagePublishResult {
|
||||
pageId: string;
|
||||
success: boolean;
|
||||
reelId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Range la vidéo rendue dans la médiathèque, avec sa vignette. */
|
||||
export async function storeRenderedVideo(
|
||||
userId: string,
|
||||
videoBuffer: Buffer,
|
||||
fileName: string,
|
||||
): Promise<Media> {
|
||||
const uploaded = await minioService.uploadMedia(videoBuffer, fileName, userId, "video/mp4");
|
||||
// Vignette extraite maintenant : la vidéo sera supprimée du disque après
|
||||
// publication, mais l'historique doit rester illustré.
|
||||
const thumbnailUrl = await createVideoThumbnail(videoBuffer);
|
||||
|
||||
return storage.createMedia({
|
||||
userId,
|
||||
type: "video",
|
||||
cloudinaryPublicId: uploaded.publicId,
|
||||
originalUrl: uploaded.originalUrl,
|
||||
facebookFeedUrl: uploaded.facebookFeedUrl,
|
||||
instagramFeedUrl: uploaded.instagramFeedUrl,
|
||||
instagramStoryUrl: uploaded.instagramStoryUrl,
|
||||
thumbnailUrl,
|
||||
fileName,
|
||||
fileSize: videoBuffer.length,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Publie (ou planifie) la vidéo sur chaque page, crée les entrées
|
||||
* `scheduled_posts` et met à jour le statut du post.
|
||||
*/
|
||||
export async function publishReelToPages(options: {
|
||||
postId: string;
|
||||
pageIds: string[];
|
||||
videoBuffer: Buffer;
|
||||
description: string;
|
||||
scheduledFor?: string;
|
||||
}): Promise<PagePublishResult[]> {
|
||||
const { postId, pageIds, videoBuffer, description, scheduledFor } = options;
|
||||
const results: PagePublishResult[] = [];
|
||||
|
||||
for (const pageId of pageIds) {
|
||||
try {
|
||||
const page = await storage.getSocialPage(pageId);
|
||||
if (!page) {
|
||||
results.push({ pageId, success: false, error: "Page non trouvée" });
|
||||
continue;
|
||||
}
|
||||
if (page.platform !== "facebook" && page.platform !== "tiktok") {
|
||||
results.push({
|
||||
pageId,
|
||||
success: false,
|
||||
error: `Plateforme non supportée pour les reels : ${page.platform}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const scheduledPost = await storage.createScheduledPost({
|
||||
postId,
|
||||
pageId: page.id,
|
||||
postType: "reel",
|
||||
scheduledAt: scheduledFor ? new Date(scheduledFor) : new Date(),
|
||||
});
|
||||
|
||||
if (scheduledFor) {
|
||||
// Le planificateur publiera à l'heure dite
|
||||
results.push({ pageId, success: true, reelId: "scheduled" });
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🚀 [Reels] Publication sur ${page.platform} ${page.pageName}...`);
|
||||
if (page.platform === "facebook") {
|
||||
const reelId = await facebookService.publishVideoFromBuffer(page, videoBuffer, description);
|
||||
await storage.updateScheduledPost(scheduledPost.id, {
|
||||
publishedAt: new Date(),
|
||||
externalPostId: reelId,
|
||||
});
|
||||
results.push({ pageId, success: true, reelId });
|
||||
} else {
|
||||
// TikTok finalise la publication de son côté : l'envoi est marqué comme
|
||||
// effectué pour ne pas republier, et le poller de statut renseignera
|
||||
// l'identifiant définitif du post.
|
||||
const publishId = await tiktokService.publishVideoFromBuffer(page, videoBuffer, description);
|
||||
await storage.updateScheduledPost(scheduledPost.id, {
|
||||
publishedAt: new Date(),
|
||||
publishId,
|
||||
publishStatus: "PROCESSING_UPLOAD",
|
||||
});
|
||||
results.push({ pageId, success: true, reelId: publishId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ [Reels] Publication sur la page ${pageId} impossible :`, error);
|
||||
results.push({
|
||||
pageId,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erreur inconnue",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const succeeded = results.filter((r) => r.success).length;
|
||||
const status = scheduledFor && succeeded > 0 ? "scheduled" : succeeded > 0 ? "published" : "failed";
|
||||
await storage.updatePost(postId, { status });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Message d'erreur lisible lorsque toutes les publications ont échoué. */
|
||||
export function describePublishFailure(results: PagePublishResult[]): string {
|
||||
const details = results.map((r) => r.error).filter(Boolean).join(" ; ");
|
||||
return `Publication impossible sur toutes les pages${details ? ` : ${details}` : ""}`;
|
||||
}
|
||||
Reference in new issue
Block a user