refactor(thumbnail): 优化缩略图生成逻辑并移除元数据文件

重构缩略图生成逻辑,使用动态文件名包含处理参数
移除不再需要的元数据文件处理代码
清理临时文件时增加对视频帧临时文件的处理
This commit is contained in:
2026-01-08 12:17:46 +08:00
parent 755f8d45c3
commit e50df0eaf6
2 changed files with 16 additions and 20 deletions

View File

@@ -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 (_) { }
}
}

2
new.js
View File

@@ -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