feat(web): MP-style media detail with seasons and episodes, shared by library and TMDB preview (MD-1)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9cd5a28943
commit
97ac59c7ac
9 files changed
+1250
-190
No files matched your search
@@ -10,6 +10,7 @@ import {
|
||||
MediaEntity,
|
||||
MediaEntityDetail,
|
||||
type MediaKind,
|
||||
type UnitMetadataField,
|
||||
} from "@umt/contracts";
|
||||
import type { z } from "zod";
|
||||
import { ApiRequestError, api, type QueryParams } from "~/lib/api.ts";
|
||||
@@ -144,11 +145,42 @@ export function useUnlockField(id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新元数据(字段级;用户锁优先) */
|
||||
/**
|
||||
* 刷新元数据(字段级;用户锁优先)。full = 拉全部季的每集信息。刷新是后台任务:
|
||||
* 提交后隔几秒再取一次详情,拿到任务写回的结果。
|
||||
*/
|
||||
export function useRefreshMetadata(id: string) {
|
||||
const done = useInvalidateEntity(id);
|
||||
return useMutation({
|
||||
mutationFn: () => api.post<unknown>(`/media-entities/${id}/refresh`),
|
||||
mutationFn: (opts?: { full?: boolean }) =>
|
||||
api.post<unknown>(`/media-entities/${id}/refresh${opts?.full ? "?full=1" : ""}`),
|
||||
onSuccess: () => {
|
||||
done();
|
||||
for (const ms of [3_000, 8_000, 20_000]) setTimeout(done, ms);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 季 / 集字段覆盖并锁定(MD-1) */
|
||||
export function useLockUnitField(entityId: string) {
|
||||
const done = useInvalidateEntity(entityId);
|
||||
return useMutation({
|
||||
mutationFn: (v: { unitId: string; field: UnitMetadataField; value: unknown; reason?: string }) =>
|
||||
api.put<unknown>(`/content-units/${v.unitId}/fields/${v.field}`, {
|
||||
value: v.value,
|
||||
lock: true,
|
||||
reason: v.reason,
|
||||
}),
|
||||
onSuccess: done,
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消季 / 集字段覆盖与锁定 */
|
||||
export function useUnlockUnitField(entityId: string) {
|
||||
const done = useInvalidateEntity(entityId);
|
||||
return useMutation({
|
||||
mutationFn: (v: { unitId: string; field: UnitMetadataField }) =>
|
||||
api.del<unknown>(`/content-units/${v.unitId}/fields/${v.field}`),
|
||||
onSuccess: done,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SearchResponse,
|
||||
type SearchScope,
|
||||
TmdbMediaDetail,
|
||||
TmdbSeasonDetailView,
|
||||
} from "@umt/contracts";
|
||||
import { api } from "~/lib/api.ts";
|
||||
import { getParsed, parseOrThrow } from "./library.ts";
|
||||
@@ -20,6 +21,7 @@ export const searchKeys = {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -114,6 +116,16 @@ export function useTmdbMedia(type: string | undefined, id: string | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 仅 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,不等于缺失 */
|
||||
|
||||
Reference in new issue
Block a user