Files
Stardream e3bf77296c
Build and Push Docker Image / build (push) Successful in 17s
Initial release
2026-06-16 20:27:36 +10:00

133 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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
};