feat: add modular services and repositories for improved code organization

Introduce repository pattern across multiple services, including `favorites`, `tags`, `sessions`, `reports`, `auth`, and more. Decouple crypto functionalities into modular services like `HashingService`, `JwtService`, and `EncryptionService`. Improve testability and maintainability by simplifying dependencies and consolidating utility logic.
This commit is contained in:
Mathis HERRIOT
2026-01-14 12:11:39 +01:00
parent 9c45bf11e4
commit 514bd354bf
64 changed files with 1801 additions and 1295 deletions

View File

@@ -1,22 +1,18 @@
import { readFile, unlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Readable } from "node:stream";
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import * as NodeClam from "clamscan";
import ffmpeg from "fluent-ffmpeg";
import sharp from "sharp";
import { v4 as uuidv4 } from "uuid";
import type {
IMediaService,
MediaProcessingResult,
ScanResult,
} from "./interfaces/media.interface";
} from "../common/interfaces/media.interface";
import { ImageProcessorStrategy } from "./strategies/image-processor.strategy";
import { VideoProcessorStrategy } from "./strategies/video-processor.strategy";
interface ClamScanner {
scanStream(
@@ -25,12 +21,16 @@ interface ClamScanner {
}
@Injectable()
export class MediaService {
export class MediaService implements IMediaService {
private readonly logger = new Logger(MediaService.name);
private clamscan: ClamScanner | null = null;
private isClamAvInitialized = false;
constructor(private readonly configService: ConfigService) {
constructor(
private readonly configService: ConfigService,
private readonly imageProcessor: ImageProcessorStrategy,
private readonly videoProcessor: VideoProcessorStrategy,
) {
this.initClamScan();
}
@@ -84,82 +84,13 @@ export class MediaService {
buffer: Buffer,
format: "webp" | "avif" = "webp",
): Promise<MediaProcessingResult> {
try {
let pipeline = sharp(buffer);
const metadata = await pipeline.metadata();
if (format === "webp") {
pipeline = pipeline.webp({ quality: 80, effort: 6 });
} else {
pipeline = pipeline.avif({ quality: 65, effort: 6 });
}
const processedBuffer = await pipeline.toBuffer();
return {
buffer: processedBuffer,
mimeType: `image/${format}`,
extension: format,
width: metadata.width,
height: metadata.height,
size: processedBuffer.length,
};
} catch (error) {
this.logger.error(`Error processing image: ${error.message}`);
throw new BadRequestException("Failed to process image");
}
return this.imageProcessor.process(buffer, { format });
}
async processVideo(
buffer: Buffer,
format: "webm" | "av1" = "webm",
): Promise<MediaProcessingResult> {
const tempInput = join(tmpdir(), `${uuidv4()}.tmp`);
const tempOutput = join(
tmpdir(),
`${uuidv4()}.${format === "av1" ? "mp4" : "webm"}`,
);
try {
await writeFile(tempInput, buffer);
await new Promise<void>((resolve, reject) => {
let command = ffmpeg(tempInput);
if (format === "webm") {
command = command
.toFormat("webm")
.videoCodec("libvpx-vp9")
.audioCodec("libopus")
.outputOptions("-crf 30", "-b:v 0");
} else {
command = command
.toFormat("mp4")
.videoCodec("libaom-av1")
.audioCodec("libopus")
.outputOptions("-crf 34", "-b:v 0", "-strict experimental");
}
command
.on("end", () => resolve())
.on("error", (err) => reject(err))
.save(tempOutput);
});
const processedBuffer = await readFile(tempOutput);
return {
buffer: processedBuffer,
mimeType: format === "av1" ? "video/mp4" : "video/webm",
extension: format === "av1" ? "mp4" : "webm",
size: processedBuffer.length,
};
} catch (error) {
this.logger.error(`Error processing video: ${error.message}`);
throw new BadRequestException("Failed to process video");
} finally {
await unlink(tempInput).catch(() => {});
await unlink(tempOutput).catch(() => {});
}
return this.videoProcessor.process(buffer, { format });
}
}