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
This commit is contained in:
ShittyKopper 2024-01-04 18:16:50 +03:00
parent c8a7e27e70
commit 1cd59c1ee3
1 changed files with 52 additions and 0 deletions

View File

@ -81,6 +81,13 @@ export class FileServerService {
.catch(err => this.errorHandler(request, reply, err)); .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<{ fastify.get<{
Params: { url: string; }; Params: { url: string; };
Querystring: { 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 @bindThis
private async getStreamAndTypeFromUrl(url: string): Promise< 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; } { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }