Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
EpisodeGroupList,
|
|
EpisodeGroupSetResult,
|
|
EpisodeMappingPreview,
|
|
type EpisodeMappingPreviewInput,
|
|
type EpisodeMappingRuleCreate,
|
|
EpisodeMappingRuleList,
|
|
type EpisodeMappingRuleUpdate,
|
|
EpisodeMappingRuleWriteResult,
|
|
EpisodeStructureSeasons,
|
|
} 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,
|
|
rules: (entityId: string) => ["episode-mapping", "rules", entityId] 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 useEpisodeMappingRules(entityId: string | undefined) {
|
|
return useQuery({
|
|
queryKey: episodeMappingKeys.rules(entityId ?? ""),
|
|
enabled: !!entityId,
|
|
queryFn: ({ signal }) =>
|
|
getParsed(EpisodeMappingRuleList, `/media-entities/${entityId}/episode-mapping-rules`, undefined, signal),
|
|
});
|
|
}
|
|
|
|
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<unknown>(`/media-entities/${entityId}/episode-mapping/preview`, input),
|
|
"/media-entities/{id}/episode-mapping/preview",
|
|
),
|
|
});
|
|
}
|
|
|
|
type WriteVars =
|
|
| { op: "create"; entityId: string; body: EpisodeMappingRuleCreate }
|
|
| { op: "update"; id: string; body: EpisodeMappingRuleUpdate }
|
|
| { op: "delete"; id: string };
|
|
|
|
/** 规则写入:成功后刷新规则、预览、待处理与作品详情(季集可能变了) */
|
|
export function useEpisodeMappingWrite(entityId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async (v: WriteVars) => {
|
|
const data =
|
|
v.op === "create"
|
|
? await api.post<unknown>(`/media-entities/${v.entityId}/episode-mapping-rules`, v.body)
|
|
: v.op === "update"
|
|
? await api.patch<unknown>(`/episode-mapping-rules/${v.id}`, v.body)
|
|
: await api.del<unknown>(`/episode-mapping-rules/${v.id}`);
|
|
return parseOrThrow(EpisodeMappingRuleWriteResult, data, "/episode-mapping-rules");
|
|
},
|
|
onSuccess: () => {
|
|
void qc.invalidateQueries({ queryKey: episodeMappingKeys.all });
|
|
void qc.invalidateQueries({ queryKey: decisionKeys.all });
|
|
void qc.invalidateQueries({ queryKey: libraryKeys.detail(entityId) });
|
|
void qc.invalidateQueries({ queryKey: libraryKeys.entity(entityId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
// ── 剧集组(季集结构) ──
|
|
|
|
/** 作品可选的 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<unknown>(`/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"] });
|
|
},
|
|
});
|
|
}
|