Initial release
Build and Push Docker Image / build (push) Successful in 17s

This commit is contained in:
Stardream
2026-06-16 19:26:32 +10:00
commit e3bf77296c
142 changed files with 5481 additions and 0 deletions
+111
View File
@@ -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 };