import chalk from "chalk";
import { parseHTML } from "linkedom";
import type { DataInDB, DBSearchResult, FC2Video } from "@/interfaces";
import { request } from "@/utils/request";
export interface SukebeiMagnet {
magnetId: string;
magnetUrl: string;
title: string;
fileSize: string;
uploadTime: string;
}
const BTIH_RE = /xt=urn:btih:([A-Fa-f0-9]{40}|[A-Za-z2-7]{32})/;
/**
* Extract a stable per-magnet identifier from the `xt=urn:btih:` param.
*
* Falls back to hashing the URL itself so we still have a primary key if
* `btih` is missing or malformed (rare — DHT magnets effectively require it).
*/
export const magnetIdFromUrl = (magnetUrl: string): string => {
const m = magnetUrl.match(BTIH_RE);
const hash = m?.[1];
if (hash !== undefined) return hash.toLowerCase();
return `sha-${Bun.hash(magnetUrl).toString(16)}`;
};
/**
* Promote a DBSearchResult row list into the shape sync/show need: stable id,
* de-duplicated by magnetId (the same hash can appear twice across re-uploads).
*/
export const extractMagnets = (result: DBSearchResult): SukebeiMagnet[] => {
const out: SukebeiMagnet[] = [];
const seen = new Set();
for (const row of result.data.list) {
const magnetId = magnetIdFromUrl(row.magnet);
if (seen.has(magnetId)) continue;
seen.add(magnetId);
out.push({
magnetId,
magnetUrl: row.magnet,
title: row.title,
fileSize: row.fileSize,
uploadTime: row.uploadTime
});
}
return out;
};
export const searchInDb = async (video: FC2Video): Promise => {
const { id, url } = video;
const queries = new URLSearchParams([["q", id]]);
try {
const res = await request(`https://sukebei.nyaa.si/?${queries}`);
const data = await res.text();
const { document } = parseHTML(data);
const rows = Array.from(document.querySelectorAll("tbody tr"));
if (rows.length === 0) {
return { code: res.status, data: { count: 0, list: [], url, id } };
}
const list: DataInDB[] = [];
for (const row of rows) {
const parsed = parseRow(row);
if (parsed) list.push(parsed);
}
const skipped = rows.length - list.length;
if (skipped > 0) {
console.warn(chalk.yellow(`sukebei ${id}: skipped ${skipped}/${rows.length} malformed rows`));
}
return { code: res.status, data: { count: list.length, list, url, id } };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.warn(chalk.red(`sukebei ${id}: request failed — ${msg}`));
return { code: 0, data: { count: 0, list: [], url, id } };
}
};
const parseRow = (row: HTMLTableRowElement): DataInDB | null => {
const tds = Array.from(row.querySelectorAll("td"));
if (tds.length < 5) return null;
const title = tds[1]?.querySelector("a")?.innerHTML ?? "";
const anchors = Array.from(tds[2]?.querySelectorAll("a") ?? []);
const magnet =
anchors.find((a) => a.href.startsWith("magnet:"))?.href ?? anchors.at(1)?.href ?? "";
if (!title || !magnet) return null;
return {
title,
magnet,
type: row.className,
fileSize: tds[3]?.innerHTML ?? "",
uploadTime: tds[4]?.innerHTML ?? ""
};
};