2026-08-27 12:33:57 +08:00
// MCN工作台 - 数据访问层(只读复用 dsh 环境数据)
// 数据源:工作台目录 mcn-plugin.db( dsh 库副本,node:sqlite 只读)+ D:\dshworkspace\抖音榜单\*.json
// 与 dsh 原库(~/.dsh/mcn-plugin.db)隔离:两边各访问各的,互不影响
// 页面逻辑参照 dsh-plugin-mcn 重新开发,仅复用数据库 schema 与榜单文件格式
'use strict' ;
const { DatabaseSync } = require ( 'node:sqlite' );
const path = require ( 'node:path' );
const fs = require ( 'node:fs' );
// 工作台自有数据库副本(与 dsh 原库隔离)
const DSH_DB = path . join ( __dirname , 'mcn-plugin.db' );
// 周榜目录:默认 dsh 工作区;可用环境变量 MCN_RANKING_DIR 覆盖
const RANKING_DIR = process . env . MCN_RANKING_DIR || 'D:\\dshworkspace\\抖音榜单' ;
let _db = null ;
let _dbTried = false ;
function getDb () {
if ( _db ) return _db ;
if ( _dbTried ) return null ;
_dbTried = true ;
try {
if ( ! fs . existsSync ( DSH_DB )) return null ;
_db = new DatabaseSync ( DSH_DB , { readOnly : true });
} catch ( e ) { _db = null ; }
return _db ;
}
function dbAvailable () { return getDb () !== null ; }
2026-08-28 18:54:01 +08:00
// 收藏切换:可写连接单独打开(与只读 getDb 隔离),操作完即关闭
function setFavorite ( accountId , fav ) {
if ( ! fs . existsSync ( DSH_DB )) throw new Error ( '数据库不可用' );
const w = new DatabaseSync ( DSH_DB );
try {
const r = w . prepare ( `UPDATE hot_accounts SET favorite=? WHERE id=?` ). run ( fav ? 1 : 0 , Number ( accountId ));
if ( r . changes === 0 ) throw new Error ( '账号不存在' );
} finally {
w . close ();
}
}
2026-08-27 12:33:57 +08:00
const num = ( v ) => { const n = Number ( v ); return Number . isFinite ( n ) ? n : 0 ; };
const fmt = ( v ) => ( v == null || v === '' ) ? null : v ;
2026-09-01 14:31:46 +08:00
// 批量删除:kind=accounts(软删账号+连坐清理)/ videos | rewrites(删视频行+关联)
// ids: 主键 id 数组;返回 { deleted: 实际删除条数 }
// 连坐范围:账号 → persona/analysis/videos(含 source/analysis/rewrite_log);视频 → source/analysis/rewrite_log
// 注意 source/analysis 存在 video_id=null 的行(俊希等历史数据),须同时按 aweme_id 双键删
function batchDelete ( kind , ids ) {
if ( ! fs . existsSync ( DSH_DB )) throw new Error ( '数据库不可用' );
const clean = ( Array . isArray ( ids ) ? ids : []). map ( Number ). filter (( n ) => Number . isFinite ( n ) && n > 0 );
if ( clean . length === 0 ) return { deleted : 0 };
const q = clean . map (() => '?' ). join ( ',' );
const w = new DatabaseSync ( DSH_DB );
try {
if ( kind === 'accounts' ) {
w . prepare ( `DELETE FROM account_persona WHERE account_id IN ( ${ q } )` ). run (... clean );
w . prepare ( `DELETE FROM account_analysis WHERE account_id IN ( ${ q } )` ). run (... clean );
deleteVideosDeep ( w , w . prepare ( `SELECT id, aweme_id FROM account_videos WHERE account_id IN ( ${ q } )` ). all (... clean ));
w . prepare ( `DELETE FROM account_videos WHERE account_id IN ( ${ q } )` ). run (... clean );
w . prepare ( `DELETE FROM rewrite_log WHERE account_id IN ( ${ q } )` ). run (... clean );
const r = w . prepare ( `UPDATE hot_accounts SET del_flag=1 WHERE id IN ( ${ q } ) AND del_flag=0` ). run (... clean );
return { deleted : Number ( r . changes ) };
}
// videos | rewrites:删视频行 + 关联解析/脚本(按 video_id 与 aweme_id 双键)
deleteVideosDeep ( w , w . prepare ( `SELECT id, aweme_id FROM account_videos WHERE id IN ( ${ q } )` ). all (... clean ));
const r = w . prepare ( `DELETE FROM account_videos WHERE id IN ( ${ q } )` ). run (... clean );
return { deleted : Number ( r . changes ) };
} finally {
w . close ();
}
}
function deleteVideosDeep ( w , vids ) {
if ( ! vids . length ) return ;
const ids = vids . map (( v ) => v . id );
const awemes = vids . map (( v ) => v . aweme_id ). filter ( Boolean );
const qi = ids . map (() => '?' ). join ( ',' );
w . prepare ( `DELETE FROM account_video_source WHERE video_id IN ( ${ qi } )` ). run (... ids );
w . prepare ( `DELETE FROM account_video_analysis WHERE video_id IN ( ${ qi } )` ). run (... ids );
w . prepare ( `DELETE FROM rewrite_log WHERE video_id IN ( ${ qi } )` ). run (... ids );
if ( awemes . length ) {
const qa = awemes . map (() => '?' ). join ( ',' );
w . prepare ( `DELETE FROM account_video_source WHERE aweme_id IN ( ${ qa } )` ). run (... awemes );
w . prepare ( `DELETE FROM account_video_analysis WHERE aweme_id IN ( ${ qa } )` ). run (... awemes );
w . prepare ( `DELETE FROM rewrite_log WHERE aweme_id IN ( ${ qa } )` ). run (... awemes );
}
}
2026-08-27 12:33:57 +08:00
/* ================= 账号 ================= */
function listTracks () {
const d = getDb (); if ( ! d ) return [];
const rows = d . prepare ( `SELECT DISTINCT track FROM hot_accounts WHERE track IS NOT NULL AND track != '' AND del_flag=0 ORDER BY track` ). all ();
return rows . map (( r ) => r . track );
}
function listAccounts ({ page = 1 , pageSize = 20 , search = '' , track = '' , type = 'all' } = {}) {
const d = getDb ();
if ( ! d ) return { total : 0 , page : 1 , pageSize , items : [] };
const where = [ 'del_flag=0' ];
const params = [];
if ( search ) {
where . push ( '(account_name LIKE ? OR content LIKE ? OR track LIKE ?)' );
const like = `% ${ search } %` ; params . push ( like , like , like );
}
if ( track ) { where . push ( 'track = ?' ); params . push ( track ); }
if ( type === 'signed' || type === 'external' ) { where . push ( 'account_type = ?' ); params . push ( type ); }
const whereSql = where . length ? 'WHERE ' + where . join ( ' AND ' ) : '' ;
const total = d . prepare ( `SELECT COUNT(*) c FROM hot_accounts ${ whereSql } ` ). get (... params ). c ;
const limit = Math . max ( 1 , Math . min ( Number ( pageSize ) || 20 , 100 ));
const offset = ( Math . max ( 1 , Number ( page ) || 1 ) - 1 ) * limit ;
const followersExpr = `CASE WHEN h.followers LIKE '%亿%' THEN CAST(REPLACE(REPLACE(h.followers,'亿',''),'万','') AS REAL) * 100000000 WHEN h.followers LIKE '%万%' THEN CAST(REPLACE(h.followers,'万','') AS REAL) * 10000 ELSE CAST(h.followers AS REAL) END` ;
const orderBySql = `h.favorite DESC, ${ followersExpr } DESC, h.id ASC` ;
const items = d . prepare ( `SELECT h.*, (SELECT COUNT(*) FROM account_videos v WHERE v.account_id = h.id) AS video_count FROM hot_accounts h ${ whereSql } ORDER BY ${ orderBySql } LIMIT ? OFFSET ?` ). all (... params , limit , offset );
return { total , page : Math . max ( 1 , Number ( page ) || 1 ), pageSize : limit , items };
}
function listAccountVideos ( accountId , page = 1 , pageSize = 20 ) {
const d = getDb (); if ( ! d ) throw new Error ( '数据库不可用' );
const acc = d . prepare ( `SELECT * FROM hot_accounts WHERE id=?` ). get ( accountId );
if ( ! acc ) throw new Error ( '账号不存在' );
const total = d . prepare ( `SELECT COUNT(*) c FROM account_videos WHERE account_id=?` ). get ( accountId ). c ;
const limit = Math . max ( 1 , Math . min ( Number ( pageSize ) || 20 , 100 ));
const offset = ( Math . max ( 1 , Number ( page ) || 1 ) - 1 ) * limit ;
const items = d . prepare ( `
SELECT v.*,
(SELECT COUNT(*) FROM account_video_source s WHERE s.video_id=v.id OR s.aweme_id=v.aweme_id) AS has_source,
(SELECT COUNT(*) FROM account_video_analysis a WHERE a.video_id=v.id OR a.aweme_id=v.aweme_id) AS has_analysis
2026-08-28 18:54:01 +08:00
FROM account_videos v WHERE v.account_id=? ORDER BY v.publish_time IS NULL, v.publish_time DESC, v.id DESC LIMIT ? OFFSET ?
2026-08-27 12:33:57 +08:00
` ). all ( accountId , limit , offset );
return { account : acc , total , page : Math . max ( 1 , Number ( page ) || 1 ), pageSize : limit , items };
}
function getPersona ( accountId ) {
const d = getDb (); if ( ! d ) return null ;
return d . prepare ( `SELECT content_json AS content, analysis_time FROM account_persona WHERE account_id=? ORDER BY id DESC LIMIT 1` ). get ( accountId ) || null ;
}
function getAccountAnalysis ( accountId ) {
const d = getDb (); if ( ! d ) return null ;
return d . prepare ( `SELECT content_json AS content, analysis_time FROM account_analysis WHERE account_id=? ORDER BY id DESC LIMIT 1` ). get ( accountId ) || null ;
}
/* ================= 视频 ================= */
function listVideos ({ page = 1 , pageSize = 20 , search = '' , parsed = '' } = {}) {
const d = getDb ();
if ( ! d ) return { total : 0 , page : 1 , pageSize , items : [] };
const where = [ `v.id IN (SELECT id FROM (SELECT id, ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY publish_time DESC, id DESC) rn FROM account_videos WHERE like_count >= 300000 AND comment_count >= 10000) t WHERE rn <= 10)` ];
const params = [];
if ( search ) { where . push ( '(v.video_title LIKE ? OR h.account_name LIKE ?)' ); params . push ( `% ${ search } %` , `% ${ search } %` ); }
if ( parsed === '1' || parsed === '0' ) {
const cond = `(v.id IN (SELECT video_id FROM account_video_source) OR v.id IN (SELECT video_id FROM account_video_analysis))` ;
where . push ( parsed === '1' ? cond : `NOT ( ${ cond } )` );
}
const w = 'WHERE ' + where . join ( ' AND ' );
const total = d . prepare ( `SELECT COUNT(*) c FROM account_videos v LEFT JOIN hot_accounts h ON h.id=v.account_id ${ w } ` ). get (... params ). c ;
const limit = Math . max ( 1 , Math . min ( Number ( pageSize ) || 20 , 100 ));
const offset = ( Math . max ( 1 , Number ( page ) || 1 ) - 1 ) * limit ;
const items = d . prepare ( `
SELECT v.*, h.account_name,
SUM(v.like_count) OVER (PARTITION BY v.account_id) AS acc_likes,
(SELECT COUNT(*) FROM account_video_source s WHERE s.video_id=v.id OR s.aweme_id=v.aweme_id) AS has_source,
(SELECT COUNT(*) FROM account_video_analysis a WHERE a.video_id=v.id OR a.aweme_id=v.aweme_id) AS has_analysis,
(SELECT COUNT(*) FROM rewrite_log r WHERE r.video_id=v.id AND r.status='done' AND r.script_text IS NOT NULL AND r.script_text <> '') AS rewrite_count
FROM account_videos v LEFT JOIN hot_accounts h ON h.id=v.account_id
${ w } ORDER BY acc_likes DESC, v.publish_time DESC, v.id DESC LIMIT ? OFFSET ?
` ). all (... params , limit , offset );
return { total , page : Math . max ( 1 , Number ( page ) || 1 ), pageSize : limit , items };
}
function getVideoDetail ({ videoId , awemeId }) {
const d = getDb (); if ( ! d ) throw new Error ( '数据库不可用' );
let video = null ;
if ( videoId ) {
video = d . prepare ( `SELECT v.*, h.account_name FROM account_videos v LEFT JOIN hot_accounts h ON h.id=v.account_id WHERE v.id=?` ). get ( videoId );
} else if ( awemeId ) {
video = d . prepare ( `SELECT v.*, h.account_name FROM account_videos v LEFT JOIN hot_accounts h ON h.id=v.account_id WHERE v.aweme_id=?` ). get ( String ( awemeId ));
}
if ( ! video ) throw new Error ( '视频不存在' );
const analysis = d . prepare ( `SELECT content_json AS content, summary, analysis_time FROM account_video_analysis WHERE aweme_id=? OR video_id=? ORDER BY id DESC LIMIT 1` ). get ( String ( video . aweme_id ), video . id ) || null ;
// MCP 解析内容:account_video_source.content_json( content.json 全文)+ analysis_json( analysis.json 全文)
let parse = { found : false , content : null , analysis : null };
const src = d . prepare ( `SELECT content_json, analysis_json FROM account_video_source WHERE video_id=? OR (aweme_id IS NOT NULL AND aweme_id=?) ORDER BY id DESC LIMIT 1` ). get ( video . id , String ( video . aweme_id ));
if ( src && src . content_json ) {
try {
const jd = JSON . parse ( src . content_json );
parse . content = typeof jd . content === 'string' ? jd . content : null ;
parse . found = true ;
if ( src . analysis_json ) {
try { parse . analysis = JSON . parse ( src . analysis_json ); } catch ( e ) {}
}
} catch ( e ) { parse . content = src . content_json ; parse . found = true ; }
}
return { video , analysis , parse };
}
function getVideoScript ( videoId ) {
const d = getDb (); if ( ! d ) throw new Error ( '数据库不可用' );
let source = null ;
const src = d . prepare ( `SELECT content_json FROM account_video_source WHERE video_id=? OR aweme_id=(SELECT aweme_id FROM account_videos WHERE id=?) ORDER BY id DESC LIMIT 1` ). get ( videoId , videoId );
if ( src && src . content_json ) {
try {
const j = JSON . parse ( src . content_json );
const c = String ( j . content || j . script || src . content_json );
const idx = c . indexOf ( '# 视频脚本' );
source = idx >= 0 ? c . slice ( idx ) : c ;
} catch ( e ) { source = src . content_json ; }
}
const rewrites = d . prepare ( `SELECT id, script_text, created_time FROM rewrite_log WHERE video_id=? AND script_text IS NOT NULL AND script_text <> '' ORDER BY id DESC` ). all ( videoId );
return { source , rewrites };
}
/* ================= 改写 / 分镜 / 复盘 ================= */
function listRewrites ( videoId ) {
const d = getDb (); if ( ! d ) return { items : [] };
const rows = videoId > 0
? d . prepare ( `SELECT id, video_id, status, title, created_time, updated_time, length(script_text) AS script_len FROM rewrite_log WHERE video_id=? ORDER BY id DESC` ). all ( videoId )
: d . prepare ( `SELECT id, video_id, status, title, created_time, updated_time, length(script_text) AS script_len FROM rewrite_log ORDER BY id DESC LIMIT 200` ). all ();
const revCols = `id, type, rewrite_id, video_id, dims_json, total, verdict, status, created_time, updated_time` ;
const aiRev = new Map (), cmpRev = new Map (), srcRev = new Map ();
const ids = rows . map (( r ) => r . id );
const vidIds = [... new Set ( rows . map (( r ) => r . video_id ). filter (( v ) => v > 0 ))];
if ( ids . length ) {
for ( const rv of d . prepare ( `SELECT ${ revCols } FROM script_review WHERE type IN ('ai','compare') AND rewrite_id IN ( ${ ids . map (() => '?' ). join ( ',' ) } ) ORDER BY id DESC` ). all (... ids )) {
if ( rv . type === 'ai' ) { if ( ! aiRev . has ( rv . rewrite_id )) aiRev . set ( rv . rewrite_id , rv ); }
else if ( ! cmpRev . has ( rv . rewrite_id )) cmpRev . set ( rv . rewrite_id , rv );
}
}
if ( vidIds . length ) {
for ( const rv of d . prepare ( `SELECT ${ revCols } FROM script_review WHERE type='source' AND video_id IN ( ${ vidIds . map (() => '?' ). join ( ',' ) } ) ORDER BY id DESC` ). all (... vidIds )) {
if ( ! srcRev . has ( rv . video_id )) srcRev . set ( rv . video_id , rv );
}
}
const runningKeys = new Set ();
for ( const rr of d . prepare ( `SELECT type, rewrite_id, video_id, status FROM script_review WHERE status='running'` ). all ()) {
runningKeys . add ( rr . type === 'source' ? 'source:' + rr . video_id : rr . type + ':' + rr . rewrite_id );
}
for ( const it of rows ) {
it . review_ai = aiRev . get ( it . id ) || null ;
it . review_compare = cmpRev . get ( it . id ) || null ;
it . review_source = ( it . video_id && srcRev . get ( it . video_id )) || null ;
it . review_running = runningKeys . has ( 'ai:' + it . id ) || runningKeys . has ( 'compare:' + it . id ) || ( it . video_id && runningKeys . has ( 'source:' + it . video_id ));
}
return { items : rows };
}
function listStoryboards ( videoId ) {
const d = getDb (); if ( ! d ) return [];
return d . prepare ( `SELECT id, video_id, rewrite_id, script_text, status, created_time, updated_time FROM storyboard_log WHERE video_id=? ORDER BY id DESC` ). all ( videoId );
}
function getReviewDetail ( id ) {
const d = getDb (); if ( ! d ) return null ;
return d . prepare ( `SELECT * FROM script_review WHERE id=?` ). get ( id ) || null ;
}
function getReviewFull ( rewriteId ) {
const d = getDb (); if ( ! d ) return null ;
const rw = d . prepare ( `SELECT video_id FROM rewrite_log WHERE id=?` ). get ( rewriteId );
const videoId = rw ? rw . video_id : 0 ;
const source = videoId ? d . prepare ( `SELECT * FROM script_review WHERE type='source' AND video_id=? AND status='done' ORDER BY id DESC LIMIT 1` ). get ( videoId ) : null ;
const ai = d . prepare ( `SELECT * FROM script_review WHERE type='ai' AND rewrite_id=? AND status='done' ORDER BY id DESC LIMIT 1` ). get ( rewriteId );
const compare = d . prepare ( `SELECT * FROM script_review WHERE type='compare' AND rewrite_id=? AND status='done' ORDER BY id DESC LIMIT 1` ). get ( rewriteId );
return { source : source || null , ai : ai || null , compare : compare || null };
}
function getStats () {
const d = getDb (); if ( ! d ) return null ;
const c = ( sql ) => d . prepare ( sql ). get (). c ;
2026-08-31 15:27:54 +08:00
// AI写作复盘 = 已完成的 AI 脚本复盘数(script_review type='ai')
let reviews = 0 ;
try { reviews = c ( `SELECT COUNT(*) c FROM script_review WHERE type='ai' AND status='done'` ); } catch ( e ) { /* 统计失败不影响其他 */ }
// AI写作评分 = 最新 20 条 AI 复盘总分的平均(保留 1 位小数;无评分返回 null)
let writingScore = null ;
try {
const rows = d . prepare ( `SELECT total FROM script_review WHERE type='ai' AND status='done' AND total IS NOT NULL AND total <> '' ORDER BY updated_time DESC, id DESC LIMIT 20` ). all ();
const nums = rows . map (( r ) => Number ( r . total )). filter (( v ) => Number . isFinite ( v ));
if ( nums . length > 0 ) writingScore = Math . round (( nums . reduce (( s , v ) => s + v , 0 ) / nums . length ) * 10 ) / 10 ;
} catch ( e ) { /* 评分统计失败不影响其他统计 */ }
2026-08-27 12:33:57 +08:00
return {
accounts : c ( `SELECT COUNT(*) c FROM hot_accounts WHERE del_flag=0` ),
analyses : c ( `SELECT COUNT(*) c FROM account_video_analysis` ),
rewrites : c ( `SELECT COUNT(*) c FROM rewrite_log WHERE status='done' AND script_text IS NOT NULL AND script_text <> ''` ),
2026-08-31 15:27:54 +08:00
reviews ,
writingScore ,
2026-08-27 12:33:57 +08:00
};
}
// 最近 AI 写剧本(首页快捷卡片):status=done 且有内容,按 id 倒序取前 N 条
function listRecentScripts ( limit = 4 ) {
const d = getDb (); if ( ! d ) return [];
const n = Math . max ( 1 , Math . min ( Number ( limit ) || 4 , 20 ));
return d . prepare ( `
SELECT r.id, r.title, r.script_text, r.created_time, r.video_id, r.account_id,
h.account_name, v.video_title
FROM rewrite_log r
LEFT JOIN hot_accounts h ON h.id = r.account_id
LEFT JOIN account_videos v ON v.id = r.video_id
WHERE r.status='done' AND r.script_text IS NOT NULL AND r.script_text <> ''
ORDER BY r.id DESC LIMIT ?
` ). all ( n );
}
/* ================= 周榜 ================= */
function rankingMeta () {
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 ();
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 };
}
function parseNum ( v ) {
const s = String ( v ?? '' );
const m = s . match ( /^([\d.]+)\s*(w|万)?$/i );
if ( ! m ) return 0 ;
let n = parseFloat ( m [ 1 ]);
if ( m [ 2 ]) n *= 10000 ;
return n ;
}
function getRanking ({ date = '' , category = '' , sortBy = 'rank' , 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 ();
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 selDate = date || dates [ 0 ];
const selFiles = files . filter (( f ) => f . includes ( selDate ) && ( ! category || f . includes ( '_' + 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 ) {}
}
2026-08-27 20:43:39 +08:00
// 默认排序:综合得分倒序,同分按排名升序(用户点表头排序时走下方 sortBy/order 逻辑)
if ( ! order && ( ! sortBy || sortBy === 'rank' )) {
items . sort (( a , b ) => {
const scA = num ( a . comprehensiveScore ), scB = num ( b . comprehensiveScore );
if ( scA !== scB ) return scB - scA ;
return num ( a . rank ) - num ( b . rank );
});
} else {
const orderDir = order === 'asc' ? 'asc' : ( order === 'desc' ? 'desc' : ( sortBy === '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 ]);
return orderDir === 'asc' ? va - vb : vb - va ;
});
}
2026-08-27 12:33:57 +08:00
const total = items . length ;
const limit = Math . max ( 1 , Math . min ( Number ( pageSize ) || 20 , 100 ));
const offset = ( Math . max ( 1 , Number ( page ) || 1 ) - 1 ) * limit ;
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 ) => {
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 );
}
return { ... row , inDb : !! hit , dbId : hit ? hit . id : null };
});
return { dates , categories , category , date : selDate , total , page : Math . max ( 1 , Number ( page ) || 1 ), pageSize : limit , items : pageItems };
}
module . exports = {
2026-09-01 14:31:46 +08:00
dbAvailable , setFavorite , batchDelete , listTracks , listAccounts , listAccountVideos , getPersona , getAccountAnalysis ,
2026-08-27 12:33:57 +08:00
listVideos , getVideoDetail , getVideoScript , listRewrites , listStoryboards , listRecentScripts ,
getReviewDetail , getReviewFull , getStats , rankingMeta , getRanking , DSH_DB , RANKING_DIR ,
};