From 5457244d7403e97dd51fbb8d140685eedcbcedf6 Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Tue, 14 Apr 2026 22:23:29 +0530 Subject: [PATCH] Add in-memory thumbnail caching --- index.js | 78 +++++++++++++++++------- package-lock.json | 151 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 3 files changed, 209 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index 3261891..0a1a276 100644 --- a/index.js +++ b/index.js @@ -1,44 +1,78 @@ -import express from "express" -import ffmpeg from "fluent-ffmpeg" -import {PassThrough} from "stream" +import express from "express"; +import ffmpeg from "fluent-ffmpeg"; +import { PassThrough } from "stream"; +import NodeCache from "node-cache"; +import ffmpegInstaller from "@ffmpeg-installer/ffmpeg"; -const app = express() -const PORT = process.env.PORT || 3100 +ffmpeg.setFfmpegPath(ffmpegInstaller.path); -app.get("/thumbnail", async (req, res) => { - const videoUrl = req.query.url +const app = express(); +const PORT = process.env.PORT || 3100; + +const thumbnailCache = new NodeCache({ + stdTTL: 3600, + checkperiod: 600, + useClones: false +}); + +app.get("/thumbnail", (req, res) => { + const videoUrl = req.query.url?.trim(); if (!videoUrl) { - return res.status(400).json({error: "Missing 'url' query parameter"}) + return res.status(400).json({ error: "Missing 'url' query parameter" }); + } + + const cachedImage = thumbnailCache.get(videoUrl); + if (cachedImage) { + res.setHeader("Content-Type", "image/jpeg"); + res.setHeader("Cache-Control", "public, max-age=86400"); + return res.send(cachedImage); } try { - const passthrough = new PassThrough() + const passthrough = new PassThrough(); + let imageBuffer = Buffer.alloc(0); + + passthrough.on("data", (chunk) => { + imageBuffer = Buffer.concat([imageBuffer, chunk]); + }); + + passthrough.on("end", () => { + if (imageBuffer.length > 0) { + thumbnailCache.set(videoUrl, imageBuffer); + } + }); ffmpeg(videoUrl) .seekInput(1) .frames(1) .format("image2") .outputOptions("-vcodec", "mjpeg") - .on("error", err => { + .on("error", (err) => { + console.error("ffmpeg error:", err.message); if (!res.headersSent) { - res.status(500).json({error: "Failed to generate thumbnail"}) + res.status(500).json({ error: "Failed to generate thumbnail" }); } }) - .pipe(passthrough, {end: true}) + .pipe(passthrough, { end: true }); - res.setHeader("Content-Type", "image/jpeg") - res.setHeader("Cache-Control", "public, max-age=86400") - passthrough.pipe(res) - } catch { - res.status(500).json({error: "Failed to process video"}) + res.setHeader("Content-Type", "image/jpeg"); + res.setHeader("Cache-Control", "public, max-age=86400"); + + passthrough.pipe(res); + + } catch (err) { + console.error("Error:", err); + if (!res.headersSent) { + res.status(500).json({ error: "Failed to process video" }); + } } -}) +}); app.get("/health", (_req, res) => { - res.json({status: "ok"}) -}) + res.json({ status: "ok" }); +}); app.listen(PORT, () => { - console.log(`Video thumbnail service running on port ${PORT}`) -}) + console.log(`Video thumbnail service running on port ${PORT}`); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7df36f1..582308d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,138 @@ "name": "vthumbs", "version": "1.0.0", "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.1.0", "express": "^4.21.0", - "fluent-ffmpeg": "^2.1.3" + "fluent-ffmpeg": "^2.1.3", + "node-cache": "^5.1.2" } }, + "node_modules/@ffmpeg-installer/darwin-arm64": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz", + "integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "license": "https://git.ffmpeg.org/gitweb/ffmpeg.git/blob_plain/HEAD:/LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffmpeg-installer/darwin-x64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz", + "integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "license": "LGPL-2.1", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffmpeg-installer/ffmpeg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz", + "integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==", + "license": "LGPL-2.1", + "optionalDependencies": { + "@ffmpeg-installer/darwin-arm64": "4.1.5", + "@ffmpeg-installer/darwin-x64": "4.1.0", + "@ffmpeg-installer/linux-arm": "4.1.3", + "@ffmpeg-installer/linux-arm64": "4.1.4", + "@ffmpeg-installer/linux-ia32": "4.1.0", + "@ffmpeg-installer/linux-x64": "4.1.0", + "@ffmpeg-installer/win32-ia32": "4.1.0", + "@ffmpeg-installer/win32-x64": "4.1.0" + } + }, + "node_modules/@ffmpeg-installer/linux-arm": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz", + "integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==", + "cpu": [ + "arm" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-arm64": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz", + "integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-ia32": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz", + "integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==", + "cpu": [ + "ia32" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-x64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz", + "integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/win32-ia32": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz", + "integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==", + "cpu": [ + "ia32" + ], + "license": "GPLv3", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ffmpeg-installer/win32-x64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz", + "integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==", + "cpu": [ + "x64" + ], + "license": "GPLv3", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -98,6 +226,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -551,6 +688,18 @@ "node": ">= 0.6" } }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index e2fc93a..2a73556 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "start": "node index.js" }, "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.1.0", "express": "^4.21.0", - "fluent-ffmpeg": "^2.1.3" + "fluent-ffmpeg": "^2.1.3", + "node-cache": "^5.1.2" } }