mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-12 01:36:54 +02:00
Deux corrections demandées ensemble. 1. Vignettes vidéo Le planificateur supprime la vidéo locale une fois publiée, alors que la ligne `media` survit pour l'historique. L'interface tentait donc de lire un fichier disparu pour en extraire une image : d'où les avertissements « Video thumbnail generation failed » et les tuiles vides. Une vignette JPEG est désormais extraite à la création du reel, dans les deux flux de génération, et stockée à part : elle reste disponible quand la vidéo ne l'est plus. La purge quotidienne la nettoie en même temps que la ligne qu'elle illustre. L'extraction est tolérante à l'échec — une vignette manquante dégrade l'affichage mais ne fait jamais échouer une publication — et le composant retombe sur son icône de remplacement pour les médias créés avant ce changement. La fonction d'extraction, jusque-là privée à la route Remotion, devient un service partagé. 2. Erreurs de compilation préexistantes (27 → 0) - remotion-video.tsx (bureau et mobile) : les composants Select étaient utilisés sans être importés, ce qui faisait planter le sélecteur de voix à l'exécution. - external.ts : fileSize et aiGenerated étaient transmis dans un type que le schéma ne déclare pas, et ne fonctionnaient que par conversion implicite de PostgreSQL. - reels.ts : l'interface MusicTrack, référencée mais jamais déclarée dans ce module. - remotion.ts : chromiumOptions.args retiré. Remotion ne lit jamais cette clé, les drapeaux Chrome qu'elle contenait n'atteignaient donc pas le navigateur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const UPLOADS_BASE = path.resolve(process.cwd(), 'uploads');
|
|
|
|
function ensureDir(dir: string): void {
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the public URL for a locally stored file.
|
|
* Returns a relative path so it works on any domain/protocol.
|
|
* objectKey is the relative path from uploads/ (e.g. "media/123-file.jpg").
|
|
*/
|
|
export function buildMinioUrl(_bucketName: string, objectKey: string, _publicUrlOverride?: string | null): string {
|
|
return `/uploads/${objectKey}`;
|
|
}
|
|
|
|
/**
|
|
* Resolves a possibly-relative URL to an absolute URL for use by external
|
|
* services (Facebook API, Instagram API) that must download the file.
|
|
* Uses APP_URL env var (public-facing domain).
|
|
*/
|
|
export function resolvePublicUrl(url: string): string {
|
|
if (!url || !url.startsWith('/')) return url;
|
|
const base = (process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, '');
|
|
return `${base}${url}`;
|
|
}
|
|
|
|
/**
|
|
* Resolves a possibly-relative URL to an absolute URL for use by internal
|
|
* Docker services (FFmpeg, etc.) that communicate via internal Docker network.
|
|
* Uses INTERNAL_APP_URL env var.
|
|
*/
|
|
export function resolveInternalUrl(url: string): string {
|
|
if (!url || !url.startsWith('/')) return url;
|
|
const base = (process.env.INTERNAL_APP_URL || process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, '');
|
|
return `${base}${url}`;
|
|
}
|
|
|
|
/**
|
|
* Charge un média en mémoire, que son URL soit locale (/uploads/...) ou distante.
|
|
* Nécessaire pour les APIs qui veulent recevoir le fichier plutôt qu'une URL
|
|
* (upload TikTok, Reels Facebook).
|
|
*/
|
|
export async function readMediaBuffer(url: string): Promise<Buffer> {
|
|
if (url.startsWith('/')) {
|
|
const localPath = path.join(process.cwd(), url);
|
|
if (fs.existsSync(localPath)) {
|
|
return await fs.promises.readFile(localPath);
|
|
}
|
|
}
|
|
|
|
const absoluteUrl = resolvePublicUrl(url);
|
|
const response = await fetch(absoluteUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Impossible de récupérer le média depuis ${absoluteUrl} (${response.status})`);
|
|
}
|
|
return Buffer.from(await response.arrayBuffer());
|
|
}
|
|
|
|
class LocalStorageService {
|
|
async uploadMedia(
|
|
file: Buffer | string,
|
|
fileName: string,
|
|
_userId: string,
|
|
_mimeType: string
|
|
): Promise<{
|
|
publicId: string;
|
|
originalUrl: string;
|
|
facebookFeedUrl: string | null;
|
|
instagramFeedUrl: string | null;
|
|
instagramStoryUrl: string | null;
|
|
}> {
|
|
const dir = path.join(UPLOADS_BASE, 'media');
|
|
ensureDir(dir);
|
|
|
|
const safeName = path.basename(fileName).replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
const objectKey = `media/${Date.now()}-${safeName}`;
|
|
const filePath = path.join(UPLOADS_BASE, objectKey);
|
|
|
|
const body: Buffer = typeof file === 'string' ? fs.readFileSync(file) : file;
|
|
fs.writeFileSync(filePath, body);
|
|
|
|
const url = buildMinioUrl('', objectKey);
|
|
return { publicId: objectKey, originalUrl: url, facebookFeedUrl: url, instagramFeedUrl: url, instagramStoryUrl: url };
|
|
}
|
|
|
|
async deleteMedia(publicId: string, _userId: string, _mediaType: 'image' | 'video'): Promise<void> {
|
|
const filePath = path.join(UPLOADS_BASE, publicId);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
|
|
async uploadStoryImageWithText(buffer: Buffer, originalFileName: string): Promise<string> {
|
|
const dir = path.join(UPLOADS_BASE, 'stories');
|
|
ensureDir(dir);
|
|
|
|
const safeName = path.basename(originalFileName).replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
const objectKey = `stories/story-${Date.now()}-${safeName}`;
|
|
|
|
fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer);
|
|
return buildMinioUrl('', objectKey);
|
|
}
|
|
|
|
/**
|
|
* Range une vignette vidéo. Elle survit volontairement à la suppression de la
|
|
* vidéo source, pour que l'historique reste illustré.
|
|
*/
|
|
async uploadThumbnail(buffer: Buffer, fileName: string): Promise<{ publicId: string; url: string }> {
|
|
const dir = path.join(UPLOADS_BASE, 'thumbnails');
|
|
ensureDir(dir);
|
|
|
|
const safeName = path.basename(fileName).replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
const objectKey = `thumbnails/${safeName}`;
|
|
|
|
fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer);
|
|
return { publicId: objectKey, url: buildMinioUrl('', objectKey) };
|
|
}
|
|
|
|
async uploadLogo(buffer: Buffer, fileName: string): Promise<{ publicId: string; url: string }> {
|
|
const dir = path.join(UPLOADS_BASE, 'logos');
|
|
ensureDir(dir);
|
|
|
|
const ext = path.extname(fileName) || '.png';
|
|
const objectKey = `logos/logo-${Date.now()}${ext}`;
|
|
|
|
fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer);
|
|
return { publicId: objectKey, url: buildMinioUrl('', objectKey) };
|
|
}
|
|
|
|
async deleteLogo(publicId: string): Promise<void> {
|
|
const filePath = path.join(UPLOADS_BASE, publicId);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const minioService = new LocalStorageService();
|