/** * M-Team API 解析。 * * 迁移自 MoviePilot e6820a8 `app/modules/indexer/spider/mtorrent.py` `MTorrentSpider`: * - `_movie_category/_tv_category/_music_category/_labels` → 同名常量 * - `__get_params` → `mteamSearchParams` * - `__parse_result` → `parseMteamResult` * - `__find_imdbid / __get_downloadvolumefactor / __get_uploadvolumefactor` → 同名函数 * - `_pageurl` → `page_url` * * 与 MP 的差异: * 1. 不生成 `[base64(json 含 x-api-key)]genDlToken` 形式的 enclosure(凭据不得进入候选对象,迁移账本 MT3), * 下载时由 adapter 现场调用 genDlToken(行为同 MP `_resolve_indirect_download_url`)。 * 2. 额外输出 `category_id`(原始分类 ID),`category` 仍是 MP 的媒体类型投影。 * 3. 额外保留 `status` 原文到 raw,便于核验。 */ import type { MediaKind, MpMediaType, SpiderTorrent } from "../spider/spider.ts"; export const MOVIE_CATEGORY = ["401", "419", "420", "421", "439", "405", "404"]; export const TV_CATEGORY = ["403", "402", "435", "438", "404", "405"]; export const MUSIC_CATEGORY = ["434", "406"]; export const LABELS: Record = { "0": "", "1": "DIY", "2": "国配", "3": "DIY 国配", "4": "中字", "5": "DIY 中字", "6": "国配 中字", "7": "DIY 国配 中字", }; export const PAGE_SIZE = 100; /** M-Team 种子 JSON(字段名按公开 API,待真实账号核验)。 */ export interface MteamTorrentJson { id: string | number; name?: string | null; smallDescr?: string | null; category?: string | number | null; size?: string | number | null; createdDate?: string | null; imdb?: string | null; douban?: string | null; labels?: string | number | null; labelsNew?: string[] | null; descr?: string | null; mediainfo?: string | null; status?: { seeders?: string | number | null; leechers?: string | number | null; timesCompleted?: string | number | null; discount?: string | null; discountEndTime?: string | null; promotionRule?: { discount?: string | null; endTime?: string | null } | null; mallSingleFree?: { status?: string | null; endDate?: string | null } | null; } | null; [k: string]: unknown; } /** MP `__get_params`(page 为 0 起) */ export function mteamSearchParams(keyword: string | null | undefined, mtype: MediaKind | undefined, page = 0) { let categories: string[]; if (!mtype) categories = []; else if (mtype === "tv") categories = TV_CATEGORY; else if (mtype === "music") categories = MUSIC_CATEGORY; else categories = MOVIE_CATEGORY; // mtorrent 搜索 imdb 需要输入完整 imdb 链接(MP 注释引 wiki.m-team.cc/zh-tw/imdbtosearch) const kw = keyword?.startsWith("tt") ? `https://www.imdb.com/title/${keyword}` : (keyword ?? null); return { keyword: kw, categories, pageNumber: Number(page) + 1, pageSize: PAGE_SIZE, visible: 1 }; } /** MP `__find_imdbid` */ export function findImdbid(imdb: string | null | undefined): string { return imdb ? (/tt\d+/.exec(imdb)?.[0] ?? "") : ""; } /** MP `__get_downloadvolumefactor` */ export function getDownloadVolumeFactor(discount: string | null | undefined): number { const map: Record = { FREE: 0, PERCENT_50: 0.5, PERCENT_70: 0.3, _2X_FREE: 0, _2X_PERCENT_50: 0.5 }; return discount ? (map[discount] ?? 1) : 1; } /** MP `__get_uploadvolumefactor` */ export function getUploadVolumeFactor(discount: string | null | undefined): number { const map: Record = { _2X: 2.0, _2X_FREE: 2.0, _2X_PERCENT_50: 2.0 }; return discount ? (map[discount] ?? 1) : 1; } /** * MP `time_tools.format_timestamp`:非纯数字字符串原样返回;Unix 秒 → 本地墙钟 "YYYY-MM-DD HH:MM:SS"。 * MP 用进程本地时区(容器默认 Asia/Shanghai);这里固定 +08:00,与 adapter 按 `umt.timezone`(+08:00)转 UTC 配套。 */ function formatTimestamp(v: string | number | null | undefined): string | undefined { if (v == null) return undefined; if (typeof v === "string" && !/^\d+$/.test(v)) return v; const n = Number(v); if (!Number.isFinite(n)) return String(v); const d = new Date(Math.trunc(n) * 1000 + 8 * 3600_000); const p = (x: number) => String(x).padStart(2, "0"); return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`; } function toInt(v: unknown): number { const n = Number.parseInt(String(v ?? "0") || "0", 10); return Number.isFinite(n) ? n : 0; } /** MP `__parse_result` */ export function parseMteamResult(results: MteamTorrentJson[] | null | undefined, webUrl: string): SpiderTorrent[] { const out: SpiderTorrent[] = []; if (!results) return out; const url = webUrl.endsWith("/") ? webUrl : `${webUrl}/`; for (const result of results) { const categoryValue = result.category == null ? undefined : String(result.category); let category: MpMediaType; if (categoryValue && MUSIC_CATEGORY.includes(categoryValue)) category = "音乐"; else if (categoryValue && TV_CATEGORY.includes(categoryValue) && !MOVIE_CATEGORY.includes(categoryValue)) category = "电视剧"; else if (categoryValue && MOVIE_CATEGORY.includes(categoryValue)) category = "电影"; else category = "未知"; let labels: string[] = []; if (result.labelsNew && result.labelsNew.length > 0) labels = result.labelsNew.map(String); else { const v = LABELS[String(result.labels || "0")] ?? ""; if (v) labels = v.split(/\s+/); } const status = result.status ?? {}; const torrent: SpiderTorrent = { title: result.name ?? undefined, description: result.smallDescr ?? undefined, size: toInt(result.size), seeders: toInt(status.seeders), peers: toInt(status.leechers), grabs: toInt(status.timesCompleted), downloadvolumefactor: getDownloadVolumeFactor(status.discount), uploadvolumefactor: getUploadVolumeFactor(status.discount), page_url: `${url}detail/${result.id}`, imdbid: findImdbid(result.imdb), labels, category, }; const pubdate = formatTimestamp(result.createdDate); if (pubdate) torrent.pubdate = pubdate; if (categoryValue) torrent.category_id = categoryValue; if (status.discountEndTime) torrent.freedate = formatTimestamp(status.discountEndTime); // 全站促销规则(当前馒头只有下载促销) if (status.promotionRule) { torrent.downloadvolumefactor = getDownloadVolumeFactor(status.promotionRule.discount ?? "NORMAL"); if (status.promotionRule.endTime) torrent.freedate = formatTimestamp(status.promotionRule.endTime); } if (status.mallSingleFree && status.mallSingleFree.status === "ONGOING") { torrent.downloadvolumefactor = getDownloadVolumeFactor("FREE"); if (status.mallSingleFree.endDate) torrent.freedate = formatTimestamp(status.mallSingleFree.endDate); } out.push(torrent); } return out; }