Compare commits
8 Commits
f4cd20a010
...
v1.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
878c35cbcd
|
||
|
|
8cf0036248
|
||
|
|
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,
|
"when": 1768423315172,
|
||||||
"tag": "0006_friendly_adam_warlock",
|
"tag": "0006_friendly_adam_warlock",
|
||||||
"breakpoints": true
|
"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"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
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 -L -s https://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
|
FROM base AS build
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/backend",
|
"name": "@memegoat/backend",
|
||||||
"version": "1.4.1",
|
"version": "1.5.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const envSchema = z.object({
|
|||||||
// Media Limits
|
// Media Limits
|
||||||
MAX_IMAGE_SIZE_KB: z.coerce.number().default(512),
|
MAX_IMAGE_SIZE_KB: z.coerce.number().default(512),
|
||||||
MAX_GIF_SIZE_KB: z.coerce.number().default(1024),
|
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>;
|
export type Env = z.infer<typeof envSchema>;
|
||||||
|
|||||||
@@ -55,22 +55,31 @@ export class ContentsService {
|
|||||||
"image/webp",
|
"image/webp",
|
||||||
"image/gif",
|
"image/gif",
|
||||||
"video/webm",
|
"video/webm",
|
||||||
|
"video/mp4",
|
||||||
|
"video/quicktime",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!allowedMimeTypes.includes(file.mimetype)) {
|
if (!allowedMimeTypes.includes(file.mimetype)) {
|
||||||
throw new BadRequestException(
|
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 isGif = file.mimetype === "image/gif";
|
||||||
const maxSizeKb = isGif
|
const isVideo = file.mimetype.startsWith("video/");
|
||||||
? this.configService.get<number>("MAX_GIF_SIZE_KB", 1024)
|
let maxSizeKb: number;
|
||||||
: this.configService.get<number>("MAX_IMAGE_SIZE_KB", 512);
|
|
||||||
|
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) {
|
if (file.size > maxSizeKb * 1024) {
|
||||||
throw new BadRequestException(
|
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
|
// 2. Transcodage
|
||||||
let processed: MediaProcessingResult;
|
let processed: MediaProcessingResult;
|
||||||
if (file.mimetype.startsWith("image/")) {
|
if (file.mimetype.startsWith("image/") && file.mimetype !== "image/gif") {
|
||||||
// Image ou GIF -> WebP (format moderne, bien supporté)
|
// Image -> WebP (format moderne, bien supporté)
|
||||||
processed = await this.mediaService.processImage(file.buffer, "webp");
|
processed = await this.mediaService.processImage(file.buffer, "webp");
|
||||||
} else if (file.mimetype.startsWith("video/")) {
|
} else if (
|
||||||
// Vidéo -> WebM
|
file.mimetype.startsWith("video/") ||
|
||||||
|
file.mimetype === "image/gif"
|
||||||
|
) {
|
||||||
|
// Vidéo ou GIF -> WebM
|
||||||
processed = await this.mediaService.processVideo(file.buffer, "webm");
|
processed = await this.mediaService.processVideo(file.buffer, "webm");
|
||||||
} else {
|
} else {
|
||||||
throw new BadRequestException("Format de fichier non supporté");
|
throw new BadRequestException("Format de fichier non supporté");
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
export enum ContentType {
|
export enum ContentType {
|
||||||
MEME = "meme",
|
MEME = "meme",
|
||||||
GIF = "gif",
|
GIF = "gif",
|
||||||
|
VIDEO = "video",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateContentDto {
|
export class CreateContentDto {
|
||||||
@IsEnum(ContentType)
|
@IsEnum(ContentType)
|
||||||
type!: "meme" | "gif";
|
type!: "meme" | "gif" | "video";
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ContentType } from "./create-content.dto";
|
|||||||
|
|
||||||
export class UploadContentDto {
|
export class UploadContentDto {
|
||||||
@IsEnum(ContentType)
|
@IsEnum(ContentType)
|
||||||
type!: "meme" | "gif";
|
type!: "meme" | "gif" | "video";
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { categories } from "./categories";
|
|||||||
import { tags } from "./tags";
|
import { tags } from "./tags";
|
||||||
import { users } from "./users";
|
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(
|
export const contents = pgTable(
|
||||||
"contents",
|
"contents",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class VideoProcessorStrategy implements IMediaProcessorStrategy {
|
|||||||
private readonly logger = new Logger(VideoProcessorStrategy.name);
|
private readonly logger = new Logger(VideoProcessorStrategy.name);
|
||||||
|
|
||||||
canHandle(mimeType: string): boolean {
|
canHandle(mimeType: string): boolean {
|
||||||
return mimeType.startsWith("video/");
|
return mimeType.startsWith("video/") || mimeType === "image/gif";
|
||||||
}
|
}
|
||||||
|
|
||||||
async process(
|
async process(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/frontend",
|
"name": "@memegoat/frontend",
|
||||||
"version": "1.4.1",
|
"version": "1.5.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import type { Category } from "@/types/content";
|
|||||||
|
|
||||||
const uploadSchema = z.object({
|
const uploadSchema = z.object({
|
||||||
title: z.string().min(3, "Le titre doit faire au moins 3 caractères"),
|
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(),
|
categoryId: z.string().optional(),
|
||||||
tags: z.string().optional(),
|
tags: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -112,6 +112,16 @@ export default function UploadPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFile(selectedFile);
|
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();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
setPreview(reader.result as string);
|
setPreview(reader.result as string);
|
||||||
@@ -182,7 +192,7 @@ export default function UploadPage() {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormLabel>Fichier (Image ou GIF)</FormLabel>
|
<FormLabel>Fichier (Image, GIF ou Vidéo)</FormLabel>
|
||||||
{!preview ? (
|
{!preview ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -194,25 +204,31 @@ export default function UploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="font-medium">Cliquez pour choisir un fichier</p>
|
<p className="font-medium">Cliquez pour choisir un fichier</p>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
PNG, JPG ou GIF jusqu'à 10Mo
|
PNG, JPG, GIF, MP4 ou MOV jusqu'à 10Mo
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
type="file"
|
type="file"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept="image/*,.gif"
|
accept="image/*,video/mp4,video/webm,video/quicktime,.gif"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
|
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
|
||||||
<div className="relative h-[400px] w-full">
|
<div className="relative h-[400px] w-full flex items-center justify-center">
|
||||||
<NextImage
|
{file?.type.startsWith("video/") ? (
|
||||||
src={preview}
|
<video src={preview} controls className="max-h-full max-w-full">
|
||||||
alt="Preview"
|
<track kind="captions" />
|
||||||
fill
|
</video>
|
||||||
className="object-contain"
|
) : (
|
||||||
/>
|
<NextImage
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -260,6 +276,7 @@ export default function UploadPage() {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="meme">Image fixe</SelectItem>
|
<SelectItem value="meme">Image fixe</SelectItem>
|
||||||
<SelectItem value="gif">GIF Animé</SelectItem>
|
<SelectItem value="gif">GIF Animé</SelectItem>
|
||||||
|
<SelectItem value="video">Vidéo</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface Content {
|
|||||||
description?: string;
|
description?: string;
|
||||||
url: string;
|
url: string;
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
type: "meme" | "gif";
|
type: "meme" | "gif" | "video";
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
size: number;
|
size: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/source",
|
"name": "@memegoat/source",
|
||||||
"version": "1.4.1",
|
"version": "1.5.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"version:get": "cmake -P version.cmake GET",
|
"version:get": "cmake -P version.cmake GET",
|
||||||
|
|||||||
Reference in New Issue
Block a user