Files
UHD_Media_Tool/packages/adapters/src/sites/spiders/milkie.ts
T

81 lines
3.5 KiB
TypeScript

/**
* SPDX-License-Identifier: GPL-3.0-only
* @mp-source app/modules/indexer/spider/milkie.py @ 3b156a5
* 对照 MoviePilot v3.2.4 的公开 Milkie API 行为实现。搜索结果不保存含 API Key 的下载地址。
*/
import { SiteError } from "../types.ts";
import { apiRequest, businessError, downloadTorrentFile, isoWithZoneToUtc, isPlainObject, siteBase } from "./common.ts";
import type { SpecializedSiteDefinition, SpecializedSpider, SpecializedTorrent, SpiderSearchQuery } from "./types.ts";
export function milkieSearchParams(query: SpiderSearchQuery): Record<string, string | number> {
const params: Record<string, string | number> = { pi: query.page ?? 0, ps: 100, oby: "created_at", odir: "desc" };
if (query.keyword) {
params.query = query.keyword;
const category = query.mtype ? { movie: "1", tv: "2", music: "3" }[query.mtype] : undefined;
if (category) params.categories = category;
}
return params;
}
/** Milkie 资源 ID 可能为 UUID;不使用其它站点的数字 ID 提取规则。 */
function milkieId(siteId: string, value: unknown): string {
const id = String(value ?? "");
if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new SiteError(siteId, "not_found", "Milkie 资源 ID 非法");
return id;
}
export function parseMilkieResult(rows: unknown[], def: SpecializedSiteDefinition): SpecializedTorrent[] {
return rows.flatMap((row): SpecializedTorrent[] => {
if (!isPlainObject(row) || row.id == null || !row.releaseName) return [];
const id = milkieId(def.id, row.id);
const category = { "1": "电影", "2": "电视剧", "3": "音乐" } as const;
const result: SpecializedTorrent = {
title: String(row.releaseName),
description: id,
site_release_id: id,
page_url: `${siteBase(def)}browse/${encodeURIComponent(id)}`,
category: category[String(row.category) as keyof typeof category] ?? "未知",
downloadvolumefactor: 0,
uploadvolumefactor: 1,
};
if (row.category != null) result.category_id = String(row.category);
for (const [source, target] of [
["size", "size"],
["seeders", "seeders"],
["leechers", "peers"],
["downloaded", "grabs"],
] as const) {
const value = row[source] == null ? NaN : Number(row[source]);
if (Number.isFinite(value)) result[target] = value;
}
const utc = isoWithZoneToUtc(row.createdAt);
if (utc) result.pubdate_utc = utc;
else if (typeof row.createdAt === "string") result.pubdate = row.createdAt;
if (isPlainObject(row.externals) && typeof row.externals.imdb === "string") result.imdbid = row.externals.imdb;
return [result];
});
}
export const milkieSpider: SpecializedSpider = {
parser: "Milkie",
credential: "apiKey",
pageSize: () => 100,
async search(ctx, query) {
const key = (await ctx.secret("apiKey", true))!;
const payload = await apiRequest(ctx, `${siteBase(ctx.def)}api/v1/torrents`, {
headers: { "x-milkie-auth": key },
query: milkieSearchParams(query),
});
if (!isPlainObject(payload) || !Array.isArray(payload.torrents))
throw businessError(ctx, "搜索", "Milkie 响应不含 torrents 列表");
return parseMilkieResult(payload.torrents, ctx.def);
},
async resolveDownload(ctx, siteReleaseId) {
const key = (await ctx.secret("apiKey", true))!;
const id = milkieId(ctx.def.id, siteReleaseId);
const url = new URL(`api/v1/torrents/${encodeURIComponent(id)}/torrent`, siteBase(ctx.def));
url.searchParams.set("key", key);
return downloadTorrentFile(ctx, url.href);
},
};