Files
silo-server/web/src/hooks/queries/progress.ts
T

112 lines
3.3 KiB
TypeScript
Raw Normal View History

import { useMutation, useQuery, useQueries, useQueryClient } from "@tanstack/react-query";
2026-05-22 20:26:11 -04:00
import { api } from "@/api/client";
import type { ProgressListResponse, ProgressEntry, ItemDetail } from "@/api/types";
import { catalogKeys, progressKeys } from "./keys";
import { fetchCatalogItemDetail } from "./catalogRead";
interface ContinueWatchingOptions {
enabled?: boolean;
}
export function useProgressList(libraryId?: number, options?: ContinueWatchingOptions) {
return useQuery({
queryKey: progressKeys.list("in_progress", libraryId),
queryFn: () => {
const searchParams = new URLSearchParams({
status: "in_progress",
limit: "20",
});
if (libraryId) {
searchParams.set("library_id", String(libraryId));
}
return api<ProgressListResponse>(`/progress?${searchParams.toString()}`);
},
enabled: options?.enabled ?? true,
});
}
export interface ContinueWatchingItem {
progress: ProgressEntry;
detail: ItemDetail | undefined;
isLoading: boolean;
}
export function useContinueWatching(
libraryId?: number,
options?: ContinueWatchingOptions,
): {
items: ContinueWatchingItem[];
isLoading: boolean;
} {
const enabled = options?.enabled ?? true;
const { data: progressData, isLoading: progressLoading } = useProgressList(libraryId, {
enabled,
});
const entries = progressData?.progress ?? [];
const detailQueries = useQueries({
queries: entries.map((entry) => ({
queryKey: catalogKeys.itemDetail(entry.media_item_id),
queryFn: () => fetchCatalogItemDetail(entry.media_item_id),
enabled: enabled && !!entry.media_item_id,
staleTime: 2 * 60 * 1000,
})),
});
const items: ContinueWatchingItem[] = entries.map((entry, i) => ({
progress: entry,
detail: detailQueries[i]?.data,
isLoading: detailQueries[i]?.isLoading ?? true,
}));
return {
items,
isLoading: enabled && progressLoading,
};
}
interface ReportMediaProgressVars {
contentId: string;
positionSeconds: number;
durationSeconds: number;
forceOverwrite?: boolean;
}
export function useReportMediaProgress() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
contentId,
positionSeconds,
durationSeconds,
forceOverwrite = true,
}: ReportMediaProgressVars) =>
api("/sync/progress", {
method: "POST",
body: JSON.stringify({
items: [
{
media_item_id: contentId,
position: positionSeconds,
duration: durationSeconds,
force_overwrite: forceOverwrite,
},
],
}),
}),
onSuccess: (_data, variables) => {
// Progress genuinely changed → refresh progress-derived surfaces
// (continue-watching etc.). Scope the catalog invalidation to THIS item's
// detail rather than all of `catalog`: a progress report fires every ~10s
// during playback, and invalidating catalogKeys.all refetched every active
// browse/detail query — including the large audiobook author/narrator
// group lists — on every tick.
queryClient.invalidateQueries({ queryKey: progressKeys.all });
queryClient.invalidateQueries({
queryKey: catalogKeys.itemDetail(variables.contentId),
});
},
});
}