feat/add-thumbnail-caching #3

Merged
hodlbod merged 1 commits from priyanshu_bharti/vthumbs:feat/add-thumbnail-caching into master 2026-04-14 17:21:10 +00:00
3 changed files with 209 additions and 24 deletions
+56 -22
View File
@@ -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}`);
});
+150 -1
View File
@@ -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",
+3 -1
View File
@@ -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"
}
}