fix: thumbnail generation (better now)

This commit is contained in:
diced
2024-06-02 16:03:57 -07:00
parent 6de3260a2b
commit 75a1d17ff7
4 changed files with 64 additions and 102 deletions

View File

@@ -51,7 +51,7 @@
"fast-glob": "^3.3.2",
"fastify": "^4.26.2",
"fastify-plugin": "^4.5.1",
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.3",
"highlight.js": "^11.9.0",
"isomorphic-dompurify": "^1.11.0",
"katex": "^0.16.9",
@@ -75,6 +75,7 @@
"devDependencies": {
"@types/bytes": "^3.1.4",
"@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/katex": "^0.16.7",
"@types/ms": "^0.7.34",
"@types/multer": "^1.4.11",

95
pnpm-lock.yaml generated
View File

@@ -101,9 +101,9 @@ importers:
fastify-plugin:
specifier: ^4.5.1
version: 4.5.1
ffmpeg-static:
specifier: ^5.2.0
version: 5.2.0
fluent-ffmpeg:
specifier: ^2.1.3
version: 2.1.3
highlight.js:
specifier: ^11.9.0
version: 11.9.0
@@ -168,6 +168,9 @@ importers:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
'@types/fluent-ffmpeg':
specifier: ^2.1.24
version: 2.1.24
'@types/katex':
specifier: ^0.16.7
version: 0.16.7
@@ -499,10 +502,6 @@ packages:
resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==}
engines: {node: '>=6.9.0'}
'@derhuerst/http-basic@8.2.4':
resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==}
engines: {node: '>=6.0.0'}
'@esbuild/aix-ppc64@0.19.12':
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
engines: {node: '>=12'}
@@ -1158,6 +1157,9 @@ packages:
'@types/express@4.17.21':
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
'@types/fluent-ffmpeg@2.1.24':
resolution: {integrity: sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==}
'@types/geojson@7946.0.13':
resolution: {integrity: sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==}
@@ -1191,9 +1193,6 @@ packages:
'@types/multer@1.4.11':
resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==}
'@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
'@types/node@17.0.45':
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
@@ -1490,6 +1489,9 @@ packages:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
async@0.2.10:
resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
@@ -1633,9 +1635,6 @@ packages:
caniuse-lite@1.0.30001565:
resolution: {integrity: sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==}
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -1773,10 +1772,6 @@ packages:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
@@ -2339,10 +2334,6 @@ packages:
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
ffmpeg-static@5.2.0:
resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==}
engines: {node: '>=16'}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -2382,6 +2373,10 @@ packages:
flatted@3.2.9:
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
fluent-ffmpeg@2.1.3:
resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
engines: {node: '>=18'}
fmin@0.0.2:
resolution: {integrity: sha512-sSi6DzInhl9d8yqssDfGZejChO8d2bAGIpysPsvYsxFe898z89XhCZg6CPNV3nhUhFefeC/AXZK2bAJxlBjN6A==}
@@ -2603,9 +2598,6 @@ packages:
resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==}
engines: {node: '>= 14'}
http-response-object@3.0.2:
resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@@ -3637,9 +3629,6 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-cache-control@1.0.1:
resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
@@ -4859,6 +4848,10 @@ packages:
resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==}
engines: {node: '>= 0.4'}
which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -5418,13 +5411,6 @@ snapshots:
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
'@derhuerst/http-basic@8.2.4':
dependencies:
caseless: 0.12.0
concat-stream: 2.0.0
http-response-object: 3.0.2
parse-cache-control: 1.0.1
'@esbuild/aix-ppc64@0.19.12':
optional: true
@@ -6094,6 +6080,10 @@ snapshots:
'@types/qs': 6.9.10
'@types/serve-static': 1.15.5
'@types/fluent-ffmpeg@2.1.24':
dependencies:
'@types/node': 20.10.1
'@types/geojson@7946.0.13': {}
'@types/hast@2.3.8':
@@ -6122,8 +6112,6 @@ snapshots:
dependencies:
'@types/express': 4.17.21
'@types/node@10.17.60': {}
'@types/node@17.0.45': {}
'@types/node@20.10.1':
@@ -6482,6 +6470,8 @@ snapshots:
astral-regex@2.0.0: {}
async@0.2.10: {}
async@3.2.5: {}
asynciterator.prototype@1.0.0:
@@ -6627,8 +6617,6 @@ snapshots:
caniuse-lite@1.0.30001565: {}
caseless@0.12.0: {}
ccount@2.0.1: {}
center-align@0.1.3:
@@ -6775,13 +6763,6 @@ snapshots:
readable-stream: 2.3.8
typedarray: 0.0.6
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 3.6.2
typedarray: 0.0.6
console-control-strings@1.1.0: {}
content-disposition@0.5.4:
@@ -7521,15 +7502,6 @@ snapshots:
fecha@4.2.3: {}
ffmpeg-static@5.2.0:
dependencies:
'@derhuerst/http-basic': 8.2.4
env-paths: 2.2.1
https-proxy-agent: 5.0.1
progress: 2.0.3
transitivePeerDependencies:
- supports-color
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -7584,6 +7556,11 @@ snapshots:
flatted@3.2.9: {}
fluent-ffmpeg@2.1.3:
dependencies:
async: 0.2.10
which: 1.3.1
fmin@0.0.2:
dependencies:
contour_plot: 0.0.1
@@ -7828,10 +7805,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
http-response-object@3.0.2:
dependencies:
'@types/node': 10.17.60
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
@@ -9021,8 +8994,6 @@ snapshots:
dependencies:
callsites: 3.1.0
parse-cache-control@1.0.1: {}
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.23.5
@@ -10360,6 +10331,10 @@ snapshots:
gopd: 1.0.1
has-tostringtag: 1.0.0
which@1.3.1:
dependencies:
isexe: 2.0.0
which@2.0.2:
dependencies:
isexe: 2.0.0

View File

@@ -139,8 +139,6 @@ export async function uploadPartialFiles(
() => {
const res: Response['/api/upload'] = JSON.parse(req.responseText);
console.log(res);
if ((res as ErrorBody).message) {
notifications.update({
id: 'upload-partial',

View File

@@ -3,9 +3,8 @@ import { config } from '@/lib/config';
import { datasource } from '@/lib/datasource';
import { prisma } from '@/lib/db';
import { log } from '@/lib/logger';
import { spawn } from 'child_process';
import ffmpegPath from 'ffmpeg-static';
import { createWriteStream } from 'fs';
import ffmpeg from 'fluent-ffmpeg';
import { createWriteStream, readFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { isMainThread, parentPort, workerData } from 'worker_threads';
@@ -30,43 +29,31 @@ if (!enabled) {
logger.debug('started thumbnail worker');
async function ffmpeg(file: string): Promise<Buffer | undefined> {
const args = ['-i', file, '-frames:v', '1', '-f', 'mjpeg', 'pipe:1'];
function genThumbnail(file: string, thumbnailTmp: string): Promise<Buffer | undefined> {
return new Promise((resolve, reject) => {
ffmpeg(file)
.videoFilters('thumbnail')
.frames(1)
.format('mjpeg')
.output(thumbnailTmp)
.on('start', (cmd) => {
logger.debug('generating thumbnail', { cmd });
})
.on('error', (err) => {
logger.error('failed to generate thumbnail', { err: err.message });
reject(err);
})
.on('end', () => {
const buffer = readFileSync(thumbnailTmp);
const proc = spawn(ffmpegPath!, args, {
stdio: ['ignore', 'pipe', 'ignore'],
unlinkSync(thumbnailTmp);
unlinkSync(file);
logger.debug('removed temporary files', { file, thumbnail: thumbnailTmp });
resolve(buffer);
})
.run();
});
try {
const buffer = await new Promise<Buffer>((resolve, reject) => {
const data: Buffer[] = [];
proc.stdout!.on('data', (d) => {
data.push(d);
});
proc.once('error', reject);
proc.once('close', (code) => {
if (code !== 0) {
const stringed = Buffer.concat([...data]).toString();
logger.error('ffmpeg exited with non-zero code');
reject(stringed);
} else {
resolve(Buffer.concat([...data]));
}
});
});
return buffer;
} catch (e) {
logger.error('failed to generate thumbnail', {
file,
error: e,
});
}
}
async function generate(ids: string[]) {
@@ -103,7 +90,8 @@ async function generate(ids: string[]) {
writeStream.on('finish', resolve);
});
const thumbnail = await ffmpeg(tmpFile);
const thumbnailTmpFile = join(config.core.tempDirectory, `zthumbnail_${file.id}.jpg`);
const thumbnail = await genThumbnail(tmpFile, thumbnailTmpFile);
if (!thumbnail) return;
await datasource.put(`.thumbnail.${file.id}.jpg`, thumbnail);