// dsh-plugin-mcn 视频/账号匹配工具(纯函数,拆分自 index.js) import { homedir } from "node:os"; import { join } from "node:path"; import { readFileSync, existsSync } from "node:fs"; function normTitle(s) { return String(s || "").toLowerCase().replace(/[\s#,。!?、,.!?·\-_~~::"“”()()【】]+/g, ""); } function extractTopic(content) { const s = String(content || ""); if (!s.trim()) return ""; const lines = s.replace(/\r\n/g, "\n").split("\n"); let start = -1; for (let i = 0; i < lines.length; i++) { const t = lines[i].trim(); if (/^#{1,4}\s*选题\s*$/.test(t)) { start = i + 1; break; } } if (start < 0) return ""; const out = []; for (let i = start; i < lines.length; i++) { const t = lines[i].trim(); if (/^#{1,6}\s/.test(t) && !/^#{1,6}\s*选题\s*$/.test(t)) break; if (t) out.push(lines[i].trim()); } return out.join("\n").trim(); } function titleDistance(a, b) { const m = a.length, n = b.length; if (!m) return n; if (!n) return m; let prev = new Array(n + 1), curr = new Array(n + 1); for (let j = 0; j <= n; j++) prev[j] = j; for (let i = 1; i <= m; i++) { curr[0] = i; for (let j = 1; j <= n; j++) { curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); } [prev, curr] = [curr, prev]; } return prev[n]; } function findVideoByFolder(d, accountId, vDir, vName) { const rows = d.prepare(`SELECT id, aweme_id, video_title FROM account_videos WHERE account_id=?`).all(accountId); const pick = (title) => { const t = String(title || "").trim(); if (!t) return undefined; const exact = rows.find((r) => String(r.video_title || "") === t); if (exact) return exact; const tn = normTitle(t); if (!tn) return undefined; let best = null; for (const r of rows) { const cn = normTitle(r.video_title); if (!cn || cn.length < 6 || tn.length < 6) continue; if (tn.includes(cn) || cn.includes(tn)) { if (!best || tn.length > best.len) best = { id: r.id, aweme_id: r.aweme_id, len: tn.length }; } } if (best) return best; let bestSim = null; for (const r of rows) { const cn = normTitle(r.video_title); if (!cn) continue; const ratio = 1 - titleDistance(tn, cn) / Math.max(tn.length, cn.length, 1); if (ratio > 0.8 && (!bestSim || ratio > bestSim.ratio)) bestSim = { id: r.id, aweme_id: r.aweme_id, ratio }; } return bestSim ? bestSim : undefined; }; for (const jf of ["原视频解析.json", "content.json"]) { const jp = join(vDir, jf); if (!existsSync(jp)) continue; try { const jd = JSON.parse(readFileSync(jp, "utf8")); const hit = pick(jd.title); if (hit) return hit; } catch (e) { /* 单个 json 解析失败不影响后续匹配 */ } } const first = String(vName || "").split("_")[0].trim(); if (first) { const hit = pick(first); if (hit) return hit; } return undefined; } function resolveAccountDirs(acc) { const out = []; if (acc && typeof acc.source_dir === "string" && acc.source_dir && existsSync(acc.source_dir)) out.push(acc.source_dir); const legacy = acc && acc.account_name ? join(homedir(), "Desktop", "账号对标", acc.account_name) : null; if (legacy && existsSync(legacy)) out.push(legacy); return out; } function resolveAccountDir(acc) { return resolveAccountDirs(acc)[0] || null; } /** 数值化(非法输入返回 0) */ function num(v) { const n = Number(v); return Number.isFinite(n) ? n : 0; } export { num, normTitle, extractTopic, titleDistance, findVideoByFolder, resolveAccountDirs, resolveAccountDir };