import chalk from "chalk"; import { define } from "gunshi"; import pLimit from "p-limit"; import { articleUrl, collectWorksFromListing } from "@/crawlers/fc2"; import { extractMagnets, searchInDb, type SukebeiMagnet } from "@/crawlers/sukebei.db"; import { ExitCodes, wrap } from "../envelope"; import { enterJsonMode, isJsonMode, printHuman, printJson } from "../output"; const wait = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); export interface SukebeiPerWork { id: string; title: string; url: string; status: "found" | "notFound" | "blocked" | "error"; httpCode: number; magnets: SukebeiMagnet[]; } export const sukebeiCommand = define({ name: "sukebei", description: "Ad-hoc: list sukebei magnets for every FC2 work by an author (no DB write)", args: { author: { type: "string", short: "a", required: true, description: "FC2 author slug" }, concurrency: { type: "string", short: "c", default: "3", description: "Max parallel requests" }, delay: { type: "string", short: "d", default: "0", description: "Delay between requests (ms)" }, json: { type: "boolean", description: "Emit machine-readable JSON envelope on stdout" } }, async run(ctx) { const args = ctx.values; enterJsonMode(Boolean(args.json)); const concurrency = Math.max(1, Number.parseInt(args.concurrency, 10) || 3); const delayMs = Math.max(0, Number.parseInt(args.delay, 10) || 0); const listing = await collectWorksFromListing(args.author); if (!isJsonMode()) { printHuman(chalk.green(`FC2 listing: ${listing.length} work(s)`)); } const limit = pLimit(concurrency); const results: SukebeiPerWork[] = []; let done = 0; await Promise.all( listing.map((w) => limit(async () => { if (delayMs > 0) await wait(delayMs); const url = articleUrl(w.id); try { const result = await searchInDb({ id: w.id, title: w.title, url, duration: w.duration ?? "00:00" }); let status: SukebeiPerWork["status"]; if (result.code === 0) status = "error"; else if (result.code !== 200) status = "blocked"; else if (result.data.list.length === 0) status = "notFound"; else status = "found"; results.push({ id: w.id, title: w.title, url, status, httpCode: result.code, magnets: status === "found" ? extractMagnets(result) : [] }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); results.push({ id: w.id, title: w.title, url, status: "error", httpCode: 0, magnets: [] }); if (!isJsonMode()) printHuman(chalk.red(` ${w.id}: ${msg}`)); } finally { done++; if (!isJsonMode() && (done % 10 === 0 || done === listing.length)) { console.info(chalk.dim(` sukebei ${done}/${listing.length}`)); } } }) ) ); const counts = { found: results.filter((r) => r.status === "found").length, notFound: results.filter((r) => r.status === "notFound").length, blocked: results.filter((r) => r.status === "blocked").length, error: results.filter((r) => r.status === "error").length, totalMagnets: results.reduce((acc, r) => acc + r.magnets.length, 0) }; const payload = { author: args.author, counts, results }; if (isJsonMode()) { printJson(wrap("sukebei", payload)); } else { printHuman( `\n${chalk.bold("counts")} found=${chalk.green(counts.found)} notFound=${chalk.yellow(counts.notFound)} blocked=${chalk.red(counts.blocked)} error=${chalk.red(counts.error)} totalMagnets=${chalk.cyan(counts.totalMagnets)}` ); for (const r of results) { if (r.status !== "found") continue; printHuman(`\n${chalk.cyan(r.id)} ${r.title}`); for (const m of r.magnets) { printHuman( ` ${chalk.magenta(m.magnetId.slice(0, 10))} ${m.fileSize} ${m.uploadTime} ${m.title}` ); } } } if (counts.error > 0 && counts.found === 0) process.exit(ExitCodes.NETWORK); } });