From e50df0eaf62716852c31cb062544db3fae317caa Mon Sep 17 00:00:00 2001 From: XiaoMo Date: Thu, 8 Jan 2026 12:17:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(thumbnail):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BC=A9=E7=95=A5=E5=9B=BE=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=B9=B6=E7=A7=BB=E9=99=A4=E5=85=83=E6=95=B0=E6=8D=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构缩略图生成逻辑,使用动态文件名包含处理参数 移除不再需要的元数据文件处理代码 清理临时文件时增加对视频帧临时文件的处理 --- index.js | 34 +++++++++++++++------------------- new.js | 2 +- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 9f83165..bcbf7ce 100644 --- a/index.js +++ b/index.js @@ -126,18 +126,19 @@ async function generateThumbAndCache(reply, apiData, contentPath) { const srcPath = contentPath; const dir = path.dirname(srcPath); const base = path.basename(srcPath); - const thumbFinal = path.join(dir, base.replace('.data', `.thumb`)); - const metaThumbPath = contentPath.replace('.data', '.thumb.meta'); - if (fs.existsSync(thumbFinal) && fs.existsSync(metaThumbPath)) { + const fit = (apiData.data.thumb && apiData.data.thumb.fit === 'max') ? 'inside' : 'cover'; + const width = (apiData.data.thumb && apiData.data.thumb.w) ? apiData.data.thumb.w : 100; + const contentType = (apiData.data.headers && apiData.data.headers['content-type']) || getMimeFromUrl(apiData.data.url) || 'image/jpeg'; + const preferredFmt = contentType.includes('png') ? 'png' : contentType.includes('webp') ? 'webp' : 'jpeg'; + const variantFmt = contentType.includes('video/') ? 'webp' : preferredFmt; + const variantSuffix = `.thumb_${fit}_w${width}_${variantFmt}`; + const thumbFinal = path.join(dir, base.replace('.data', variantSuffix)); + if (fs.existsSync(thumbFinal)) { const st = fs.statSync(thumbFinal); if (st.size > 0) { - // metaPath 读取 metadata - const metaJson = JSON.parse(fs.readFileSync(metaThumbPath, 'utf8')); - // inputFormat - const inputFormat = metaJson.inputFormat || 'webp'; const responseHeaders = { ...apiData.data.headers, ...{ - 'Content-Type': `image/${inputFormat === 'jpg' ? 'jpeg' : inputFormat}`, + 'Content-Type': `image/${variantFmt === 'jpg' ? 'jpeg' : variantFmt}`, 'Content-Length': st.size, 'Accept-Ranges': 'bytes', } @@ -170,14 +171,9 @@ async function generateThumbAndCache(reply, apiData, contentPath) { reply.code(500); return 'Thumb source file is empty'; } - const fit = (apiData.data.thumb && apiData.data.thumb.fit === 'max') ? 'inside' : 'cover'; - const width = (apiData.data.thumb && apiData.data.thumb.w) ? apiData.data.thumb.w : 100; - const contentType = (apiData.data.headers && apiData.data.headers['content-type']) || getMimeFromUrl(apiData.data.url) || 'image/jpeg'; - const preferredFmt = contentType.includes('png') ? 'png' : contentType.includes('webp') ? 'webp' : 'jpeg'; - if (contentType.includes('video/')) { console.log('Generating video thumb:', srcPath); - const thumbFrameTemp = path.join(dir, base.replace('.data', `.thumb.frame.png.tmp`)); + const thumbFrameTemp = path.join(dir, base.replace('.data', `${variantSuffix}.frame.png.tmp`)); const { spawn } = require('child_process'); const args = ['-hide_banner', '-loglevel', 'error', '-nostdin', '-ss', '1', '-i', srcPath, '-frames:v', '1', '-vf', `scale=${width}:-2`, '-f', 'image2', '-vcodec', 'png', '-y', thumbFrameTemp]; await new Promise((resolve, reject) => { @@ -187,10 +183,9 @@ async function generateThumbAndCache(reply, apiData, contentPath) { p.on('error', reject); p.on('close', c => c === 0 ? resolve() : reject(new Error(`ffmpeg exit ${c}${err ? ': ' + err.trim() : ''}`))); }); - const thumbTemp = path.join(dir, base.replace('.data', `.thumb.tmp`)); + const thumbTemp = path.join(dir, base.replace('.data', `${variantSuffix}.tmp`)); await sharp(thumbFrameTemp).webp({ quality: 80 }).toFile(thumbTemp); try { fs.renameSync(thumbTemp, thumbFinal); } catch (e) { if (fs.existsSync(thumbFinal)) { try { fs.unlinkSync(thumbFinal); } catch (_) { } fs.renameSync(thumbTemp, thumbFinal); } else { throw e; } } - await fs.promises.writeFile(metaThumbPath, JSON.stringify({ api: apiData, headers: apiData.data.headers || {}, srcSize: stat.size, inputFormat: 'webp' })); try { if (fs.existsSync(thumbFrameTemp)) fs.unlinkSync(thumbFrameTemp); } catch (_) { } const tstat = fs.statSync(thumbFinal); reply.headers({ 'Content-Type': 'image/webp', 'Content-Length': tstat.size, 'Accept-Ranges': 'bytes', 'Access-Control-Allow-Origin': '*' }); @@ -198,7 +193,7 @@ async function generateThumbAndCache(reply, apiData, contentPath) { } const inputMeta = await sharp(srcPath).metadata(); const outFmt = preferredFmt || inputMeta.format || 'jpeg'; - const thumbTemp = path.join(dir, base.replace('.data', `.thumb.tmp`)); + const thumbTemp = path.join(dir, base.replace('.data', `${variantSuffix}.tmp`)); const pipeline = sharp(srcPath).resize({ width, fit }); if (outFmt === 'jpeg') pipeline.jpeg({ quality: 85 }); else if (outFmt === 'png') pipeline.png(); @@ -214,7 +209,6 @@ async function generateThumbAndCache(reply, apiData, contentPath) { throw e; } } - await fs.promises.writeFile(metaThumbPath, JSON.stringify({ api: apiData, headers: apiData.data.headers || {}, srcSize: stat.size, inputFormat: inputMeta.format || null })); const tstat = fs.statSync(thumbFinal); const responseHeaders = { ...apiData.data.headers, ...{ @@ -232,8 +226,10 @@ async function generateThumbAndCache(reply, apiData, contentPath) { // cleanup leftover temp if any const dir = path.dirname(srcPath); const base = path.basename(srcPath); - const thumbTemp = path.join(dir, base.replace('.data', `.thumb.tmp`)); + const thumbTemp = path.join(dir, base.replace('.data', `${variantSuffix}.tmp`)); + const thumbFrameTemp = path.join(dir, base.replace('.data', `${variantSuffix}.frame.png.tmp`)); try { if (fs.existsSync(thumbTemp)) fs.unlinkSync(thumbTemp); } catch (_) { } + try { if (fs.existsSync(thumbFrameTemp)) fs.unlinkSync(thumbFrameTemp); } catch (_) { } } } diff --git a/new.js b/new.js index 2ba9cef..ec2c756 100644 --- a/new.js +++ b/new.js @@ -8,7 +8,7 @@ const EventEmitter = require('events'); // Configuration const PORT = process.env.PORT || 9520; -const API_BASE = 'http://127.0.0.1:9558/api'; +const API_BASE = process.env.API_BASE || 'http://127.0.0.1:9558/api'; const CACHE_DIR = process.env.CACHE_DIR ? path.resolve(process.env.CACHE_DIR) : path.join(__dirname, '.cache'); // Ensure cache directory exists