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 ?? "" }; };