热点三榜数据接入定稿:全品类单文件(不传赛道一次调用)+ 三子目录落盘
- 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:
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 };
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user