- sites/spiders: Haidan/HDDolby/RousiPro/SunnyPT/TNode/TorrentLeech/Yema (+mTorrent wrapper), specialized SiteAdapter, indirect download resolved at download time - sites/user-parsers: SiteParserBase + all 20 MP SiteSchema parsers (lxml XPath via xpath+xmldom), refreshSiteUserData with schema fallback - sites/definitions: zod SiteDefinition schemas, 23 MovieClaw v0.27.0 (MIT) definitions via scripts/mp-sync/movieclaw-import.mjs, registry getSiteDefinition/listSiteDefinitions, NOTICE - scripts/mp-sync: ledger.json (upstream sha256), check.mjs, build-ledger.mjs; docs/porting/mp-sync-ledger.md - license-and-provenance: add copied-mit kind, re-anchor MovieClaw rows to v0.27.0 Co-Authored-By: Claude Opus 5.5 <[email protected]>
107 lines
3.5 KiB
JavaScript
107 lines
3.5 KiB
JavaScript
// mp-sync 公共函数(Node 24 / Deno 2 均可运行,无第三方依赖)
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
export const LEDGER_PATH = join(ROOT, "scripts/mp-sync/ledger.json");
|
|
export const MANUAL_PATH = join(ROOT, "scripts/mp-sync/ledger.manual.json");
|
|
export const DOC_PATH = join(ROOT, "docs/porting/mp-sync-ledger.md");
|
|
export const DEFAULT_MP_REPO = "/opt/moviepilot-plus/upstream/MoviePilot";
|
|
/** 需要监视"新增文件"的上游目录(MP 开源站点框架) */
|
|
export const WATCH_DIRS = ["app/modules/indexer/spider", "app/modules/indexer/parser"];
|
|
/** 扫描 @mp-source 标签的本项目目录 */
|
|
export const SCAN_DIRS = ["packages/adapters/src", "packages/testkit/src"];
|
|
|
|
export function parseArgs(argv) {
|
|
const out = { _: [] };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a.startsWith("--")) {
|
|
const k = a.slice(2);
|
|
const v = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true;
|
|
out[k] = v;
|
|
} else out._.push(a);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function git(repo, ...args) {
|
|
return execFileSync("git", ["-C", repo, ...args], {
|
|
encoding: "buffer",
|
|
maxBuffer: 256 * 1024 * 1024,
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
}
|
|
|
|
export function tryGit(repo, ...args) {
|
|
try {
|
|
return git(repo, ...args);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function sha256(buf) {
|
|
return createHash("sha256").update(buf).digest("hex");
|
|
}
|
|
|
|
export function resolveCommit(repo, ref) {
|
|
const out = tryGit(repo, "rev-parse", "--verify", `${ref}^{commit}`);
|
|
if (!out) throw new Error(`上游仓库 ${repo} 中找不到提交 ${ref}(浅克隆需先 git fetch --depth=1 origin <commit>)`);
|
|
return out.toString().trim();
|
|
}
|
|
|
|
/** 上游文件在某提交的内容 sha256;不存在返回 null */
|
|
export function upstreamHash(repo, commit, path) {
|
|
const buf = tryGit(repo, "show", `${commit}:${path}`);
|
|
return buf ? sha256(buf) : null;
|
|
}
|
|
|
|
export function listUpstreamDir(repo, commit, dir) {
|
|
const out = tryGit(repo, "ls-tree", "--name-only", `${commit}:${dir}`);
|
|
if (!out) return [];
|
|
return out
|
|
.toString()
|
|
.split("\n")
|
|
.filter((f) => f.endsWith(".py"))
|
|
.map((f) => `${dir}/${f}`);
|
|
}
|
|
|
|
function walk(dir, acc) {
|
|
for (const name of readdirSync(dir)) {
|
|
if (name === "node_modules") continue;
|
|
const p = join(dir, name);
|
|
if (statSync(p).isDirectory()) walk(p, acc);
|
|
else if (/\.(ts|mjs)$/.test(name)) acc.push(p);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
/** 扫描源码文件头 `@mp-source <path> @ <commit>` 标签 */
|
|
export function scanTags() {
|
|
const tags = [];
|
|
for (const d of SCAN_DIRS) {
|
|
for (const file of walk(join(ROOT, d), [])) {
|
|
const text = readFileSync(file, "utf8");
|
|
for (const m of text.matchAll(/@mp-source\s+(\S+)\s+@\s+([0-9a-f]{7,40})/g)) {
|
|
tags.push({ upstream: m[1], commit: m[2], ours: relative(ROOT, file) });
|
|
}
|
|
}
|
|
}
|
|
return tags;
|
|
}
|
|
|
|
/** 用仓库 biome 格式化(与 lint 一致,生成物重跑幂等);不可用时原样返回 */
|
|
export function biomeFormat(text, fakePath) {
|
|
const bin = join(ROOT, "node_modules/.bin/biome");
|
|
if (!existsSync(bin)) return text;
|
|
try {
|
|
return execFileSync(bin, ["format", `--stdin-file-path=${fakePath}`], { input: text, encoding: "utf8", cwd: ROOT });
|
|
} catch {
|
|
return text;
|
|
}
|
|
}
|