diff --git a/backend/src/config/env.schema.ts b/backend/src/config/env.schema.ts index 628d1cf..358a08b 100644 --- a/backend/src/config/env.schema.ts +++ b/backend/src/config/env.schema.ts @@ -48,6 +48,7 @@ export const envSchema = z.object({ // Media Limits MAX_IMAGE_SIZE_KB: z.coerce.number().default(512), MAX_GIF_SIZE_KB: z.coerce.number().default(1024), + MAX_VIDEO_SIZE_KB: z.coerce.number().default(10240), }); export type Env = z.infer; diff --git a/backend/src/contents/contents.service.ts b/backend/src/contents/contents.service.ts index 04fe82d..c0e2dd6 100644 --- a/backend/src/contents/contents.service.ts +++ b/backend/src/contents/contents.service.ts @@ -55,22 +55,31 @@ export class ContentsService { "image/webp", "image/gif", "video/webm", + "video/mp4", + "video/quicktime", ]; if (!allowedMimeTypes.includes(file.mimetype)) { throw new BadRequestException( - "Format de fichier non supporté. Formats acceptés: png, jpeg, jpg, webp, webm, gif.", + "Format de fichier non supporté. Formats acceptés: png, jpeg, jpg, webp, webm, mp4, mov, gif.", ); } const isGif = file.mimetype === "image/gif"; - const maxSizeKb = isGif - ? this.configService.get("MAX_GIF_SIZE_KB", 1024) - : this.configService.get("MAX_IMAGE_SIZE_KB", 512); + const isVideo = file.mimetype.startsWith("video/"); + let maxSizeKb: number; + + if (isGif) { + maxSizeKb = this.configService.get("MAX_GIF_SIZE_KB", 1024); + } else if (isVideo) { + maxSizeKb = this.configService.get("MAX_VIDEO_SIZE_KB", 10240); + } else { + maxSizeKb = this.configService.get("MAX_IMAGE_SIZE_KB", 512); + } if (file.size > maxSizeKb * 1024) { throw new BadRequestException( - `Fichier trop volumineux. Limite pour ${isGif ? "GIF" : "image"}: ${maxSizeKb} Ko.`, + `Fichier trop volumineux. Limite pour ${isGif ? "GIF" : isVideo ? "vidéo" : "image"}: ${maxSizeKb} Ko.`, ); } @@ -87,11 +96,14 @@ export class ContentsService { // 2. Transcodage let processed: MediaProcessingResult; - if (file.mimetype.startsWith("image/")) { - // Image ou GIF -> WebP (format moderne, bien supporté) + if (file.mimetype.startsWith("image/") && file.mimetype !== "image/gif") { + // Image -> WebP (format moderne, bien supporté) processed = await this.mediaService.processImage(file.buffer, "webp"); - } else if (file.mimetype.startsWith("video/")) { - // Vidéo -> WebM + } else if ( + file.mimetype.startsWith("video/") || + file.mimetype === "image/gif" + ) { + // Vidéo ou GIF -> WebM processed = await this.mediaService.processVideo(file.buffer, "webm"); } else { throw new BadRequestException("Format de fichier non supporté"); diff --git a/backend/src/contents/dto/create-content.dto.ts b/backend/src/contents/dto/create-content.dto.ts index 7c0aa92..d7ad545 100644 --- a/backend/src/contents/dto/create-content.dto.ts +++ b/backend/src/contents/dto/create-content.dto.ts @@ -12,11 +12,12 @@ import { export enum ContentType { MEME = "meme", GIF = "gif", + VIDEO = "video", } export class CreateContentDto { @IsEnum(ContentType) - type!: "meme" | "gif"; + type!: "meme" | "gif" | "video"; @IsString() @IsNotEmpty() diff --git a/backend/src/contents/dto/upload-content.dto.ts b/backend/src/contents/dto/upload-content.dto.ts index 5cc6902..cf108bf 100644 --- a/backend/src/contents/dto/upload-content.dto.ts +++ b/backend/src/contents/dto/upload-content.dto.ts @@ -11,7 +11,7 @@ import { ContentType } from "./create-content.dto"; export class UploadContentDto { @IsEnum(ContentType) - type!: "meme" | "gif"; + type!: "meme" | "gif" | "video"; @IsString() @IsNotEmpty() diff --git a/backend/src/database/schemas/content.ts b/backend/src/database/schemas/content.ts index 0c1c43c..268ef6e 100644 --- a/backend/src/database/schemas/content.ts +++ b/backend/src/database/schemas/content.ts @@ -12,7 +12,7 @@ import { categories } from "./categories"; import { tags } from "./tags"; import { users } from "./users"; -export const contentType = pgEnum("content_type", ["meme", "gif"]); +export const contentType = pgEnum("content_type", ["meme", "gif", "video"]); export const contents = pgTable( "contents", diff --git a/backend/src/media/strategies/video-processor.strategy.ts b/backend/src/media/strategies/video-processor.strategy.ts index 2deffb5..e2c8aa1 100644 --- a/backend/src/media/strategies/video-processor.strategy.ts +++ b/backend/src/media/strategies/video-processor.strategy.ts @@ -12,7 +12,7 @@ export class VideoProcessorStrategy implements IMediaProcessorStrategy { private readonly logger = new Logger(VideoProcessorStrategy.name); canHandle(mimeType: string): boolean { - return mimeType.startsWith("video/"); + return mimeType.startsWith("video/") || mimeType === "image/gif"; } async process( diff --git a/frontend/src/app/(dashboard)/upload/page.tsx b/frontend/src/app/(dashboard)/upload/page.tsx index c817953..8d09d71 100644 --- a/frontend/src/app/(dashboard)/upload/page.tsx +++ b/frontend/src/app/(dashboard)/upload/page.tsx @@ -42,7 +42,7 @@ import type { Category } from "@/types/content"; const uploadSchema = z.object({ title: z.string().min(3, "Le titre doit faire au moins 3 caractères"), - type: z.enum(["meme", "gif"]), + type: z.enum(["meme", "gif", "video"]), categoryId: z.string().optional(), tags: z.string().optional(), }); @@ -112,6 +112,16 @@ export default function UploadPage() { return; } setFile(selectedFile); + + // Auto-détection du type + if (selectedFile.type === "image/gif") { + form.setValue("type", "gif"); + } else if (selectedFile.type.startsWith("video/")) { + form.setValue("type", "video"); + } else { + form.setValue("type", "meme"); + } + const reader = new FileReader(); reader.onloadend = () => { setPreview(reader.result as string); @@ -182,7 +192,7 @@ export default function UploadPage() {
- Fichier (Image ou GIF) + Fichier (Image, GIF ou Vidéo) {!preview ? ( ) : (
-
- +
+ {file?.type.startsWith("video/") ? ( +