热点三榜数据接入定稿:全品类单文件(不传赛道一次调用)+ 三子目录落盘

- fetch_week_ranks.py 重写:28 赛道循环→全品类不传 type 每榜每期 1 次调用;hotContentRank source 修正为生产名「抖音每日点赞飙升榜」(GitHub 后缀版返回空);API 单次上限 TOP50(limit/page/offset 实测均无效)固化防再探测;成本铁律「批量付费调用前须先经用户确认」
- 落盘契约:抖音榜单/ 三子目录(账号周榜/视频热榜/点赞榜),hot/likes 全品类单文件/期 抖音周榜_{视频热榜|点赞榜}_{周一}.json;旧 28 赛道 56 文件归档 _archive_旧28赛道结构_20260903
- dsh-data.js:RANK_SUB 子目录映射 + rankFilePath + listRankFiles(子目录优先+根目录旧平铺回退) + collectCategories(week=文件名赛道、hot/likes=条目 category 并集)+ getRanking 赛道筛选(week 文件名级/hot likes 条目级)
- 前端三榜渲染(data-pages.js/app.js/style.css 承接前段)+ 文档同步(主 SKILL.md 落盘表/抓取器说明段)+ dsh 副本 diff=0 + 记忆日志
This commit is contained in:
maogeigei committed 2026-09-03 15:00:01 +08:00
1 parent 2fc8b7b3f6
commit 250d51c8c2
10 files changed
+553 -116

No files matched your search

@@ -326,16 +326,72 @@ function listRecentScripts(limit = 4) {
`).all(n);
}
/* ================= 周榜 ================= */
/* ================= 周榜(三榜族:week 账号周榜 / hot 视频热榜 / likes 点赞榜) ================= */
function rankingMeta() {
// ============ 榜单目录结构(09-03 分目录管理)============
// 抖音榜单/
// 账号周榜/抖音周榜_{赛道}_{周一}.json → week(每赛道一期一文件,douyin-top-account 任务落盘)
// 视频热榜/抖音周榜_视频热榜_{周一}.json → hot(全品类单文件,fetch_week_ranks.py 落盘,不传赛道一次调用)
// 点赞榜/ 抖音周榜_点赞榜_{周一}.json → likes(全品类单文件,同上)
// 根目录旧平铺文件仍兼容读取(任务旧产物)。
// 全品类单文件(hot/likes)无赛道段:category 顶层标记「全部」,实际赛道在 list[].category,
// 赛道下拉 = 各期文件条目 category 并集,赛道筛选按条目 category 匹配。
const RANK_SUB = { week: '账号周榜', hot: '视频热榜', likes: '点赞榜' };
const RANK_FILE_RE = {
week: /^抖音周榜_(.+)_(\d{4}-\d{2}-\d{2})\.json$/,
hot: /^抖音周榜_视频热榜_(\d{4}-\d{2}-\d{2})\.json$/,
likes: /^抖音周榜_点赞榜_(\d{4}-\d{2}-\d{2})\.json$/,
};
function rankFilePath(dir, name) {
return dir ? path.join(RANKING_DIR, dir, name) : path.join(RANKING_DIR, name);
}
function parseRankFileIn(board, dir, name) {
const m = String(name).match(RANK_FILE_RE[board]);
if (!m) return null;
if (board === 'week') return { board, dir, name, category: m[1], date: m[2] };
return { board, dir, name, category: '全部', date: m[1] };
}
function listRankFiles(board) {
if (!fs.existsSync(RANKING_DIR)) return [];
const out = [];
// ① 子目录(新结构,主路径)
const subDir = path.join(RANKING_DIR, RANK_SUB[board]);
if (fs.existsSync(subDir)) {
for (const f of fs.readdirSync(subDir)) {
const p = parseRankFileIn(board, RANK_SUB[board], f);
if (p) out.push(p);
}
}
// ② 根目录(旧平铺兼容)
for (const f of fs.readdirSync(RANKING_DIR)) {
const p = parseRankFileIn(board, '', f);
if (p) out.push(p);
}
return out.sort((a, b) => b.date.localeCompare(a.date));
}
// week:文件名赛道;hot/likes:条目 category 并集(「全部」文件读 list 去重)
function collectCategories(board, files) {
if (board === 'week') return [...new Set(files.map((f) => f.category))];
const set = new Set();
for (const pf of files) {
try {
const d = JSON.parse(fs.readFileSync(rankFilePath(pf.dir, pf.name), 'utf8'));
for (const it of (d.list || [])) { const c = String(it.category || '').trim(); if (c) set.add(c); }
} catch (e) {}
}
return [...set].sort();
}
function rankingMeta(board = 'week') {
const empty = { dates: [], categories: [] };
if (!fs.existsSync(RANKING_DIR)) return empty;
const files = fs.readdirSync(RANKING_DIR).filter((f) => /^抖音周榜_.+_\d{4}-\d{2}-\d{2}\.json$/.test(f)).sort().reverse();
const files = listRankFiles(board);
if (!files.length) return empty;
const dates = [...new Set(files.map((f) => f.match(/_(\d{4}-\d{2}-\d{2})\.json$/)[1]))];
const categories = [...new Set(files.map((f) => { const m = f.match(/^抖音周榜_(.+)_\d{4}-\d{2}-\d{2}\.json$/); return m ? m[1] : null; }).filter(Boolean))];
return { dates, categories };
const dates = [...new Set(files.map((f) => f.date))];
return { dates, categories: collectCategories(board, files) };
}
function parseNum(v) {
@@ -347,31 +403,36 @@ function parseNum(v) {
return n;
}
function getRanking({ date = '', category = '', sortBy = 'rank', order = '', page = 1, pageSize = 20 } = {}) {
function getRanking({ board = 'week', date = '', category = '', sortBy = '', order = '', page = 1, pageSize = 20 } = {}) {
const empty = { dates: [], categories: [], category: '', date: '', total: 0, items: [] };
if (!fs.existsSync(RANKING_DIR)) return empty;
const files = fs.readdirSync(RANKING_DIR).filter((f) => /^抖音周榜_.+_\d{4}-\d{2}-\d{2}\.json$/.test(f)).sort().reverse();
const files = listRankFiles(board);
if (!files.length) return empty;
const dates = [...new Set(files.map((f) => f.match(/_(\d{4}-\d{2}-\d{2})\.json$/)[1]))];
const categories = [...new Set(files.map((f) => { const m = f.match(/^抖音周榜_(.+)_\d{4}-\d{2}-\d{2}\.json$/); return m ? m[1] : null; }).filter(Boolean))];
const dates = [...new Set(files.map((f) => f.date))];
const categories = collectCategories(board, files);
const selDate = date || dates[0];
const selFiles = files.filter((f) => f.includes(selDate) && (!category || f.includes('_' + category + '_')));
// week:按文件名赛道过滤(每赛道一文件);hot/likes 全品类单文件:赛道过滤在条目层
const selFiles = files.filter((f) => f.date === selDate && (board !== 'week' || !category || f.category === category));
let items = [];
for (const f of selFiles) {
try { items = items.concat(JSON.parse(fs.readFileSync(path.join(RANKING_DIR, f), 'utf8')).list || []); } catch (e) {}
for (const pf of selFiles) {
try { items = items.concat(JSON.parse(fs.readFileSync(rankFilePath(pf.dir, pf.name), 'utf8')).list || []); } catch (e) {}
}
// 默认排序:综合得分倒序,同分按排名升序(用户点表头排序时走下方 sortBy/order 逻辑)
if (!order && (!sortBy || sortBy === 'rank')) {
if (board !== 'week' && category && category !== '全部') {
items = items.filter((it) => String(it.category || '').trim() === category);
}
// 默认排序键随榜:week=综合得分 / hot=点赞 / likes=新增点赞(同分按排名升序)
const DEF_KEY = board === 'week' ? 'comprehensiveScore' : board === 'hot' ? 'likeCount' : 'add_digg_count';
if (!order && (!sortBy || sortBy === DEF_KEY)) {
items.sort((a, b) => {
const scA = num(a.comprehensiveScore), scB = num(b.comprehensiveScore);
if (scA !== scB) return scB - scA;
const va = num(a[DEF_KEY]), vb = num(b[DEF_KEY]);
if (va !== vb) return vb - va;
return num(a.rank) - num(b.rank);
});
} else {
const orderDir = order === 'asc' ? 'asc' : (order === 'desc' ? 'desc' : (sortBy === 'rank' ? 'asc' : 'desc'));
const key = sortBy || DEF_KEY;
const orderDir = order === 'asc' ? 'asc' : (order === 'desc' ? 'desc' : (key === 'rank' ? 'asc' : 'desc'));
items.sort((a, b) => {
const va = sortBy === 'rank' ? num(a.rank) : sortBy === 'comprehensiveScore' ? num(a.comprehensiveScore) : parseNum(a[sortBy]);
const vb = sortBy === 'rank' ? num(b.rank) : sortBy === 'comprehensiveScore' ? num(b.comprehensiveScore) : parseNum(b[sortBy]);
const va = key === 'rank' ? num(a.rank) : parseNum(a[key]);
const vb = key === 'rank' ? num(b.rank) : parseNum(b[key]);
return orderDir === 'asc' ? va - vb : vb - va;
});
}
@@ -381,12 +442,13 @@ function getRanking({ date = '', category = '', sortBy = 'rank', order = '', pag
const d = getDb();
const secOf = (url) => { const m = String(url || '').match(/\/user\/([^/?#]+)/); return m ? m[1] : ''; };
const pageItems = items.slice(offset, offset + limit).map((row) => {
// 账号维度(week,含 profileUrl)才做库内命中关联;作品维度不关联账号表
const sec = row.profileUrl ? secOf(row.profileUrl) : '';
let hit = null;
if (d) {
const sec = secOf(row.profileUrl);
hit = sec
? d.prepare(`SELECT id FROM hot_accounts WHERE sec_uid=? OR account_name=? LIMIT 1`).get(sec, row.accountName)
: d.prepare(`SELECT id FROM hot_accounts WHERE account_name=? LIMIT 1`).get(row.accountName);
if (d && sec) {
hit = d.prepare(`SELECT id FROM hot_accounts WHERE sec_uid=? OR account_name=? LIMIT 1`).get(sec, row.accountName);
} else if (d && row.accountName && !row.profileUrl && row.workUrl) {
hit = null; // 作品榜条目不查账号表
}
return { ...row, inDb: !!hit, dbId: hit ? hit.id : null };
});