docs(plan): on-demand site detail page parsing (HHClub details.php)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
94fd48bff2
commit
ac478673bc
39 files changed
+2446
-339
No files matched your search
@@ -2,17 +2,19 @@ import { z } from "zod";
|
||||
import { Id, NonNegInt, Timestamp } from "./common.ts";
|
||||
import { MediaEntityStatus, MediaKind } from "./content.ts";
|
||||
import { ManualAcquisitionRequest } from "./execution.ts";
|
||||
import { InventoryEntry, SlotStatusCounts } from "./organization.ts";
|
||||
import { InventoryEntry, MediaServerInventory, SlotStatusCounts } from "./organization.ts";
|
||||
|
||||
// 全局搜索(W9-G,对标 MP 顶栏搜索 + 资源搜索页):
|
||||
// GET /search 聚合本地作品 / TMDB 候选 / 跨站资源 / 页面与操作;GET /search/releases 是资源结果页(筛选/排序/游标,复用同一缓存)。
|
||||
// SEARCH-2 两阶段:先查作品并与库存对比(scope=all 不再查站点),需要时再按作品显式搜资源(站点可选);
|
||||
// 仅 TMDB 的作品详情走 GET /media/tmdb/{type}/{id}。
|
||||
|
||||
export const SearchScope = z.enum(["all", "media", "releases", "actions"]);
|
||||
export type SearchScope = z.infer<typeof SearchScope>;
|
||||
|
||||
export const SearchQuery = z.object({
|
||||
q: z.string().trim().min(1).max(200),
|
||||
/** all = 四组都查;命令面板先查 media/actions(快),资源组单独查(最慢一站可达 15s) */
|
||||
/** all = 作品 + 操作(不查站点);资源只在 scope=releases 或 /search/releases 显式查询(最慢一站可达 15s) */
|
||||
scope: SearchScope.default("all"),
|
||||
/** 每组最多返回条数 */
|
||||
limit: z.coerce.number().int().min(1).max(50).default(10),
|
||||
@@ -171,6 +173,47 @@ export type SearchAction = z.infer<typeof SearchAction>;
|
||||
|
||||
export const SearchGroupBase = z.object({ tookMs: NonNegInt });
|
||||
|
||||
// ── 作品(本地 + TMDB 合并去重,本地优先)与库存对比 ──
|
||||
|
||||
/**
|
||||
* 库内状态:not_collected 未收录(本地没有这部作品)> tracked 已建档但没有入库版本 > owned 已有版本 > downloading 正在下载。
|
||||
* 只看本地账本;Emby 另查 GET /inventory/media-servers(未知 ≠ 缺失)。
|
||||
*/
|
||||
export const WorkInventoryState = z.enum(["not_collected", "tracked", "owned", "downloading"]);
|
||||
export type WorkInventoryState = z.infer<typeof WorkInventoryState>;
|
||||
|
||||
export const WorkInventory = z.object({
|
||||
state: WorkInventoryState,
|
||||
subscribed: z.boolean(),
|
||||
/** 已入库版本数(主视频资产) */
|
||||
versionCount: NonNegInt,
|
||||
downloadingCount: NonNegInt,
|
||||
/** 版本目标:总数 / 已满足 */
|
||||
slotTotal: NonNegInt,
|
||||
slotSatisfied: NonNegInt,
|
||||
/** 未满足的版本目标(empty / attention / upgrade_pending) */
|
||||
missing: z.array(z.object({ slotId: Id, name: z.string() })),
|
||||
});
|
||||
export type WorkInventory = z.infer<typeof WorkInventory>;
|
||||
|
||||
export const SearchWork = z.object({
|
||||
/** local:<作品 ID> 或 tmdb:<movie|tv>:<TMDB ID> */
|
||||
key: z.string(),
|
||||
source: z.enum(["local", "tmdb"]),
|
||||
kind: MediaKind,
|
||||
title: z.string(),
|
||||
originalTitle: z.string().nullable(),
|
||||
year: z.number().int().nullable(),
|
||||
/** 简介摘要(≤ 300 字) */
|
||||
overview: z.string().nullable(),
|
||||
posterPath: z.string().nullable(),
|
||||
/** 本地作品 ID(合并过的跟到最终作品);null = 未收录 */
|
||||
localEntityId: Id.nullable(),
|
||||
tmdb: z.object({ entityType: z.enum(["movie", "tv"]), externalId: z.string() }).nullable(),
|
||||
inventory: WorkInventory,
|
||||
});
|
||||
export type SearchWork = z.infer<typeof SearchWork>;
|
||||
|
||||
export const SearchResponse = z.object({
|
||||
q: z.string(),
|
||||
scope: SearchScope,
|
||||
@@ -183,6 +226,8 @@ export const SearchResponse = z.object({
|
||||
configured: z.boolean(),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
/** 本地 + TMDB 合并去重(同一 TMDB 身份只出现一次,本地在前),带库存对比 */
|
||||
works: z.array(SearchWork),
|
||||
})
|
||||
.nullable(),
|
||||
releases: SearchGroupBase.extend({
|
||||
@@ -206,9 +251,19 @@ export const ReleaseSearchSort = z.enum(["default", "seeders", "size", "uploaded
|
||||
export type ReleaseSearchSort = z.infer<typeof ReleaseSearchSort>;
|
||||
|
||||
export const ReleaseSearchQuery = z.object({
|
||||
q: z.string().trim().min(1).max(200),
|
||||
/** 逗号分隔的站点 ID */
|
||||
/** 关键词;按作品搜时可省略,由服务端按作品生成查询词 */
|
||||
q: z.string().trim().min(1).max(200).optional(),
|
||||
/** 按本地作品搜:用作品名 / 原名 / 年份生成查询词 */
|
||||
entityId: Id.optional(),
|
||||
/** 按 TMDB 条目搜:`movie:157336` / `tv:1399` */
|
||||
tmdb: z
|
||||
.string()
|
||||
.regex(/^(movie|tv):\d{1,12}$/)
|
||||
.optional(),
|
||||
/** 逗号分隔的站点 ID:只查这些站点;省略时按 settings.search.defaultSiteMode */
|
||||
sites: csv,
|
||||
/** 逗号分隔的站点 ID:只筛选结果(仍查 sites 里的全部站点,分面计数不变) */
|
||||
siteFilter: csv,
|
||||
/** 逗号分隔:2160p,1080p… */
|
||||
resolution: csv,
|
||||
/** 逗号分隔:REMUX,WEB-DL,BluRay… */
|
||||
@@ -236,6 +291,8 @@ export type FacetCount = z.infer<typeof FacetCount>;
|
||||
export const ReleaseSearchPage = z
|
||||
.object({
|
||||
q: z.string(),
|
||||
/** 实际发给站点的查询词(按作品搜时由服务端生成,每个站点每个词查一次) */
|
||||
queries: z.array(z.string()).optional(),
|
||||
items: z.array(SearchRelease),
|
||||
nextCursor: z.string().nullable(),
|
||||
/** 筛选后的条数 */
|
||||
@@ -259,3 +316,56 @@ export type SearchReleaseParams = z.infer<typeof SearchReleaseParams>;
|
||||
/** 从搜索结果直接下载:与 POST /acquisition-plans 同一请求体,资源由路径定位 */
|
||||
export const SearchReleasePlanRequest = ManualAcquisitionRequest.omit({ sourceReleaseId: true });
|
||||
export type SearchReleasePlanRequest = z.infer<typeof SearchReleasePlanRequest>;
|
||||
|
||||
// ── 仅 TMDB 作品的详情(SEARCH-2) ──
|
||||
|
||||
export const TmdbMediaParams = z.object({
|
||||
type: z.enum(["movie", "tv"]),
|
||||
id: z.string().regex(/^\d{1,12}$/),
|
||||
});
|
||||
export type TmdbMediaParams = z.infer<typeof TmdbMediaParams>;
|
||||
|
||||
export const TmdbMediaQuery = z.object({
|
||||
/** mediaServers = 同时转调 GET /inventory/media-servers(最慢约 8s) */
|
||||
include: z.enum(["mediaServers"]).optional(),
|
||||
});
|
||||
export type TmdbMediaQuery = z.infer<typeof TmdbMediaQuery>;
|
||||
|
||||
export const TmdbMediaSeason = z.object({
|
||||
seasonNumber: z.number().int(),
|
||||
episodeCount: NonNegInt,
|
||||
name: z.string().nullable(),
|
||||
airDate: z.string().nullable(),
|
||||
/** 本地已入库的集数(整季资产按全季计);未收录为 null */
|
||||
localHave: NonNegInt.nullable(),
|
||||
});
|
||||
export type TmdbMediaSeason = z.infer<typeof TmdbMediaSeason>;
|
||||
|
||||
export const TmdbMediaDetail = z.object({
|
||||
provider: z.literal("tmdb"),
|
||||
entityType: z.enum(["movie", "tv"]),
|
||||
externalId: z.string(),
|
||||
kind: MediaKind,
|
||||
title: z.string(),
|
||||
originalTitle: z.string().nullable(),
|
||||
originalLanguage: z.string().nullable(),
|
||||
year: z.number().int().nullable(),
|
||||
releaseDate: z.string().nullable(),
|
||||
overview: z.string().nullable(),
|
||||
posterPath: z.string().nullable(),
|
||||
backdropPath: z.string().nullable(),
|
||||
originCountry: z.array(z.string()),
|
||||
/** 别名 / 译名(最多 20 个) */
|
||||
aliases: z.array(z.string()),
|
||||
externalIds: z.object({ imdb: z.string().nullable(), tvdb: z.string().nullable() }),
|
||||
seasons: z.array(TmdbMediaSeason),
|
||||
/** 已建档:本地作品 ID(跟随合并) */
|
||||
localEntityId: Id.nullable(),
|
||||
/** 本地库存对比;未收录时 state = not_collected */
|
||||
inventory: WorkInventory,
|
||||
/** include=mediaServers 时给出;未知 ≠ 缺失 */
|
||||
mediaServers: MediaServerInventory.nullable(),
|
||||
/** 搜资源时服务端会用的查询词 */
|
||||
queries: z.array(z.string()),
|
||||
});
|
||||
export type TmdbMediaDetail = z.infer<typeof TmdbMediaDetail>;
|
||||
Reference in new issue
Block a user