129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
ManualAcquisitionResult,
|
|
MediaServerInventory,
|
|
ReleaseSearchPage,
|
|
type ReleaseSearchSort,
|
|
type SearchReleasePlanRequest,
|
|
SearchResponse,
|
|
type SearchScope,
|
|
TmdbMediaDetail,
|
|
} 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,
|
|
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;
|
|
/** 字节 */
|
|
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<unknown>(`/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,
|
|
});
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|