feat(sites): 公开 BT 站详情(简介、文件清单、bgm 条目);修 AniBT 资源 ID 带 - 时判非法

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 00:43:25 +08:00
1 parent 06ba0711e2
commit c29dbb4376
12 files changed
+464 -9

No files matched your search

+6
View File
@@ -4,6 +4,12 @@
## [Unreleased]
### 新增
- 公开 BT 站支持详情:Nyaa(简介、文件清单、上传者)、蜜柑(简介去广告、番组页的 bgm 条目、文件清单从种子读)、AniBT(发布说明、关联番剧的 bgm 条目、文件清单从种子读)、AnimeGarden 与萌番组(接口里的简介与文件表)。简介里的 IMDb / 豆瓣 / TMDB / bgm 链接作为识别证据。
### 修复
- AniBT 资源 ID 带 `-` 时被当成非法 ID,详情与下载都会失败。
## [0.3.20] - 2026-10-03
### 调整
@@ -24,6 +24,8 @@ export interface DetailIds {
imdbId?: string;
doubanId?: string;
tmdb?: { type: "movie" | "tv"; id: string };
/** 公开 BT 站简介 / 番组页里的 bgm.tv 条目 */
bangumiId?: string;
}
export async function saveDetailPage(
@@ -42,6 +44,7 @@ export async function saveDetailPage(
...(page.imdbId ? { imdbId: page.imdbId } : {}),
...(page.doubanId ? { doubanId: page.doubanId } : {}),
...(page.tmdb ? { tmdb: page.tmdb } : {}),
...(page.bangumiId ? { bangumiId: page.bangumiId } : {}),
};
await sql`update source_release set raw = raw || jsonb_build_object('_detail', ${JSON.stringify(ids)}::jsonb)
where id = ${srId}`.execute(exec);
@@ -152,6 +155,7 @@ export function siteExplicitIds(
add("imdb", r._site?.imdbId);
add("douban", r._detail?.doubanId);
add("douban", r._site?.doubanId);
add("bangumi", r._detail?.bangumiId);
add("bangumi", r._site?.bangumiId);
if (r._detail?.tmdb) add("tmdb", r._detail.tmdb.id, r._detail.tmdb.type);
return out;
@@ -366,6 +366,13 @@ describe.skipIf(!pg)("ID-3 识别升级链:详情页 ID", () => {
});
await resolveRelease(h.rt, { sourceReleaseId: r2.id, trigger: "manual" });
expect(metaRequests.length).toBe(n);
// 详情里的 bgm 条目(蜜柑番组页 / 简介链接)同样用上
const rd = await release(pub.id, "rel_A9", "[Nix-Raws] Sousou no Frieren S2 - 07 [1080p]", {
_detail: { bangumiId: "515759" },
});
await h.db.updateTable("source_release").set({ detail_fetched_at: new Date() }).where("id", "=", rd.id).execute();
const resd = await resolveRelease(h.rt, { sourceReleaseId: rd.id, trigger: "manual" });
expect(await tmdbIdOf(resd.mediaEntityId!)).toBe("990031");
const r3 = await release(pub.id, "rel_A3", "[SubsPlease] Sousou no Frieren - 03 (1080p)");
const res3 = await resolveRelease(h.rt, { sourceReleaseId: r3.id, trigger: "manual" });
@@ -1,10 +1,10 @@
/**
* 公开 BT 站定义(本项目手写,不来自 MovieClaw):解析器在 ../spiders/public.ts。
* 都不需要登录;能力 = 列表(最新资源,供定时抓取)+ 搜索 + 下载,没有详情页解析。
* 都不需要登录;能力 = 列表(最新资源,供定时抓取)+ 搜索 + 详情(简介、文件清单)+ 下载。
*/
import { NYAA_CATEGORY_NAMES } from "../spiders/public.ts";
const caps = { list: true, search: true, detail: false, download: true, rss: false };
const caps = { list: true, search: true, detail: true, download: true, rss: false };
const polite = { minIntervalMs: 2000, burst: 2 };
export const PUBLIC_SITE_DEFINITION_JSON: Record<string, unknown> = {
@@ -51,7 +51,7 @@ const BR = "\u0000";
* 保留换行的文本:<br> 与块级元素换行。源码换行不算换行(观众的简介是 `<br />\r\n`,
* 两者都算会让每行之间多一空行);<pre> 里的换行保留。
*/
function blockText($: CheerioAPI, el: El): string {
export function blockText($: CheerioAPI, el: El): string {
const c = el.clone();
c.find("script, style").remove();
c.find("pre, textarea").each((_, n) => {
@@ -22,6 +22,7 @@ import {
type SitePage,
} from "../types.ts";
import { specializedTorrentToRelease } from "./map.ts";
import { specializedDetail } from "./public-detail.ts";
import type { SpecializedSiteDefinition, SpecializedSpider, SpiderContext } from "./types.ts";
export function createSpecializedSiteAdapter(
@@ -103,8 +104,9 @@ export function createSpecializedSiteAdapter(
search(query, filters = {}, opts = {}) {
return doSearch(filters.page ?? 1, filters, query.trim() || null, opts.signal);
},
async detail() {
throw new SiteError(def.id, "unsupported", `${def.name}:专用 API spider 不提供详情接口`);
async detail(siteReleaseId, opts = {}) {
if (!spider.detail) throw new SiteError(def.id, "unsupported", `${def.name}:专用 API spider 不提供详情接口`);
return specializedDetail(ctx(opts.signal), spider, def, siteReleaseId, enclosures.get(String(siteReleaseId)));
},
async downloadTarget(siteReleaseId, opts = {}): Promise<DownloadTarget> {
return spider.resolveDownload(ctx(opts.signal), siteReleaseId, enclosures.get(String(siteReleaseId)));
@@ -0,0 +1,69 @@
/**
* 公开 BT 站详情(本项目扩展,MP 无对应):spider.detail 给出简介 HTML / 文本与文件清单 → SiteReleaseDetail。
* 简介里的 IMDb / 豆瓣 / TMDB / bgm.tv 链接取作识别证据;页面没有文件清单的站(蜜柑、AniBT)下载种子读文件列表。
*/
import { load } from "cheerio";
import { parseTorrent } from "../../qbittorrent/torrent.ts";
import { emptyDetailPage, extractTmdb, MAX_FILES, MAX_SCREENSHOTS, screenshotUrl } from "../detail-text.ts";
import { blockText, imdbFromText } from "../spider/detail-page.ts";
import { SiteError, type SiteReleaseDetail } from "../types.ts";
import { extractDoubanId } from "../util.ts";
import { siteBase } from "./common.ts";
import { specializedTorrentToRelease } from "./map.ts";
import type { SpecializedSiteDefinition, SpecializedSpider, SpiderContext } from "./types.ts";
const BGM = /(?:bgm\.tv|bangumi\.tv|chii\.in)\/subject\/(\d{1,8})\b/i;
export async function specializedDetail(
ctx: SpiderContext,
spider: SpecializedSpider,
def: SpecializedSiteDefinition,
siteReleaseId: string,
enclosure: string | undefined,
): Promise<SiteReleaseDetail> {
if (!spider.detail) throw new SiteError(def.id, "unsupported", `${def.name}:不提供详情`);
const d = await spider.detail(ctx, siteReleaseId, enclosure);
const r = specializedTorrentToRelease(
{ site_release_id: siteReleaseId, ...d.torrent },
def,
ctx.http.now().toISOString(),
);
if (!r) throw new SiteError(def.id, "parse_error", `${def.name} 详情缺少标题`);
const page = emptyDetailPage();
const base = siteBase(def);
let text = d.text?.trim() || null;
if (d.html) {
const $ = load(d.html);
const imgs: string[] = [];
$("img").each((_, n) => {
const u = screenshotUrl($(n).attr("data-src") ?? $(n).attr("src"), base);
if (u && !imgs.includes(u)) imgs.push(u);
});
page.screenshots = imgs.slice(0, MAX_SCREENSHOTS);
page.probe.imageCount = imgs.length;
$("img, script, style").remove();
text = blockText($, $("body")) || text;
}
page.descriptionText = text ? text.slice(0, 20_000) : null;
page.probe.hasDescription = !!page.descriptionText;
const src = `${d.html ?? ""}\n${d.text ?? ""}`;
page.imdbId = imdbFromText(src);
page.doubanId = extractDoubanId(src) ?? null;
page.tmdb = extractTmdb(src);
page.bangumiId = BGM.exec(src)?.[1] ?? d.torrent.bangumi_id ?? null;
page.mediaInfo = d.mediaInfo ?? null;
page.uploader = d.uploader ?? null;
page.tags = d.tags ?? [];
let files = d.files ?? [];
if (!files.length && d.filesFromTorrent) {
try {
const t = await spider.resolveDownload(ctx, siteReleaseId, enclosure);
if (t.kind === "torrent")
files = parseTorrent(t.bytes).files.map((f) => ({ name: f.path, sizeBytes: f.length || null }));
} catch {
// 种子取不到只是没有文件清单,不影响详情
}
}
page.files = files.slice(0, MAX_FILES);
return { ...r, ...(page.descriptionText ? { descriptionText: page.descriptionText } : {}), page };
}
@@ -1,11 +1,16 @@
import {
ANIBT_RELEASE_PAGE,
ANIBT_RSS_LATEST,
ANIBT_TORZNAB_SEARCH,
ANIMEGARDEN_DETAIL,
ANIMEGARDEN_RESOURCES,
BANGUMIMOE_FETCH,
BANGUMIMOE_LATEST,
createMockFetch,
MIKAN_EPISODE_PAGE,
MIKAN_RSS_SEARCH,
NYAA_RSS_SEARCH,
NYAA_VIEW_PAGE,
} from "@umt/testkit";
import { describe, expect, it } from "vitest";
import { BUILTIN_SITE_DEFINITIONS } from "../definitions/index.ts";
@@ -101,4 +106,61 @@ describe("公开 BT 站(本项目扩展)", () => {
it("五个内置公开站都是 public 定义", () => {
for (const id of ["nyaa", "mikan", "anibt", "animegarden", "bangumimoe"]) expect(def(id).public).toBe(true);
});
it("详情 Nyaa:Markdown 简介、文件清单(去掉大小括号)、上传者、info hash", async () => {
const m = createMockFetch({ "GET /view/2160092": { body: NYAA_VIEW_PAGE } });
const d = await adapterWith(def("nyaa"), nyaaSpider, m.fetch).detail("2160092");
expect(d.title).toContain("Sousou no Frieren 2nd Season");
expect(d.infoHash).toBe("ef3e7ad1b12bdd9fc341691d8866cd1fa8374a4b");
expect(d.page!.descriptionText).toBeNull();
expect(d.page!.uploader).toBe("VINT64");
expect(d.page!.files.length).toBeGreaterThan(5);
expect(d.page!.files[0]!.name).toMatch(/Sousou no Frieren 2nd Season - 01 .*\.mkv$/);
expect(d.page!.files[0]!.sizeBytes).toBe(Math.round(733.5 * 1024 ** 2));
});
it("详情 蜜柑:去掉广告位;番组页取 bgm 条目;文件清单从种子读", async () => {
const hash = "f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19";
// 剧集页里的下载链接(种子从这里取)
const page = MIKAN_EPISODE_PAGE.replace(
"</body>",
`<a href="/Download/20260828/${hash}.torrent">下载种子</a></body>`,
);
const m = createMockFetch({
[`GET /Home/Episode/${hash}`]: { body: page },
"GET /Home/Bangumi/3141": { body: '<a href="https://bgm.tv/subject/400602">Bangumi番组计划链接</a>' },
});
const t = withTorrent(m.fetch, (u) => u.pathname.startsWith("/Download/"), "Frieren.S01E01.mkv");
const d = await adapterWith(def("mikan"), mikanSpider, t.fetch).detail(hash);
expect(d.title).toContain("葬送的芙莉莲");
expect(d.page!.descriptionText).toContain("Content Info");
expect(d.page!.descriptionText).not.toMatch(/tmall/);
expect(d.page!.bangumiId).toBe("400602");
expect(d.page!.uploader).toBe("7³ACG");
expect(d.uploadedAt).toBe("2026-08-28T15:15:00.000Z");
expect(d.page!.files.map((f) => f.name)).toEqual(["Frieren.S01E01.mkv"]);
});
it("详情 AniBT:发布说明、关联番剧的 bgm 条目", async () => {
const m = createMockFetch({ "GET /release/rel_U6lR5dHzDTZS": { body: ANIBT_RELEASE_PAGE } });
const d = await adapterWith(def("anibt"), anibtSpider, m.fetch).detail("rel_U6lR5dHzDTZS");
expect(d.title).toContain("葬送的芙莉莲");
expect(d.page!.descriptionText).toContain("Content Info");
expect(d.page!.descriptionText).not.toContain("查看完整图片");
expect(d.page!.bangumiId).toBe("400602");
});
it("详情 AnimeGarden / 萌番组:接口给简介 HTML 与文件表", async () => {
const g = createMockFetch({ "GET https://api.animes.garden/detail/dmhy/725809": { body: ANIMEGARDEN_DETAIL } });
const gd = await adapterWith(def("animegarden"), animeGardenSpider, g.fetch).detail("dmhy-725809");
expect(gd.page!.files).toHaveLength(3);
expect(gd.page!.files[0]!.sizeBytes).toBe(Math.round(13.9 * 1024 ** 2));
expect(gd.page!.descriptionText).toBeTruthy();
const id = BANGUMIMOE_FETCH._id;
const b = createMockFetch({ "POST /api/torrent/fetch": { body: BANGUMIMOE_FETCH } });
const bd = await adapterWith(def("bangumimoe"), bangumiMoeSpider, b.fetch).detail(id);
expect(JSON.parse(b.requests[0]!.body!)).toEqual({ _id: id });
expect(bd.page!.files[0]!.name).toMatch(/\.mkv$/);
expect(bd.page!.bangumiId).toBe("613438");
});
});
+183 -4
View File
@@ -7,11 +7,18 @@
* - 萌番组 bangumi.moe:JSON API;部分网络直连不通,可在站点上开「通过浏览器请求」
* 都不需要登录;只读请求走站点限流。资源 ID 设计成「只凭 ID 就能取到种子」(下载时不依赖搜索缓存)。
*/
import { load } from "cheerio";
import { XMLParser } from "fast-xml-parser";
import { parseSizeMp } from "../spider/spider.ts";
import { SiteError } from "../types.ts";
import { apiRequest, downloadTorrentFile, isPlainObject, siteBase, textRequest } from "./common.ts";
import type { SpecializedSpider, SpecializedTorrent, SpiderContext, SpiderSearchQuery } from "./types.ts";
import type {
SpecializedDetail,
SpecializedSpider,
SpecializedTorrent,
SpiderContext,
SpiderSearchQuery,
} from "./types.ts";
const xml = new XMLParser({
ignoreAttributes: false,
@@ -84,6 +91,7 @@ const NYAA_CATEGORY: Record<string, string> = {
export const nyaaSpider: SpecializedSpider = {
parser: "Nyaa",
detail: nyaaDetail,
credential: "cookie",
pageSize: () => null,
async search(ctx, q) {
@@ -131,6 +139,7 @@ export const NYAA_CATEGORY_NAMES = NYAA_CATEGORY;
export const mikanSpider: SpecializedSpider = {
parser: "Mikan",
detail: mikanDetail,
credential: "cookie",
pageSize: () => null,
async search(ctx, q) {
@@ -179,6 +188,7 @@ export const mikanSpider: SpecializedSpider = {
export const anibtSpider: SpecializedSpider = {
parser: "AniBT",
detail: anibtDetail,
credential: "cookie",
pageSize: () => null,
async search(ctx, q) {
@@ -190,7 +200,7 @@ export const anibtSpider: SpecializedSpider = {
return items.flatMap((it): SpecializedTorrent[] => {
const id = text(it.guid);
const title = text(it.title);
if (!id || !/^o?rel_/.test(id) || !title) return [];
if (!id || !/^o?rel_[A-Za-z0-9_-]+$/.test(id) || !title) return [];
const attrs = Object.fromEntries(
((it["torznab:attr"] as Node[] | undefined) ?? []).map((a) => [text(a["@_name"]), text(a["@_value"])]),
);
@@ -215,7 +225,7 @@ export const anibtSpider: SpecializedSpider = {
return (await rssItems(ctx, `${base}rss/magnets.xml`)).flatMap((it): SpecializedTorrent[] => {
const id = text(it.guid) ?? text(it["anibt:releaseId"]);
const title = text(it.title);
if (!id || !/^o?rel_/.test(id) || !title) return [];
if (!id || !/^o?rel_[A-Za-z0-9_-]+$/.test(id) || !title) return [];
const tor = (it.torrent ?? {}) as Node;
const labels = [
text(it["anibt:groupName"]),
@@ -243,7 +253,7 @@ export const anibtSpider: SpecializedSpider = {
});
},
async resolveDownload(ctx, id) {
const rel = /^o?rel_[A-Za-z0-9]+$/.exec(String(id).trim())?.[0];
const rel = /^o?rel_[A-Za-z0-9_-]+$/.exec(String(id).trim())?.[0];
if (!rel) throw new SiteError(ctx.def.id, "not_found", "资源 ID 非法");
return downloadTorrentFile(ctx, `${siteBase(ctx.def)}api/torrent/${rel}.torrent`);
},
@@ -278,6 +288,7 @@ function gardenTorrent(ctx: SpiderContext, r: Node): SpecializedTorrent | null {
export const animeGardenSpider: SpecializedSpider = {
parser: "AnimeGarden",
detail: gardenDetail,
credential: "cookie",
pageSize: () => null,
async search(ctx, q) {
@@ -331,6 +342,7 @@ function moeTorrent(ctx: SpiderContext, r: Node): SpecializedTorrent | null {
export const bangumiMoeSpider: SpecializedSpider = {
parser: "BangumiMoe",
detail: moeDetail,
credential: "cookie",
pageSize: () => null,
async search(ctx, q) {
@@ -354,3 +366,170 @@ export const bangumiMoeSpider: SpecializedSpider = {
return downloadTorrentFile(ctx, `${siteBase(ctx.def)}download/torrent/${oid}/${oid}.torrent`);
},
};
// ── 详情(spider.detail,转成页面内容见 public-detail.ts) ──
const clean = (s: string | undefined) => (s ?? "").replace(/\s+/g, " ").trim();
const BGM_LINK = /bgm\.tv\/subject\/(\d{1,8})/;
async function nyaaDetail(ctx: SpiderContext, id: string): Promise<SpecializedDetail> {
const n = /^\d+$/.exec(String(id).trim())?.[0];
if (!n) throw new SiteError(ctx.def.id, "not_found", "资源 ID 非法");
const base = siteBase(ctx.def);
const $ = load(await textRequest(ctx, `${base}view/${n}`, {}));
const title = clean($("h3.panel-title").first().text());
if (!title) throw new SiteError(ctx.def.id, "not_found", "资源不存在");
// 信息表:「Seeders:」标签格的下一格是值
const row = (label: string) =>
$(".panel-body .row > div")
.filter((_, e) => clean($(e).text()) === label)
.first()
.next();
const files = $(".torrent-file-list li")
.filter((_, e) => $(e).children("i.fa-file").length > 0)
.map((_, e) => {
const el = $(e).clone();
const size = clean(el.find(".file-size").text()).replace(/[()]/g, "");
el.find(".file-size").remove();
return { name: clean(el.text()), sizeBytes: size ? parseSizeMp(size) || null : null };
})
.get()
.filter((f) => f.name);
const ts = Number(row("Date:").attr("data-timestamp"));
const desc = $("#torrent-description").first().text().trim();
return {
torrent: {
title,
page_url: `${base}view/${n}`,
enclosure: `${base}download/${n}.torrent`,
size: parseSizeMp(clean(row("File size:").text())),
seeders: num(clean(row("Seeders:").text())) ?? 0,
peers: num(clean(row("Leechers:").text())) ?? 0,
grabs: num(clean(row("Completed:").text())) ?? 0,
...(ts ? { pubdate_utc: new Date(ts * 1000).toISOString() } : {}),
infohash: clean($("kbd").first().text()) || undefined,
downloadvolumefactor: 0,
uploadvolumefactor: 1,
labels: [],
} as SpecializedTorrent,
// 简介是 Markdown 原文;「No description.」当没有
text: /^#*\s*No description\.?$/i.test(desc) ? null : desc,
files,
uploader: clean(row("Submitter:").text()) || null,
};
}
async function mikanDetail(ctx: SpiderContext, id: string): Promise<SpecializedDetail> {
const hash = /[0-9a-f]{40}/i.exec(String(id))?.[0]?.toLowerCase();
if (!hash) throw new SiteError(ctx.def.id, "not_found", "资源 ID 非法");
const base = siteBase(ctx.def);
const $ = load(await textRequest(ctx, `${base}Home/Episode/${hash}`, {}));
const title = clean($("p.episode-title").first().text());
if (!title) throw new SiteError(ctx.def.id, "not_found", "资源不存在");
const info = (label: string) =>
clean(
$("p.bangumi-info")
.filter((_, e) => $(e).text().includes(label))
.first()
.text(),
).replace(new RegExp(`^.*?${label}[::]\\s*`), "");
// 简介开头是站点广告位(带 ga 点击统计的天猫链接),去掉
const desc = $(".episode-desc").first().clone();
desc
.children("div")
.filter((_, e) => $(e).find('a[onclick*="clicked"]').length > 0)
.remove();
const date = /(\d{4})\/(\d{2})\/(\d{2}) (\d{2}:\d{2})/.exec(info("发布日期"));
// 番组页上有 bgm.tv 条目链接(识别用);取不到不影响详情
let bgm: string | undefined;
const bangumiPath = /\/Home\/Bangumi\/\d+/.exec($("p.bangumi-title a").first().attr("href") ?? "")?.[0];
if (bangumiPath) {
bgm = await textRequest(ctx, new URL(bangumiPath, base).href, {})
.then((h) => BGM_LINK.exec(h)?.[1])
.catch(() => undefined);
}
return {
torrent: {
title,
page_url: `${base}Home/Episode/${hash}`,
size: parseSizeMp(info("文件大小")),
...(date ? { pubdate_utc: utcCst(`${date[1]}-${date[2]}-${date[3]}T${date[4]}:00`) } : {}),
infohash: hash,
downloadvolumefactor: 0,
uploadvolumefactor: 1,
labels: [],
...(bgm ? { bangumi_id: bgm } : {}),
} as SpecializedTorrent,
html: desc.html(),
filesFromTorrent: true,
uploader: info("字幕组") || null,
};
}
async function anibtDetail(ctx: SpiderContext, id: string): Promise<SpecializedDetail> {
const rel = /^o?rel_[A-Za-z0-9_-]+$/.exec(String(id).trim())?.[0];
if (!rel) throw new SiteError(ctx.def.id, "not_found", "资源 ID 非法");
const base = siteBase(ctx.def);
const page = await textRequest(ctx, `${base}release/${rel}`, {});
const $ = load(page);
const title = clean($("h1").first().text());
if (!title) throw new SiteError(ctx.def.id, "not_found", "资源不存在");
// 「发布说明」一节;文件列表与 MediaInfo 由前端异步加载,文件从种子里读
const desc = $("section h2")
.filter((_, e) => clean($(e).text()) === "发布说明")
.first()
.closest("section")
.find(".prose")
.first()
.clone();
desc.find("[aria-hidden], svg").remove();
const bgm = BGM_LINK.exec(page)?.[1];
return {
torrent: {
title,
page_url: `${base}release/${rel}`,
enclosure: `${base}api/torrent/${rel}.torrent`,
downloadvolumefactor: 0,
uploadvolumefactor: 1,
labels: [],
...(bgm ? { bangumi_id: bgm } : {}),
} as SpecializedTorrent,
html: desc.length ? desc.html() : null,
filesFromTorrent: true,
};
}
async function gardenDetail(ctx: SpiderContext, id: string): Promise<SpecializedDetail> {
const m = /^([a-z]+)-([A-Za-z0-9_-]+)$/.exec(String(id).trim());
if (!m) throw new SiteError(ctx.def.id, "not_found", "资源 ID 非法");
const res = await apiRequest(ctx, `${gardenApi(ctx)}/detail/${m[1]}/${m[2]}`);
const r = isPlainObject(res) && isPlainObject(res.resource) ? gardenTorrent(ctx, res.resource) : null;
if (!r) throw new SiteError(ctx.def.id, "not_found", "资源不存在");
const d = isPlainObject(res) && isPlainObject(res.detail) ? res.detail : {};
const files = (Array.isArray(d.files) ? (d.files as Node[]) : []).flatMap((f) => {
const name = text(f.name);
const size = text(f.size);
return name ? [{ name, sizeBytes: size ? parseSizeMp(size) || null : null }] : [];
});
return {
torrent: r,
html: text(d.description) ?? null,
files,
...(r.uploader ? { uploader: r.uploader } : {}),
};
}
async function moeDetail(ctx: SpiderContext, id: string): Promise<SpecializedDetail> {
const oid = /^[0-9a-f]{24}$/i.exec(String(id).trim())?.[0];
if (!oid) throw new SiteError(ctx.def.id, "not_found", "资源 ID 非法");
const res = await apiRequest(ctx, `${siteBase(ctx.def)}api/torrent/fetch`, { json: { _id: oid } });
const r = isPlainObject(res) ? moeTorrent(ctx, res) : null;
if (!r || !isPlainObject(res)) throw new SiteError(ctx.def.id, "not_found", "资源不存在");
const files = (Array.isArray(res.content) ? (res.content as unknown[]) : []).flatMap((row) => {
if (!Array.isArray(row)) return [];
const name = text(row[0]);
const size = text(row[1]);
return name ? [{ name, sizeBytes: size ? parseSizeMp(size) || null : null }] : [];
});
return { torrent: r, html: text(res.introduction) ?? null, files };
}
@@ -107,4 +107,21 @@ export interface SpecializedSpider {
* `enclosure` 为最近一次搜索结果里的直链(仅内存,不落库);缺失时按站点规则构造。
*/
resolveDownload(ctx: SpiderContext, siteReleaseId: string, enclosure?: string): Promise<DownloadTarget>;
/** 详情(本项目扩展,公开 BT 站):简介 HTML / 文本、文件清单;没给文件清单且 filesFromTorrent 时从种子里读 */
detail?(ctx: SpiderContext, siteReleaseId: string, enclosure?: string): Promise<SpecializedDetail>;
}
export interface SpecializedDetail {
/** 详情上的资源字段(标题必填;其余缺的沿用列表快照) */
torrent: SpecializedTorrent;
/** 简介 HTML(取文本、图片、外部链接) */
html?: string | null;
/** 简介纯文本 / Markdown(没有 HTML 时用) */
text?: string | null;
files?: import("../types.ts").SiteDetailFile[];
/** 页面上没有文件清单:下载种子读文件列表 */
filesFromTorrent?: boolean;
mediaInfo?: string | null;
uploader?: string | null;
tags?: string[];
}
+2
View File
@@ -80,6 +80,8 @@ export interface SiteDetailPage {
imdbId: string | null;
doubanId: string | null;
tmdb: { type: "movie" | "tv"; id: string } | null;
/** 简介里的 bgm.tv 条目 ID(公开 BT 站的番剧资源常带) */
bangumiId?: string | null;
files: SiteDetailFile[];
tags: string[];
uploader: string | null;
@@ -72,3 +72,110 @@ export const BANGUMIMOE_LATEST = {
},
],
};
/**
* 公开 BT 站详情:2026-10-03 取自 Nyaa 详情页、蜜柑剧集页、AniBT 资源页、AnimeGarden 详情接口、萌番组 torrent/fetch(公开数据)。
* 页面截取用到的片段,AniBT 去掉了样式属性;接口截短了文件列表与简介。
*/
export const NYAA_VIEW_PAGE =
'<!DOCTYPE html><html><head><title>t</title></head><body><div class="panel panel-default">\n\t<div class="panel-heading">\n\t\t<h3 class="panel-title">\n\t\t\t[Erai-raws] Sousou no Frieren 2nd Season [1080p CR WEBRip HEVC AAC][MultiSub] (unofficial batch)\n\t\t</h3>\n\t</div>\n\t<div class="panel-body">\n\t\t<div class="row">\n\t\t\t<div class="col-md-1">Category:</div>\n\t\t\t<div class="col-md-5">\n\t\t\t\t<a href="/?c=1_0">Anime</a> - <a href="/?c=1_2">English-translated</a>\n\t\t\t</div>\n\t\t\t<div class="col-md-1">Date:</div>\n\t\t\t<div class="col-md-5" data-timestamp="1789227900">2026-09-12 15:45 UTC</div>\n\t\t</div>\n\t\t<div class="row">\n\t\t\t<div class="col-md-1">Submitter:</div>\n\t\t\t<div class="col-md-5">\n<a class="text-default" href="/user/VINT64" data-toggle="tooltip" title="User">VINT64</a>\t\t\t</div>\n\t\t\t<div class="col-md-1">Seeders:</div>\n\t\t\t<div class="col-md-5"><span style="color: green;">32</span></div>\n\t\t</div>\n\t\t<div class="row">\n\t\t\t<div class="col-md-1">Information:</div>\n\t\t\t<div class="col-md-5">\n\t\t\t\t<a rel="noopener noreferrer nofollow" href="https://www.erai-raws.info/anime-list/sousou-no-frieren-2nd-season/">https://www.erai-raws.info/anime-list/sousou-no-frieren-2nd-season/</a>\n\t\t\t</div>\n\t\t\t<div class="col-md-1">Leechers:</div>\n\t\t\t<div class="col-md-5"><span style="color: red;">5</span></div>\n\t\t</div>\n\t\t<div class="row">\n\t\t\t<div class="col-md-1">File size:</div>\n\t\t\t<div class="col-md-5">6.6 GiB</div>\n\t\t\t<div class="col-md-1">Completed:</div>\n\t\t\t<div class="col-md-5">837</div>\n\t\t</div>\n\t\t<div class="row">\n\t\t\t<div class="col-md-offset-6 col-md-1">Info hash:</div>\n\t\t\t<div class="col-md-5"><kbd>ef3e7ad1b12bdd9fc341691d8866cd1fa8374a4b</kbd></div>\n\t\t</div>\n\t</div><!--/.panel-body -->\n\t<div class="panel-footer clearfix">\n<a href="/download/2160092.torrent"><i class="fa fa-download fa-fw"></i>Download Torrent</a> or <a href="magnet:?xt=urn:btih:ef3e7ad1b12bdd9fc341691d8866cd1fa8374a4b&amp;dn=%5BErai-raws%5D%20Sousou%20no%20Frieren%202nd%20Season%20%5B1080p%20CR%20WEBRip%20HEVC%20AAC%5D%5BMultiSub%5D%20%28unofficial%20batch%29&amp;tr=http%3A%2F%2Fnyaa.tracker.wf%3A7777%2Fannounce&amp;tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&amp;tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&amp;tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce&amp;tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce" class="card-footer-item"><i class="fa fa-magnet fa-fw"></i>Magnet</a>\n\t</div>\n</div><!--/.panel -->\n<div class="panel panel-default">\n\t<div markdown-text class="panel-body" id="torrent-description">#### No description.</div>\n</div>\n<div class="panel panel-default">\n\t<div class="panel-heading">\n\t\t<h3 class="panel-title">File list</h3>\n\t</div>\n\t<div class="torrent-file-list panel-body">\n\t\t<ul>\n\t\t\t<li>\n\t\t\t\t<a href="" class="folder"><i class="fa fa-folder-open"></i>[Erai-raws] Sousou no Frieren 2nd Season [1080p CR WEBRip HEVC AAC][MultiSub]</a>\n\t\t\t\t<ul data-show="yes">\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 01 [1080p CR WEBRip HEVC AAC][MultiSub][DF313249].mkv <span class="file-size">(733.5 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 02 [1080p CR WEBRip HEVC AAC][MultiSub][7DC8365B].mkv <span class="file-size">(649.7 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 03 [1080p CR WEBRip HEVC AAC][MultiSub][4CBD8375].mkv <span class="file-size">(691.2 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 04 [1080p CR WEBRip HEVC AAC][MultiSub][4AC464D8].mkv <span class="file-size">(784.1 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 05 [1080p CR WEBRip HEVC AAC][MultiSub][D3077C54].mkv <span class="file-size">(741.0 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 06 [1080p CR WEBRip HEVC AAC][MultiSub][6E78A397].mkv <span class="file-size">(554.1 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 07 [1080p CR WEBRip HEVC AAC][MultiSub][A3F98EE4].mkv <span class="file-size">(585.1 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 08 [1080p CR WEBRip HEVC AAC][MultiSub][0D217EED].mkv <span class="file-size">(660.5 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 09 [1080p CR WEBRip HEVC AAC][MultiSub][D9A6C94A].mkv <span class="file-size">(726.2 MiB)</span></li>\n\t\t\t<li><i class="fa fa-file"></i>[Erai-raws] Sousou no Frieren 2nd Season - 10 [1080p CR WEBRip HEVC AAC][MultiSub][8DF1FC34].mkv <span class="file-size">(661.9 MiB)</span></li>\n\t\t\t\t</ul>\n\t\t\t</li>\n\t\t</ul>\n\t</div></div></body></html>';
export const MIKAN_EPISODE_PAGE =
'<!DOCTYPE html><html><head><title>t</title></head><body><p class="bangumi-title"><a target="_blank" class="w-other-c" style="color:#555" href="/Home/Bangumi/3141#1254">&#x846C;&#x9001;&#x7684;&#x8299;&#x8389;&#x83B2;</a> <a href="/RSS/Bangumi?bangumiId=3141&subgroupid=1254" class="mikan-rss" data-placement="bottom" data-toggle="tooltip" data-original-title="RSS" target="_blank"><i class="fa fa-rss-square"></i></a></p>\n\n <p class="bangumi-info">\n 字幕组:<a class="magnet-link-wrap" href="/Home/PublishGroup/1025" target="_blank"> 7&#xB3;ACG</a>\n </p>\n\n <p class="bangumi-info">发布日期:2026/08/28 23:15</p>\n\n <p class="bangumi-info">文件大小:48.5 GB</p>\n\n <div class="central-container" style="min-height: 532px;">\n <div class="episode-header" style="">\n <p class="episode-title">[7&#xB3;ACG] &#x846C;&#x9001;&#x7684;&#x8299;&#x8389;&#x83B2;/Sousou no Frieren S01 | 01-28&#x2B;SPx11 [&#x7B80;&#x7E41;&#x5B57;&#x5E55;] BDrip 1080p x265 OPUS 2.0 [48.5 GB]</p>\n </div>\n <br />\n <div style="padding-bottom:20px" class="episode-desc">\n <div style="margin-top: -10px; margin-bottom: 10px;">\n \n\n<div style="width:100%; margin-right: auto; margin-left: auto;" class="hidden-xs hidden-sm">\n <a href="https://equity.tmall.com/tm?agentId=030f9003a26d4361&amp;bc_fl_src=tmall_market_llb_1_2698459&amp;llbPlatform=_pube&amp;llbOsd=1&amp;di=__IDFA__&amp;dim=__IMEI__&amp;oaid=__OAID__&amp;caid=__CAID__&amp;sy=llb" onclick="ga(\'send\', \'event\', \'sswj_lg\', \'clicked\', \'ad\');">\n <img src="/images/SSWJ/sswj7_lg.jpg" style=\'height: 100%; width: 100%; object-fit: contain\' />\n </a>\n</div>\n\n<div style="width:100%; margin-right: auto; margin-left: auto;" class="hidden-lg hidden-md">\n <a href="https://equity.tmall.com/tm?agentId=030f9003a26d4361&amp;bc_fl_src=tmall_market_llb_1_2698459&amp;llbPlatform=_pube&amp;llbOsd=1&amp;di=__IDFA__&amp;dim=__IMEI__&amp;oaid=__OAID__&amp;caid=__CAID__&amp;sy=llb" onclick="ga(\'send\', \'event\', \'sswj_sm\', \'clicked\', \'ad\');">\n <img src="/images/SSWJ/sswj7_sm.jpg" style=\'height: 100%; width: 100%; object-fit: contain\' />\n </a>\n</div>\n </div>\n\n<a href="https://ktnbytes.com/2026/08/27/260827-sousou-no-frieren-s01-x265/kv.jpg" target="_blank"><img src="https://ktnbytes.com/2026/08/27/260827-sousou-no-frieren-s01-x265/kv.jpg" /></a><br/><br/>post via api service<br/><br/><font size="2"><strong>Content Info</strong></font><br/><br/>SUBS : Nest方案(Haruhana、Nekomoe-kissaten、KitaujiSub、Sakurato、Netflix、Cantonese)<br/>SIZE : 48.451 GiB<br/><br/><font size="2"><strong>Team Info</strong></font><br/><br/><a href="https://ktnbytes.com/" target="_blank">主發布站 : ktnbytes.com</a><br/><a href="https://t.me/ktnbytes" target="_blank">RSS 頻道 : t.me/ktnbytes</a><br/><a href="https://t.me/+lOZ13tUZHo8zZWE1" target="_blank">TG 交流群 : t.me/+lOZ13tUZHo8zZWE1</a> </div>\n </div>\n\n </div></body></html>';
export const ANIBT_RELEASE_PAGE =
'<!DOCTYPE html><html><head><title>t</title></head><body><h1 class="truncate-anywhere line-clamp-4 font-sans text-[18px] leading-[1.4] font-bold text-ink lg:line-clamp-2 lg:text-[24px]" title="[7³ACG] 葬送的芙莉莲/Sousou no Frieren S01 | 01-28+SPx11 [简繁字幕] BDrip 1080p x265 OPUS 2.0">[7³ACG] 葬送的芙莉莲/Sousou no Frieren S01 | 01-28+SPx11 [简繁字幕] BDrip 1080p x265 OPUS 2.0</h1><section><h2><span>发布说明</span></h2><div><div class="prose"><div class="prose"><figure data-md="image-block"><div><img alt="" referrerPolicy="no-referrer" src="https://ktnbytes.com/2026/08/27/260827-sousou-no-frieren-s01-x265/kv.jpg" loading="eager" decoding="async" fetchPriority="high"/><div aria-hidden="true"><div></div><div><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" x2="16.65" y1="21" y2="16.65"></line><line x1="11" x2="11" y1="8" y2="14"></line><line x1="8" x2="14" y1="11" y2="11"></line></svg><span>查看完整图片</span></div></div></div></figure></div>\n<p>post via api service</p>\n<h2 id="content-info"><span>Content Info<button type="button" aria-label="复制" title="复制"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 17H7A5 5 0 0 1 7 7h2"></path><path d="M15 7h2a5 5 0 1 1 0 10h-2"></path><line x1="8" x2="16" y1="12" y2="12"></line></svg></button></span></h2>\n<p>SUBS : Nest方案(Haruhana、Nekomoe-kissaten、KitaujiSub、Sakurato、Netflix、Cantonese)<br/>SIZE : 48.451 GiB</p>\n<h2 id="team-info"><span>Team Info<button type="button" aria-label="复制" title="复制"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 17H7A5 5 0 0 1 7 7h2"></path><path d="M15 7h2a5 5 0 1 1 0 10h-2"></path><line x1="8" x2="16" y1="12" y2="12"></line></svg></button></span></h2>\n<p><a rel="noopener noreferrer ugc nofollow" href="https://ktnbytes.com/" target="_blank">主發布站 : ktnbytes.com</a><br/><a target="_blank" rel="noopener noreferrer ugc nofollow" href="https://t.me/ktnbytes">RSS 頻道 : t.me/ktnbytes</a><br/><a target="_blank" rel="noopener noreferrer ugc nofollow" href="https://t.me/+lOZ13tUZHo8zZWE1">TG 交流群 : t.me/+lOZ13tUZHo8zZWE1</a></p>\n<br/></div></div></section><a href="https://bgm.tv/subject/400602">Bangumi</a></body></html>';
export const ANIMEGARDEN_DETAIL = {
status: "OK",
resource: {
id: 2456977,
provider: "dmhy",
providerId: "725809",
title: "[7³ACG] 葬送的芙莉莲/Sousou no Frieren S01 | 01-28+SPx11 [简繁字幕] BDrip 1080p x265 OPUS 2.0",
href: "https://share.dmhy.org/topics/view/725809_7_ACG_Sousou_no_Frieren_S01_01-28_SPx11_BDrip_1080p_x265_OPUS_2_0.html",
type: "合集",
magnet: "magnet:?xt=urn:btih:7BSRIRFCYCZ767R5UPF5OXNFR327BHQZ",
tracker:
"&dn=&tr=http%3A%2F%2F104.143.10.186%3A8000%2Fannounce&tr=udp%3A%2F%2F104.143.10.186%3A8000%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=http%3A%2F%2Ftracker3.itzmx.com%3A6961%2Fannounce&tr=http%3A%2F%2Ftracker4.itzmx.com%3A2710%2Fannounce&tr=http%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=http%3A%2F%2Ftracker.prq.to%2Fannounce&tr=http%3A%2F%2Fopen.acgtracker.com%3A1096%2Fannounce&tr=https%3A%2F%2Ft-115.rhcloud.com%2Fonly_for_ylbud&tr=http%3A%2F%2Ftracker1.itzmx.com%3A8080%2Fannounce&tr=http%3A%2F%2Ftracker2.itzmx.com%3A6961%2Fannounce&tr=udp%3A%2F%2Ftracker1.itzmx.com%3A8080%2Fannounce&tr=udp%3A%2F%2Ftracker2.itzmx.com%3A6961%2Fannounce&tr=udp%3A%2F%2Ftracker3.itzmx.com%3A6961%2Fannounce&tr=udp%3A%2F%2Ftracker4.itzmx.com%3A2710%2Fannounce",
size: 52076478464,
createdAt: "2026-08-27T21:32:00.000Z",
fetchedAt: "2026-09-19T05:26:47.016Z",
publisher: {
id: 46,
name: "c8l",
avatar: "https://cdn.discordapp.com/attachments/926889384962318377/1027937252858400778/unknown.png",
},
metadata: {},
},
detail: {
id: 2456977,
description:
'<strong>簡介:&nbsp;</strong><br><p style="margin: 1.6em 0px; padding: 0px; border: 0px; outline: 0px; font-family: &amp;quot;Noto Sans TC&amp;quot;, &amp;quot;Noto Sans SC&amp;quot;, -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Roboto, Oxygen, Ubuntu, Cantarell, &amp;quot;Fira Sans&amp;quot;, &amp;quot;Droid Sans&amp;quot;, &amp;quot;Helvetica Neue&amp;quot;, sans-serif; font-size: 14px; vertical-align: baseline; line-height: 1.6em; color: rgb(85, 85, 85); text-align: center;"><a href="https://ktnbytes.com/2026/08/27/260827-sousou-no-frieren-s01-x265/kv.jpg" style="margin: 0px; padding: 0px; border: 0px; outline: 0px; font-weight: inherit; font-style: inherit; font-family: inherit; vertical-align: baseline; color: rgb(37, 143, 184); text-decoration: none;" rel="external nofollow"><img src="https://ktnbytes.com/2026/08/27/260827-sousou-no-frieren-s01-x265/kv.jpg" width="480" decoding="async" loading="eager" fetchpriority="high" style="margin: auto; padding: 0px; border-width: medium; border-style: none; border-color: currentcolor; border-image: initial; outline: 0px; font-weight: inherit; font-style: inherit; font-family: inherit; vertical-align: baseline; max-width: 100%; height: auto; display: block;" alt=""></a></p>\n<p style="margin: 1.6em 0px; padding: 0px; border: 0px; outline: 0px; font-family: &amp;quot;Noto Sans TC&amp;quot;, &amp;quot;Noto Sans SC&amp;quot;, -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Roboto, Oxygen, Ubuntu, Cant',
magnets: [
{
name: "会员专用链接",
url: "https://dl.dmhy.org/2026/08/28/f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19.torrent",
},
{
name: "磁力链接",
url: "magnet:?xt=urn:btih:7BSRIRFCYCZ767R5UPF5OXNFR327BHQZ&dn=&tr=http%3A%2F%2F104.143.10.186%3A8000%2Fannounce&tr=udp%3A%2F%2F104.143.10.186%3A8000%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=http%3A%2F%2Ftracker3.itzmx.com%3A6961%2Fannounce&tr=http%3A%2F%2Ftracker4.itzmx.com%3A2710%2Fannounce&tr=http%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=http%3A%2F%2Ftracker.prq.to%2Fannounce&tr=http%3A%2F%2Fopen.acgtracker.com%3A1096%2Fannounce&tr=https%3A%2F%2Ft-115.rhcloud.com%2Fonly_for_ylbud&tr=http%3A%2F%2Ftracker1.itzmx.com%3A8080%2Fannounce&tr=http%3A%2F%2Ftracker2.itzmx.com%3A6961%2Fannounce&tr=udp%3A%2F%2Ftracker1.itzmx.com%3A8080%2Fannounce&tr=udp%3A%2F%2Ftracker2.itzmx.com%3A6961%2Fannounce&tr=udp%3A%2F%2Ftracker3.itzmx.com%3A6961%2Fannounce&tr=udp%3A%2F%2Ftracker4.itzmx.com%3A2710%2Fannounce",
},
{
name: "磁力链接 type II",
url: "magnet:?xt=urn:btih:f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19",
},
{
name: "弹幕播放链接",
url: "ddplay:magnet:?xt=urn:btih:7BSRIRFCYCZ767R5UPF5OXNFR327BHQZ",
},
],
files: [
{
size: "13.9MB",
name: "[BDrip] Sousou no Frieren S01 [7鲁ACG]/Sousou no Frieren 2023 S00E01-[1080p][BDRIP][x265.OPUS].mkv",
},
{
size: "18.8MB",
name: "[BDrip] Sousou no Frieren S01 [7鲁ACG]/Sousou no Frieren 2023 S00E02-[1080p][BDRIP][x265.OPUS].mkv",
},
{
size: "23.2MB",
name: "[BDrip] Sousou no Frieren S01 [7鲁ACG]/Sousou no Frieren 2023 S00E03-[1080p][BDRIP][x265.OPUS].mkv",
},
],
hasMoreFiles: false,
fetchedAt: "2026-09-26T10:08:27.755Z",
},
isDeleted: false,
};
export const BANGUMIMOE_FETCH = {
_id: "6abfdc3e992853e4c950e988",
category_tag_id: "549ef207fe682f7549f1ea90",
title: "[黒ネズミたち] 我家的弟弟們真是讓您費心了 / Uchi no Otouto-domo ga Sumimasen - 13 (CR 1920x1080 AVC AAC MKV)",
introduction:
'<p><img src="https://i0.wp.com/imgsrv.crunchyroll.com/catalog/crunchyroll/e5093a79138e3f59793fb5b9116baefc.png" alt /></p>\n<hr />\n<p><h3><strong><em><i>Uchi no Otouto-domo ga Sumimasen</i></em></strong> - EP 13</h3> <strong><a href="https://anilist.co/anime/203490" target="_blank">AniList</a> | <a href="https://bgm.tv/subject/613438" target="_blank">bangumi.tv</a> | <a href="https://myanimelist.net/anime/63061" target="_blank">MAL</a> | <a href="https://www.themoviedb.org/tv/307585" target="_blank">TMDB</a></strong></p>\n<hr />\n<p><strong>Information:</strong></p>\n<ul> <li><strong>Overall Bit Rate:</strong> 5 152 kb/s</li> <li><strong>Subtitle:</strong> English, Latin American Spanish, European Spanish, Brazilian Portuguese, French, German, Arabic (Saudi Arabia), Italian, Russian</li> <li><strong>Duration:</strong> 00:23:50.350</li> <li><strong>CRC32:</strong> C17CCF49</li> <li><strong><a href="https://rr1---nfo.ouo.si/%5BDynamis%20One%5D%20Uchi%20no%20Otouto-domo%20ga%20Sumimasen%20-%2013%20%28CR%201920x1080%20AVC%20AAC%20MKV%29%20%5BC17CCF49%5D.mkv?v=pre-20260429" target="_blank">MediaInfo</a></strong></li>\n</ul>\n<hr />\n<p>In case of any issues with the file, please let us know via the contact details below.<br><strong>Xunlei has been banned by default.</strong><br><strong>Seeding after downloading is appreciated.</strong></p>\n<hr />\n<p><strong>Contact:</strong> <ul> <li><strong>Rel: <a href="https://ouo.si/user/BraveSail" target="_blank">ouo.si</a> | <a href="https://mikana',
tag_ids: [
"63e4b7585fa12c0007949b88",
"548ee2ce4ab7379536f56358",
"5ed5f6ad306f1a0007da8779",
"55c3036c7030f8c855f992ee",
"548ee00b4ab7379536f56353",
"6a715390b56eb30ad03fd531",
"549ef207fe682f7549f1ea90",
],
comments: 0,
downloads: 3,
finished: 0,
leechers: 0,
seeders: 0,
uploader_id: "60098e9132f14c000739f8f4",
team_id: "63e497a55fa12c0007944399",
publish_time: "2026-10-02T16:30:54.858Z",
magnet: "magnet:?xt=urn:btih:fae4141df6efe9a65471f1e933d14687be713040",
infoHash: "fae4141df6efe9a65471f1e933d14687be713040",
file_id: "6abfdc3e992853e4c950e987",
teamsync: null,
content: [
["[Dynamis One] Uchi no Otouto-domo ga Sumimasen - 13 (CR 1920x1080 AVC AAC MKV) [C17CCF49].mkv", "921.11 MB"],
],
size: "921.11 MB",
btskey: "",
};