From 1cd59c1ee3ae0166d073b461c106a5cd2f280269 Mon Sep 17 00:00:00 2001 From: ShittyKopper Date: Thu, 4 Jan 2024 18:16:50 +0300 Subject: [PATCH] feat: initial builtin video thumbnail generator implementation when you disable "cache external media", video thumbnails off of remote instances do not get generated. misskey has a videoThumbnailGenerator config option to point to an external service to make that happen, but they do not provide any kind of implementation (or any documentation beyond a comment on the config file) this provides a video thumbnail generator that uses the same thumbnail generation code path used in local files, providing a quick and dirty solution to instances that want video thumbnails without the need to store external media permanently the eventual goal of this is to be the fallback implementation when that config option is unset. the current implementation is extremely bare bones and performs no caching or any other optimizations whatsoever --- .../backend/src/server/FileServerService.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index e82ef64dc4..714dddb162 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -81,6 +81,13 @@ export class FileServerService { .catch(err => this.errorHandler(request, reply, err)); }); + fastify.get<{ + Querystring: { url: string; }; + }>('/proxy/thumbnail.webp', async (request, reply) => { + return await this.videoThumbnailHandler(request, reply) + .catch(err => this.errorHandler(request, reply, err)); + }); + fastify.get<{ Params: { url: string; }; Querystring: { url?: string; }; @@ -371,6 +378,51 @@ export class FileServerService { } } + @bindThis + private async videoThumbnailHandler(request: FastifyRequest<{ Querystring: { url: string; }; }>, reply: FastifyReply) { + const file = await this.getStreamAndTypeFromUrl(request.query.url); + + if (file === '404') { + reply.code(404); + reply.header('Cache-Control', 'max-age=86400'); + return reply.sendFile('/dummy.png', assets); // TODO: return webp + } + + if (file === '204') { + reply.code(204); + reply.header('Cache-Control', 'max-age=86400'); + return; + } + + if (file.file?.thumbnailUrl) { + return await reply.redirect(301, file.file.thumbnailUrl); + } + + if (!file.mime.startsWith('video/')) { + if ('cleanup' in file) { + file.cleanup(); + } + + reply.code(400); + return; + } + + try { + const image = await this.videoProcessingService.generateVideoThumbnail(file.path); + + if ('cleanup' in file) { + file.cleanup(); + } + + reply.header('Content-Type', image.type); + reply.header('Cache-Control', 'max-age=31536000, immutable'); + return image.data; + } catch (e) { + if ('cleanup' in file) file.cleanup(); + throw e; + } + } + @bindThis private async getStreamAndTypeFromUrl(url: string): Promise< { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }