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
+132
View File
@@ -0,0 +1,132 @@
'use strict';
const { STATUS } = require('./constants');
function nowISO() { return new Date().toISOString(); }
function uniqStrings(arr) {
const seen = new Set(); const out = [];
for (const v of arr || []) {
const name = String(v ?? '').trim();
if (!name || seen.has(name)) continue;
seen.add(name); out.push(name);
}
return out;
}
function normalizeTags(tags) {
if (!tags) return [];
if (Array.isArray(tags)) return uniqStrings(tags.map(t => typeof t === 'string' ? t : (t?.name || t?.label || t?.tag || '')));
if (typeof tags === 'string') return uniqStrings(tags.split(/[,\s]+/));
return [];
}
function normalizePublicTags(tags) {
if (!tags) return [];
const list = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(/[,\s]+/) : []);
const out = []; const seen = new Set();
for (const t of list) {
const name = String(typeof t === 'string' ? t : (t?.name || t?.label || t?.tag || '')).trim();
if (!name || seen.has(name)) continue;
seen.add(name);
const count = typeof t === 'object' && t ? Number(t.count ?? t.total_cont ?? t.total ?? t.num ?? 0) || 0 : 0;
out.push(count ? { name, count } : { name });
}
return out;
}
function myTagsOf(c) { return normalizeTags(c?.myTags || c?.my_tags || c?.tags || []); }
function publicTagsOf(c) { return normalizePublicTags(c?.publicTags || c?.public_tags || c?.subjectTags || c?.raw?.subject?.tags || []); }
function normalizeCollection(item, fallbackType) {
const s = item.subject || {};
const imgs = s.images || {};
const st = item.subject_type || s.type || fallbackType?.subject_type || 0;
const ct = item.type || fallbackType?.collection_type || 0;
const title = s.name_cn || s.name || `Subject ${item.subject_id || s.id}`;
const raw = item;
return {
subject_id: Number(item.subject_id || s.id), subject_type: Number(st), collection_type: Number(ct),
title, name: s.name || '', name_cn: s.name_cn || '', date: s.date || '', eps: s.eps ?? '', volumes: s.volumes ?? '',
bgm_score: s.score ?? s.rating?.score ?? '', rank: s.rank ?? '',
summary: s.summary || s.short_summary || '', image: imgs.large || imgs.common || imgs.medium || imgs.grid || imgs.small || '', images: imgs,
rate: item.rate ?? 0, comment: item.comment || '', tags: normalizeTags(item.tags), myTags: normalizeTags(item.tags), publicTags: normalizePublicTags(s.tags || item.publicTags || item.public_tags), ep_status: item.ep_status ?? '', vol_status: item.vol_status ?? '',
private: !!item.private,
collection_created_at: item.created_at || item.createdAt || item.collect_created_at || '',
bangumi_updated_at: item.updated_at || item.updatedAt || '', raw
};
}
function applySubjectDetail(n, detail) {
if (!detail) return n;
return {
...n,
platform: detail.platform || '',
nsfw: !!detail.nsfw,
infobox: Array.isArray(detail.infobox) ? detail.infobox : [],
meta_tags: Array.isArray(detail.meta_tags) ? detail.meta_tags : [],
series: !!detail.series,
locked: !!detail.locked,
rating_detail: detail.rating || {},
collection_stats: detail.collection || {},
total_episodes: detail.total_episodes ?? n.eps ?? '',
publicTags: detail.tags ? normalizePublicTags(detail.tags) : n.publicTags,
subject_raw: detail,
subject_fetched_at: nowISO(),
};
}
function stableStringify(obj) {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
return '{' + Object.keys(obj).sort().map(k => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}';
}
function hashString(str) { let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0).toString(16); }
function comparable(n) {
return {
subject_id: n.subject_id, subject_type: n.subject_type, collection_type: n.collection_type, title: n.title, name: n.name, name_cn: n.name_cn,
date: n.date, eps: n.eps, volumes: n.volumes, bgm_score: n.bgm_score, rank: n.rank, summary: n.summary, image: n.image, rate: n.rate, comment: n.comment,
tags: myTagsOf(n), publicTags: publicTagsOf(n), ep_status: n.ep_status, vol_status: n.vol_status, private: n.private,
collection_created_at: n.collection_created_at, bangumi_updated_at: n.bangumi_updated_at
};
}
function makeSnapshot(n) { return { ...n, content_hash: hashString(stableStringify(comparable(n))) }; }
// --- change-detection for history entries (server-side simplified formatting) ---
function fmtDate(s) { if (!s) return ''; const d = new Date(s); return isNaN(d) ? String(s) : d.toLocaleString(); }
function fmtScore(v) { const n = Number(v); if (!n) return ''; return Number.isInteger(n) ? String(n) : n.toFixed(1).replace(/\.0$/, ''); }
function shortValue(v, max = 80) { const t = Array.isArray(v) ? v.join(' / ') : String(v ?? ''); return t.length > max ? t.slice(0, max) + '…' : t; }
function valuesSame(a, b) { return stableStringify(a ?? '') === stableStringify(b ?? ''); }
function collectionFieldDiff(oldC, newC) {
const fields = [
['collection_type', '收藏状态', v => STATUS[v] || v || '-'],
['rate', '我的评分', v => v || '未评分'],
['ep_status', '章节进度', v => v || '-'],
['vol_status', '卷进度', v => v || '-'],
['comment', '吐槽/观后感', v => shortValue(v || '无吐槽', 120)],
['private', '私密状态', v => v ? '私密' : '公开'],
['tags', '我的标签', v => shortValue(normalizeTags(v), 120)],
['publicTags', '公共标签', v => shortValue(normalizePublicTags(v).map(x => x.count ? `${x.name}(${x.count})` : x.name), 120)],
['bangumi_updated_at', 'API 更新时间', v => fmtDate(v) || '-'],
['title', '标题', v => v || '-'],
['date', '上映日期', v => v || '-'],
['bgm_score', 'Bangumi 均分', v => fmtScore(v) || '-'],
['rank', '排名', v => v || '-']
];
const changes = [];
for (const [key, label, fmt] of fields) {
const oldVal = key === 'tags' ? myTagsOf(oldC) : (key === 'publicTags' ? publicTagsOf(oldC) : oldC?.[key]);
const newVal = key === 'tags' ? myTagsOf(newC) : (key === 'publicTags' ? publicTagsOf(newC) : newC?.[key]);
if (!valuesSame(oldVal, newVal)) changes.push({ field: key, label, from: fmt(oldVal), to: fmt(newVal), old: oldVal ?? '', new: newVal ?? '' });
}
return changes;
}
module.exports = {
nowISO, uniqStrings, normalizeTags, normalizePublicTags, myTagsOf, publicTagsOf,
normalizeCollection, applySubjectDetail, stableStringify, hashString, comparable, makeSnapshot, collectionFieldDiff
};