import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { EntityWordsDryRun, EntityWordsList, EntityWordsSaveResult, EntityWordsView, EpisodeGroupList, EpisodeGroupSetResult, EpisodeMappingPreview, type EpisodeMappingPreviewInput, EpisodeStructureSeasons, GlobalWordsDryRun, GlobalWordsSaveResult, IdentifyWordsBlock, type WordsTryInput, WordsTryResult, } from "@umt/contracts"; import { api } from "~/lib/api.ts"; import { decisionKeys } from "./decisions.ts"; import { getParsed, libraryKeys, parseOrThrow } from "./library.ts"; // 识别词(全局 / 作品,原文整块读写;保存前试算,作品保存后服务端按作品重算)、集数映射预览(纯读)、剧集组。 export const episodeMappingKeys = { all: ["episode-mapping"] as const, words: (entityId: string | null) => ["episode-mapping", "words", entityId ?? "global"] as const, wordsList: (q: string) => ["episode-mapping", "words-list", q] as const, dryRun: (entityId: string | null, text: string) => ["episode-mapping", "dry-run", entityId ?? "global", text] as const, tryLine: (input: WordsTryInput) => ["episode-mapping", "try", input] as const, preview: (entityId: string, input: EpisodeMappingPreviewInput) => ["episode-mapping", "preview", entityId, input] as const, groups: (entityId: string) => ["episode-mapping", "groups", entityId] as const, structure: (entityId: string, groupId: string | null) => ["episode-mapping", "structure", entityId, groupId ?? "default"] as const, }; // ── 识别词 ── export function useGlobalWords() { return useQuery({ queryKey: episodeMappingKeys.words(null), queryFn: ({ signal }) => getParsed(IdentifyWordsBlock, "/identify-words/global", undefined, signal), // 保存后后台重算有进度:进行中时每 3 秒刷一次 refetchInterval: (q) => { const s = q.state.data?.reresolve?.status; return s === "queued" || s === "running" ? 3_000 : false; }, }); } export function useEntityWords(entityId: string | null | undefined) { return useQuery({ queryKey: episodeMappingKeys.words(entityId ?? ""), enabled: !!entityId, queryFn: ({ signal }) => getParsed(EntityWordsView, `/media-entities/${entityId}/identify-words`, undefined, signal), }); } export function useEntityWordsList(q: string) { return useQuery({ queryKey: episodeMappingKeys.wordsList(q), placeholderData: keepPreviousData, queryFn: ({ signal }) => getParsed(EntityWordsList, "/identify-words/entities", { q: q || undefined }, signal), }); } /** 试算(纯读):text 为 null 时不发 */ export function useWordsDryRun(entityId: string | null, text: string | null) { return useQuery({ queryKey: episodeMappingKeys.dryRun(entityId, text ?? ""), enabled: text !== null, placeholderData: keepPreviousData, queryFn: async () => entityId ? parseOrThrow( EntityWordsDryRun, await api.post(`/media-entities/${entityId}/identify-words/dry-run`, { text }), "/media-entities/{id}/identify-words/dry-run", ) : parseOrThrow( GlobalWordsDryRun, await api.post("/identify-words/global/dry-run", { text }), "/identify-words/global/dry-run", ), }); } /** 一行识别词的实时示例(纯读) */ export function useWordsTry(input: WordsTryInput | null) { return useQuery({ queryKey: episodeMappingKeys.tryLine(input ?? { scope: "global", line: "" }), enabled: !!input?.line.trim(), placeholderData: keepPreviousData, staleTime: 60_000, queryFn: async () => parseOrThrow(WordsTryResult, await api.post("/identify-words/try", input), "/identify-words/try"), }); } /** 保存识别词:成功后刷新识别词、预览、事项与作品详情(季集可能变了) */ export function useSaveWords(entityId: string | null) { const qc = useQueryClient(); return useMutation({ mutationFn: async (body: { text: string; expectedVersion: number }) => entityId ? parseOrThrow( EntityWordsSaveResult, await api.put(`/media-entities/${entityId}/identify-words`, body), "/media-entities/{id}/identify-words", ) : parseOrThrow( GlobalWordsSaveResult, await api.put("/identify-words/global", body), "/identify-words/global", ), onSuccess: () => { void qc.invalidateQueries({ queryKey: episodeMappingKeys.all }); void qc.invalidateQueries({ queryKey: decisionKeys.all }); if (entityId) { void qc.invalidateQueries({ queryKey: libraryKeys.detail(entityId) }); void qc.invalidateQueries({ queryKey: libraryKeys.entity(entityId) }); } }, }); } export function useEpisodeMappingPreview( entityId: string | null | undefined, input: EpisodeMappingPreviewInput, enabled = true, ) { return useQuery({ queryKey: episodeMappingKeys.preview(entityId ?? "", input), enabled: !!entityId && enabled, placeholderData: keepPreviousData, queryFn: async () => parseOrThrow( EpisodeMappingPreview, await api.post(`/media-entities/${entityId}/episode-mapping/preview`, input), "/media-entities/{id}/episode-mapping/preview", ), }); } // ── 剧集组(季集结构) ── /** 作品可选的 TMDB 剧集组与当前选择 */ export function useEpisodeGroups(entityId: string | undefined, enabled = true) { return useQuery({ queryKey: episodeMappingKeys.groups(entityId ?? ""), enabled: !!entityId && enabled, staleTime: 5 * 60_000, queryFn: ({ signal }) => getParsed(EpisodeGroupList, `/media-entities/${entityId}/episode-groups`, undefined, signal), }); } /** 某套季集结构(null = TMDB 默认季)的季卡片与每集,带本地已入库 */ export function useEpisodeStructure(entityId: string | undefined, groupId: string | null, enabled = true) { return useQuery({ queryKey: episodeMappingKeys.structure(entityId ?? "", groupId), enabled: !!entityId && enabled, placeholderData: keepPreviousData, queryFn: ({ signal }) => getParsed( EpisodeStructureSeasons, `/media-entities/${entityId}/episode-structure`, groupId ? { groupId } : undefined, signal, ), }); } /** 切换作品的季集结构:成功后刷新剧集组、预览、规则、待处理与作品详情(季集都变了) */ export function useSetEpisodeGroup(entityId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: async (groupId: string | null) => parseOrThrow( EpisodeGroupSetResult, await api.put(`/media-entities/${entityId}/episode-group`, { groupId }), "/media-entities/{id}/episode-group", ), onSuccess: () => { void qc.invalidateQueries({ queryKey: episodeMappingKeys.all }); void qc.invalidateQueries({ queryKey: decisionKeys.all }); void qc.invalidateQueries({ queryKey: libraryKeys.all }); void qc.invalidateQueries({ queryKey: ["subscriptions"] }); }, }); }