refactor(thumbnail): 优化缩略图生成逻辑并移除元数据文件
重构缩略图生成逻辑,使用动态文件名包含处理参数 移除不再需要的元数据文件处理代码 清理临时文件时增加对视频帧临时文件的处理
This commit is contained in:
34
index.js
34
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 (_) { }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user