diff --git a/package.json b/package.json index b4b837e30..704f6fbf7 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 889a0ce3d..3cb897594 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/components/pages/upload/uploadPartialFiles.tsx b/src/components/pages/upload/uploadPartialFiles.tsx index 78184f669..d5989e774 100755 --- a/src/components/pages/upload/uploadPartialFiles.tsx +++ b/src/components/pages/upload/uploadPartialFiles.tsx @@ -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', diff --git a/src/offload/thumbnails.ts b/src/offload/thumbnails.ts index bc16d6a84..2ae4024b9 100755 --- a/src/offload/thumbnails.ts +++ b/src/offload/thumbnails.ts @@ -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 { - const args = ['-i', file, '-frames:v', '1', '-f', 'mjpeg', 'pipe:1']; - - const proc = spawn(ffmpegPath!, args, { - stdio: ['ignore', 'pipe', 'ignore'], +function genThumbnail(file: string, thumbnailTmp: string): Promise { + 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); + + unlinkSync(thumbnailTmp); + unlinkSync(file); + logger.debug('removed temporary files', { file, thumbnail: thumbnailTmp }); + + resolve(buffer); + }) + .run(); }); - - try { - const buffer = await new Promise((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);