107 lines
3.6 KiB
JavaScript
107 lines
3.6 KiB
JavaScript
// 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 };
|