Files
torlink/src/sources/subsplease.ts
T
bairon.dev 3302f46762 feat: polish prompt ux and fix z hiding sources without seeder data
- new PromptHints action row shared by the folder and trackers prompts
- download-to prompt shows the torrent name and size, enter reads "download"
- export torrent rebound from T to s and centralized in the store
- footer trimmed to essentials, d/D and z detail live in the ? sheet
- z keeps FitGirl and SubsPlease rows: their feeds carry no seeder data,
  a new per-source reportsHealth flag marks seeders: 0 as unknown, not dead
- version 1.3.0
2026-07-07 01:55:04 -04:00

78 lines
2.3 KiB
TypeScript

import { fetchResilient, HttpError, USER_AGENT } from "../util/net";
import { parseMagnet } from "./magnet";
import type { SearchOptions, Source, TorrentResult } from "./types";
const API = "https://subsplease.org/api/";
const RES_PREFERENCE = ["1080", "720", "480"];
interface SpDownload {
res?: string;
magnet?: string;
}
interface SpEntry {
show?: string;
episode?: string;
release_date?: string;
downloads?: SpDownload[];
}
function pickBest(downloads: SpDownload[]): SpDownload | undefined {
for (const res of RES_PREFERENCE) {
const d = downloads.find((d) => d.res === res && d.magnet);
if (d) return d;
}
return downloads.find((d) => d.magnet);
}
async function search(query: string, opts: SearchOptions = {}): Promise<TorrentResult[]> {
const q = query.trim();
const params = new URLSearchParams({ tz: "UTC" });
if (q) {
params.set("f", "search");
params.set("s", q);
} else {
params.set("f", "latest");
}
const res = await fetchResilient(`${API}?${params.toString()}`, {
headers: { "User-Agent": USER_AGENT },
signal: opts.signal,
});
if (!res.ok) throw new HttpError(res.status, `SubsPlease returned ${res.status}`);
const json = (await res.json()) as Record<string, SpEntry> | unknown[];
if (!json || Array.isArray(json)) return [];
const out: TorrentResult[] = [];
for (const entry of Object.values(json)) {
const dl = pickBest(entry.downloads ?? []);
if (!dl?.magnet) continue;
const parsed = parseMagnet(dl.magnet);
if (!parsed) continue;
const show = entry.show ?? "Unknown";
const ep = entry.episode ? ` - ${entry.episode}` : "";
const sizeMatch = dl.magnet.match(/[?&]xl=(\d+)/);
out.push({
infoHash: parsed.infoHash,
name: `${show}${ep} [${dl.res ?? "?"}p]`,
sizeBytes: sizeMatch ? Number(sizeMatch[1]) : 0,
seeders: 0,
leechers: 0,
source: "subsplease",
magnet: parsed.magnet,
added: entry.release_date ? new Date(entry.release_date).getTime() / 1000 : undefined,
});
}
return out;
}
export const subsplease: Source = {
id: "subsplease",
label: "SubsPlease",
group: "Anime",
homepage: "https://subsplease.org",
// The SubsPlease API has no swarm data; every result reports seeders: 0.
reportsHealth: false,
search,
};