This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
const http = require('node:http');
|
||||
const https = require('node:https');
|
||||
const fs = require('node:fs');
|
||||
const fsp = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const { URL } = require('node:url');
|
||||
|
||||
const { safeName, extFromContentType } = require('./http');
|
||||
|
||||
const FILES_DIR = process.env.BANGUMI_VAULT_FILES_DIR || '/data/files';
|
||||
const COVERS_DIR = path.join(FILES_DIR, 'covers');
|
||||
const LOGS_DIR = path.join(FILES_DIR, 'logs');
|
||||
|
||||
function ensureDirs() {
|
||||
fs.mkdirSync(COVERS_DIR, { recursive: true });
|
||||
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function downloadBuffer(remoteUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(remoteUrl);
|
||||
} catch {
|
||||
reject(new Error('invalid url'));
|
||||
return;
|
||||
}
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
reject(new Error('unsupported url protocol'));
|
||||
return;
|
||||
}
|
||||
const client = parsed.protocol === 'https:' ? https : http;
|
||||
const req = client.get(parsed, {
|
||||
headers: { 'User-Agent': 'BangumiVaultDocker/1.0', 'Referer': 'https://bgm.tv/' },
|
||||
timeout: 30000
|
||||
}, res => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
res.resume();
|
||||
downloadBuffer(new URL(res.headers.location, parsed).toString()).then(resolve, reject);
|
||||
return;
|
||||
}
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
res.resume();
|
||||
reject(new Error(`HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
res.on('data', chunk => {
|
||||
size += chunk.length;
|
||||
if (size > 25 * 1024 * 1024) {
|
||||
req.destroy(new Error('image too large'));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
res.on('end', () => resolve({ buffer: Buffer.concat(chunks), contentType: res.headers['content-type'] || '' }));
|
||||
});
|
||||
req.on('timeout', () => req.destroy(new Error('download timeout')));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Rewrite an online image URL through the configured proxy/mirror (empty = no change).
|
||||
function applyImageProxy(url, proxy) {
|
||||
proxy = (proxy || '').trim();
|
||||
if (!proxy || !url || !/^https?:\/\//i.test(url)) return url;
|
||||
return proxy.includes('{url}') ? proxy.replace('{url}', encodeURIComponent(url)) : proxy + url;
|
||||
}
|
||||
|
||||
// Download one cover and write it to disk. Returns { url, file } paths used by the app.
|
||||
async function cacheCover(subjectId, remoteUrl, imageProxy) {
|
||||
const sid = safeName(subjectId, 'subject');
|
||||
const downloaded = await downloadBuffer(applyImageProxy(remoteUrl, imageProxy));
|
||||
const file = `${sid}${extFromContentType(downloaded.contentType, remoteUrl)}`;
|
||||
await fsp.writeFile(path.join(COVERS_DIR, file), downloaded.buffer);
|
||||
return { url: `/images/${file}`, file: `covers/${file}` };
|
||||
}
|
||||
|
||||
function onlineCoverUrl(c) {
|
||||
const imgs = c.images || {};
|
||||
return c.image || imgs.large || imgs.common || imgs.medium || imgs.grid || imgs.small || '';
|
||||
}
|
||||
|
||||
// Cache covers for every collection that doesn't already have a local copy.
|
||||
// Mutates each row in-place (cover_local_url / cover_local_file / cover_cached_at).
|
||||
async function cacheCoversForState(collections, onProgress, imageProxy) {
|
||||
const rows = Object.values(collections || {}).filter(c => c.status !== 'confirmed_deleted' && !c.cover_local_url);
|
||||
let ok = 0, fail = 0;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const c = rows[i];
|
||||
const url = onlineCoverUrl(c);
|
||||
if (!url) { fail++; continue; }
|
||||
try {
|
||||
const r = await cacheCover(c.subject_id, url, imageProxy);
|
||||
c.cover_local_url = r.url;
|
||||
c.cover_local_file = r.file;
|
||||
c.cover_cached_at = new Date().toISOString();
|
||||
ok++;
|
||||
} catch {
|
||||
fail++;
|
||||
}
|
||||
if (onProgress) onProgress(i + 1, rows.length, ok, fail);
|
||||
await new Promise(r => setTimeout(r, 35));
|
||||
}
|
||||
return { ok, fail, total: rows.length };
|
||||
}
|
||||
|
||||
module.exports = { FILES_DIR, COVERS_DIR, LOGS_DIR, ensureDirs, downloadBuffer, cacheCover, cacheCoversForState };
|
||||
Reference in New Issue
Block a user