import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ManualAcquisitionResult, MediaServerInventory, ReleaseSearchPage, type ReleaseSearchSort, type SearchReleasePlanRequest, SearchResponse, type SearchScope, TmdbMediaDetail, TmdbSeasonDetailView, } from "@umt/contracts"; import { api } from "~/lib/api.ts"; import { getParsed, parseOrThrow } from "./library.ts"; // 全局搜索(W9-G / SEARCH-2):先查作品(本地 + TMDB,带库存对比,不碰站点);需要时按作品显式搜资源 // /search/releases(站点可选,筛选/排序/游标,(站点, 查询词)30s 缓存)。仅 TMDB 的作品详情 /media/tmdb/{type}/{id}。 export const searchKeys = { all: ["search"] as const, aggregate: (q: string, scope: SearchScope, limit: number) => ["search", "aggregate", q, scope, limit] as const, releases: (f: ReleaseSearchFilters) => ["search", "releases", f] as const, tmdb: (type: string, id: string, ms: boolean) => ["search", "tmdb", type, id, ms] as const, tmdbSeason: (id: string, season: number) => ["search", "tmdb-season", id, season] as const, mediaServers: (ref: MediaServerRef) => ["search", "media-servers", ref] as const, }; /** 聚合搜索;q 为空不发请求 */ export function useSearch(q: string, scope: SearchScope, limit = 8, enabled = true) { const term = q.trim(); return useQuery({ queryKey: searchKeys.aggregate(term, scope, limit), enabled: enabled && term.length > 0, queryFn: ({ signal }) => getParsed(SearchResponse, "/search", { q: term, scope, limit }, signal), placeholderData: keepPreviousData, staleTime: 15_000, }); } export type ReleaseSearchFilters = { /** 关键词;按作品搜时省略,由服务端生成查询词 */ q?: string; /** 按本地作品搜 */ entityId?: string; /** 按 TMDB 条目搜:movie:157336 */ tmdb?: string; /** 逗号分隔:只查这些站点 */ sites?: string; /** 逗号分隔:只筛选结果 */ siteFilter?: string; resolution?: string; source?: string; /** 快速筛选(SEARCH-3):`维度:值,值;维度:值` */ qf?: string; /** 字节 */ minSize?: number; maxSize?: number; free?: "true" | "false"; hr?: "true" | "false"; sort?: ReleaseSearchSort; order?: "asc" | "desc"; }; /** 资源结果页:游标翻页;refresh() 带 refresh=true 跳过缓存重查第一页 */ export function useReleaseSearch(filters: ReleaseSearchFilters, enabled = true) { return useInfiniteQuery({ queryKey: searchKeys.releases(filters), enabled: enabled && (!!filters.q?.trim() || !!filters.entityId || !!filters.tmdb), initialPageParam: undefined as string | undefined, queryFn: ({ pageParam, signal }) => getParsed(ReleaseSearchPage, "/search/releases", { ...filters, cursor: pageParam, limit: 50 }, signal), getNextPageParam: (last) => last.nextCursor ?? undefined, placeholderData: keepPreviousData, staleTime: 30_000, }); } /** 跳过缓存重查站点(仍参与并发合并),结果写回当前筛选的第一页 */ export function useRefreshReleaseSearch() { const qc = useQueryClient(); return useMutation({ mutationFn: (filters: ReleaseSearchFilters) => getParsed(ReleaseSearchPage, "/search/releases", { ...filters, refresh: "true", limit: 50 }), onSuccess: (page, filters) => { qc.setQueryData(searchKeys.releases(filters), { pages: [page], pageParams: [undefined] }); void qc.invalidateQueries({ queryKey: ["search", "aggregate"] }); }, }); } /** 从搜索结果下载:与 POST /acquisition-plans 同一流程 */ export function usePlanFromSearch() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ siteId, siteReleaseId, ...body }: SearchReleasePlanRequest & { siteId: string; siteReleaseId: string }) => api .post(`/search/releases/${encodeURIComponent(siteId)}/${encodeURIComponent(siteReleaseId)}/plan`, body) .then((d) => parseOrThrow(ManualAcquisitionResult, d, "/search/releases/plan")), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["plans"] }); void qc.invalidateQueries({ queryKey: ["slots"] }); void qc.invalidateQueries({ queryKey: searchKeys.all }); }, }); } /** 仅 TMDB 作品的详情(TMDB 信息 + 季 + 本地对比);Emby 对比另走 useMediaServerInventory */ export function useTmdbMedia(type: string | undefined, id: string | undefined) { return useQuery({ queryKey: searchKeys.tmdb(type ?? "", id ?? "", false), enabled: (type === "movie" || type === "tv") && !!id && /^\d+$/.test(id), queryFn: ({ signal }) => getParsed(TmdbMediaDetail, `/media/tmdb/${type}/${id}`, {}, signal), staleTime: 60_000, }); } /** 仅 TMDB 剧集的季详情与每集信息(MD-1,季折叠展开时才拉) */ export function useTmdbSeason(id: string | undefined, season: number, enabled: boolean) { return useQuery({ queryKey: searchKeys.tmdbSeason(id ?? "", season), enabled: enabled && !!id && /^\d+$/.test(id), queryFn: ({ signal }) => getParsed(TmdbSeasonDetailView, `/media/tmdb/tv/${id}/season/${season}`, {}, signal), staleTime: 5 * 60_000, }); } export type MediaServerRef = { mediaEntityId: string } | { tmdbType: "movie" | "tv"; tmdbId: string }; /** Emby 库存对比(按本地作品或 TMDB 条目);实例失败 / 超时为 unknown,不等于缺失 */ export function useMediaServerInventory(ref: MediaServerRef | null, enabled = true) { return useQuery({ queryKey: searchKeys.mediaServers(ref ?? { mediaEntityId: "" }), enabled: enabled && !!ref, queryFn: ({ signal }) => getParsed(MediaServerInventory, "/inventory/media-servers", ref ?? {}, signal), staleTime: 5 * 60_000, retry: false, }); }