81 lines
3.5 KiB
TypeScript
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);
|
|
},
|
|
};
|