176 lines
6.6 KiB
TypeScript
176 lines
6.6 KiB
TypeScript
/**
|
|
* SPDX-License-Identifier: GPL-3.0-only
|
|
* 移植自 MoviePilot(https://github.com/jxxghp/MoviePilot,GPLv3),本文件按 GPLv3 条款提供。
|
|
* @mp-source app/modules/indexer/spider/rousi.py @ 3b156a5
|
|
*
|
|
* Rousi.pro PeerGo 兼容 API(parser "RousiPro",Bearer 个人 API Key,需 torrent:read + torrent:download)。
|
|
* 函数对照 `RousiSpider`:
|
|
* - `_movie_category/_tv_category/_music_category` → `ROUSI_*_CATEGORY`
|
|
* - `get_search_page_size` → `pageSize`(100)
|
|
* - `__get_params` → `rousiSearchParams`;`__get_category_names_by_ids` → `rousiCategoryNamesByIds`
|
|
* - `__process_response` → `rousiSpider.search` 错误分支;`__parse_result` → `parseRousiResult`
|
|
* - `__get_download_url`(GET 详情接口,result=data.download_url)→ `rousiSpider.resolveDownload`(现场换票,不生成含 key 的 enclosure)
|
|
*/
|
|
import type { MpMediaType } from "../spider/spider.ts";
|
|
import { extractDomain, normalizeDatetime } from "../user-parsers/common.ts";
|
|
import {
|
|
apiRequest,
|
|
businessError,
|
|
checkReleaseId,
|
|
downloadTorrentFile,
|
|
isoWithZoneToUtc,
|
|
isPlainObject,
|
|
resolveIndirectDownloadUrl,
|
|
toFloat,
|
|
toInt,
|
|
} from "./common.ts";
|
|
import type { SpecializedSiteDefinition, SpecializedSpider, SpecializedTorrent, SpiderSearchQuery } from "./types.ts";
|
|
|
|
/** API 不支持多分类搜索,每次只使用一个分类 */
|
|
export const ROUSI_MOVIE_CATEGORY = "movie";
|
|
export const ROUSI_TV_CATEGORY = "tv";
|
|
export const ROUSI_MUSIC_CATEGORY = "music";
|
|
|
|
const ROUSI_ID_TO_NAME: Record<string, string> = {
|
|
"1": "movie",
|
|
"2": "tv",
|
|
"3": "documentary",
|
|
"4": "animation",
|
|
"5": "music",
|
|
"6": "variety",
|
|
};
|
|
|
|
/** 站点 origin(MP `https://%s` % extract_domain(domain)) */
|
|
export function rousiBase(def: SpecializedSiteDefinition): string {
|
|
return (def.umt.apiBaseUrl ?? `https://${extractDomain(def.domain)}`).replace(/\/+$/, "");
|
|
}
|
|
|
|
/** MP `__get_category_names_by_ids` */
|
|
export function rousiCategoryNamesByIds(cat: string | null | undefined): string[] | null {
|
|
if (!cat) return null;
|
|
const ids = cat
|
|
.split(",")
|
|
.map((c) => c.trim())
|
|
.filter(Boolean);
|
|
const names = ids.filter((id) => id in ROUSI_ID_TO_NAME).map((id) => ROUSI_ID_TO_NAME[id]!);
|
|
return names.length ? names : null;
|
|
}
|
|
|
|
/** MP `__get_params` */
|
|
export function rousiSearchParams(q: SpiderSearchQuery): Record<string, string | number> {
|
|
const params: Record<string, string | number> = { page: Number(q.page ?? 0) + 1, page_size: 100 };
|
|
if (q.keyword) params.keyword = q.keyword;
|
|
if (q.cat) {
|
|
const names = rousiCategoryNamesByIds(q.cat);
|
|
if (names) params.category = names[0]!;
|
|
} else if (q.mtype) {
|
|
if (q.mtype === "movie") params.category = ROUSI_MOVIE_CATEGORY;
|
|
else if (q.mtype === "tv") params.category = ROUSI_TV_CATEGORY;
|
|
else if (q.mtype === "music") params.category = ROUSI_MUSIC_CATEGORY;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
export interface RousiItem {
|
|
id: number | string;
|
|
title?: string;
|
|
subtitle?: string;
|
|
category?: string | { slug?: string; name?: string } | null;
|
|
size?: number | string;
|
|
seeders?: number | string;
|
|
leechers?: number | string;
|
|
downloads?: number | string;
|
|
created_at?: string;
|
|
promotion?: { is_active?: boolean; down_multiplier?: number; up_multiplier?: number; until?: string } | null;
|
|
[k: string]: unknown;
|
|
}
|
|
|
|
/** MP `__parse_result` */
|
|
export function parseRousiResult(results: RousiItem[] | null | undefined, def: SpecializedSiteDefinition) {
|
|
const torrents: SpecializedTorrent[] = [];
|
|
if (!results?.length) return torrents;
|
|
const base = rousiBase(def);
|
|
for (const result of results) {
|
|
const rawCat = result.category;
|
|
let catVal: string | null | undefined = null;
|
|
let category: MpMediaType = "未知";
|
|
if (rawCat && typeof rawCat === "object") catVal = rawCat.slug || rawCat.name;
|
|
else if (typeof rawCat === "string") catVal = rawCat;
|
|
if (catVal) {
|
|
catVal = String(catVal).toLowerCase();
|
|
if (catVal === ROUSI_MOVIE_CATEGORY) category = "电影";
|
|
else if (catVal === ROUSI_TV_CATEGORY) category = "电视剧";
|
|
else if (catVal === ROUSI_MUSIC_CATEGORY) category = "音乐";
|
|
}
|
|
// API 后端已处理全站促销优先级,直接使用返回的 promotion 数据
|
|
let dl = 1.0;
|
|
let ul = 1.0;
|
|
let freedate: string | undefined;
|
|
let freedateUtc: string | undefined;
|
|
const promotion = result.promotion;
|
|
if (promotion?.is_active) {
|
|
dl = toFloat(promotion.down_multiplier ?? 1.0);
|
|
ul = toFloat(promotion.up_multiplier ?? 1.0);
|
|
if (promotion.until) {
|
|
freedate = normalizeDatetime(promotion.until) ?? undefined;
|
|
freedateUtc = isoWithZoneToUtc(promotion.until);
|
|
}
|
|
}
|
|
const id = result.id;
|
|
const t: SpecializedTorrent = {
|
|
title: result.title,
|
|
description: result.subtitle,
|
|
size: toInt(result.size),
|
|
seeders: toInt(result.seeders),
|
|
peers: toInt(result.leechers),
|
|
grabs: toInt(result.downloads),
|
|
downloadvolumefactor: dl,
|
|
uploadvolumefactor: ul,
|
|
page_url: `${base}/torrents/${id}`,
|
|
labels: [],
|
|
category,
|
|
site_release_id: String(id),
|
|
};
|
|
const pubdate = normalizeDatetime(result.created_at);
|
|
if (pubdate) t.pubdate = pubdate;
|
|
const pubUtc = isoWithZoneToUtc(result.created_at);
|
|
if (pubUtc) t.pubdate_utc = pubUtc;
|
|
if (freedate) t.freedate = freedate;
|
|
if (freedateUtc) t.freedate_utc = freedateUtc;
|
|
if (catVal) t.category_id = catVal;
|
|
torrents.push(t);
|
|
}
|
|
return torrents;
|
|
}
|
|
|
|
export const rousiSpider: SpecializedSpider = {
|
|
parser: "RousiPro",
|
|
credential: "apiKey",
|
|
pageSize: () => 100,
|
|
async search(ctx, q) {
|
|
const apikey = await ctx.secret("apiKey", true);
|
|
const data = await apiRequest(ctx, `${rousiBase(ctx.def)}/api/v1/torrents`, {
|
|
method: "GET",
|
|
headers: { Authorization: `Bearer ${apikey}`, Accept: "application/json" },
|
|
query: rousiSearchParams(q),
|
|
});
|
|
if (!isPlainObject(data)) throw businessError(ctx, "搜索", "搜索响应结构无效");
|
|
if (data.code === 0) {
|
|
const d = isPlainObject(data.data) ? data.data : {};
|
|
return parseRousiResult((d.torrents as RousiItem[]) ?? [], ctx.def);
|
|
}
|
|
throw businessError(ctx, "搜索", data.message);
|
|
},
|
|
async resolveDownload(ctx, siteReleaseId) {
|
|
const apikey = await ctx.secret("apiKey", true);
|
|
const id = checkReleaseId(ctx.def.id, siteReleaseId);
|
|
// MP __get_download_url:method=get, cookie=False, Bearer, result=data.download_url
|
|
const url = await resolveIndirectDownloadUrl(ctx, `${rousiBase(ctx.def)}/api/v1/torrents/${id}`, {
|
|
method: "get",
|
|
header: { Authorization: `Bearer ${apikey}`, Accept: "application/json" },
|
|
result: "data.download_url",
|
|
});
|
|
return downloadTorrentFile(ctx, url);
|
|
},
|
|
};
|