feat: implement multi-provider support in media library

- Introduced IProviderRegistry to manage multiple media providers.
- Updated AppState to use provider_registry instead of a single media_provider.
- Refactored library routes to support provider-specific queries for collections, series, genres, and items.
- Enhanced ProgrammingBlock to include provider_id for algorithmic and manual content types.
- Modified frontend components to allow selection of providers and updated API calls to include provider parameters.
- Adjusted hooks and types to accommodate provider-specific functionality.
This commit is contained in:
2026-03-14 23:59:21 +01:00
parent c53892159a
commit ead65e6be2
21 changed files with 468 additions and 150 deletions

View File

@@ -7,38 +7,38 @@ import type { MediaFilter } from "@/lib/types";
const STALE = 10 * 60 * 1000; // 10 min — library metadata rarely changes in a session
/** List top-level collections (Jellyfin libraries, Plex sections, etc.) */
export function useCollections() {
/** List top-level collections for a provider (empty = primary). */
export function useCollections(provider?: string) {
const { token } = useAuthContext();
return useQuery({
queryKey: ["library", "collections"],
queryFn: () => api.library.collections(token!),
queryKey: ["library", "collections", provider ?? null],
queryFn: () => api.library.collections(token!, provider),
enabled: !!token,
staleTime: STALE,
});
}
/**
* List TV series, optionally scoped to a collection.
* List TV series, optionally scoped to a collection and provider.
* All series are loaded upfront so the series picker can filter client-side
* without a request per keystroke.
*/
export function useSeries(collectionId?: string, opts?: { enabled?: boolean }) {
export function useSeries(collectionId?: string, opts?: { enabled?: boolean; provider?: string }) {
const { token } = useAuthContext();
return useQuery({
queryKey: ["library", "series", collectionId ?? null],
queryFn: () => api.library.series(token!, collectionId),
queryKey: ["library", "series", collectionId ?? null, opts?.provider ?? null],
queryFn: () => api.library.series(token!, collectionId, opts?.provider),
enabled: !!token && (opts?.enabled ?? true),
staleTime: STALE,
});
}
/** List available genres, optionally scoped to a content type. */
export function useGenres(contentType?: string, opts?: { enabled?: boolean }) {
/** List available genres, optionally scoped to a content type and provider. */
export function useGenres(contentType?: string, opts?: { enabled?: boolean; provider?: string }) {
const { token } = useAuthContext();
return useQuery({
queryKey: ["library", "genres", contentType ?? null],
queryFn: () => api.library.genres(token!, contentType),
queryKey: ["library", "genres", contentType ?? null, opts?.provider ?? null],
queryFn: () => api.library.genres(token!, contentType, opts?.provider),
enabled: !!token && (opts?.enabled ?? true),
staleTime: STALE,
});
@@ -64,11 +64,12 @@ export function useLibraryItems(
filter: Pick<MediaFilter, "content_type" | "series_names" | "collections" | "search_term" | "genres"> | null,
enabled: boolean,
strategy?: string,
provider?: string,
) {
const { token } = useAuthContext();
return useQuery({
queryKey: ["library", "items", filter, strategy ?? null],
queryFn: () => api.library.items(token!, filter!, 30, strategy),
queryKey: ["library", "items", filter, strategy ?? null, provider ?? null],
queryFn: () => api.library.items(token!, filter!, 30, strategy, provider),
enabled: !!token && enabled && !!filter,
staleTime: 2 * 60 * 1000,
});