Compare commits
6 Commits
f4cd20a010
...
v1.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c389024f59
|
||
|
|
bbdbe58af5
|
||
|
|
5951e41eb5
|
||
|
|
7442236e8d
|
||
|
|
3ef7292287
|
||
|
|
f1a571196d
|
1
backend/.migrations/0007_melodic_synch.sql
Normal file
1
backend/.migrations/0007_melodic_synch.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."content_type" ADD VALUE 'video';
|
||||
1653
backend/.migrations/meta/0007_snapshot.json
Normal file
1653
backend/.migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,13 @@
|
||||
"when": 1768423315172,
|
||||
"tag": "0006_friendly_adam_warlock",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1769605995410,
|
||||
"tag": "0007_melodic_synch",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,23 @@ ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
ENV FFMPEG_VERSION=3.0.2
|
||||
|
||||
WORKDIR /tmp/ffmpeg
|
||||
|
||||
RUN apk add --update build-base curl nasm tar bzip2 \
|
||||
zlib-dev openssl-dev yasm-dev lame-dev libogg-dev x264-dev libvpx-dev libvorbis-dev x265-dev freetype-dev libass-dev libwebp-dev rtmpdump-dev libtheora-dev opus-dev && \
|
||||
DIR=$(mktemp -d) && cd ${DIR} && \
|
||||
curl -s http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz | tar zxvf - -C . && \
|
||||
cd ffmpeg-${FFMPEG_VERSION} && \
|
||||
./configure \
|
||||
--enable-version3 --enable-gpl --enable-nonfree --enable-small --enable-libmp3lame --enable-libx264 --enable-libx265 --enable-libvpx --enable-libtheora --enable-libvorbis --enable-libopus --enable-libass --enable-libwebp --enable-librtmp --enable-postproc --enable-avresample --enable-libfreetype --enable-openssl --disable-debug && \
|
||||
make && \
|
||||
make install && \
|
||||
make distclean && \
|
||||
rm -rf ${DIR} && \
|
||||
apk del build-base curl tar bzip2 x264 openssl nasm && rm -rf /var/cache/apk/*
|
||||
|
||||
FROM base AS build
|
||||
WORKDIR /usr/src/app
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@memegoat/backend",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -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<typeof envSchema>;
|
||||
|
||||
@@ -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<number>("MAX_GIF_SIZE_KB", 1024)
|
||||
: this.configService.get<number>("MAX_IMAGE_SIZE_KB", 512);
|
||||
const isVideo = file.mimetype.startsWith("video/");
|
||||
let maxSizeKb: number;
|
||||
|
||||
if (isGif) {
|
||||
maxSizeKb = this.configService.get<number>("MAX_GIF_SIZE_KB", 1024);
|
||||
} else if (isVideo) {
|
||||
maxSizeKb = this.configService.get<number>("MAX_VIDEO_SIZE_KB", 10240);
|
||||
} else {
|
||||
maxSizeKb = this.configService.get<number>("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é");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ContentType } from "./create-content.dto";
|
||||
|
||||
export class UploadContentDto {
|
||||
@IsEnum(ContentType)
|
||||
type!: "meme" | "gif";
|
||||
type!: "meme" | "gif" | "video";
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@memegoat/frontend",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -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() {
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<FormLabel>Fichier (Image ou GIF)</FormLabel>
|
||||
<FormLabel>Fichier (Image, GIF ou Vidéo)</FormLabel>
|
||||
{!preview ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -194,25 +204,31 @@ export default function UploadPage() {
|
||||
</div>
|
||||
<p className="font-medium">Cliquez pour choisir un fichier</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
PNG, JPG ou GIF jusqu'à 10Mo
|
||||
PNG, JPG, GIF, MP4 ou MOV jusqu'à 10Mo
|
||||
</p>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*,.gif"
|
||||
accept="image/*,video/mp4,video/webm,video/quicktime,.gif"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
|
||||
<div className="relative h-[400px] w-full">
|
||||
<NextImage
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<div className="relative h-[400px] w-full flex items-center justify-center">
|
||||
{file?.type.startsWith("video/") ? (
|
||||
<video src={preview} controls className="max-h-full max-w-full">
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
) : (
|
||||
<NextImage
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -260,6 +276,7 @@ export default function UploadPage() {
|
||||
<SelectContent>
|
||||
<SelectItem value="meme">Image fixe</SelectItem>
|
||||
<SelectItem value="gif">GIF Animé</SelectItem>
|
||||
<SelectItem value="video">Vidéo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface Content {
|
||||
description?: string;
|
||||
url: string;
|
||||
thumbnailUrl?: string;
|
||||
type: "meme" | "gif";
|
||||
type: "meme" | "gif" | "video";
|
||||
mimeType: string;
|
||||
size: number;
|
||||
width?: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@memegoat/source",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.1",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"version:get": "cmake -P version.cmake GET",
|
||||
|
||||
Reference in New Issue
Block a user