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:
@@ -19,7 +19,7 @@ import type {
|
||||
FillStrategy,
|
||||
ContentType,
|
||||
MediaFilter,
|
||||
ProviderCapabilities,
|
||||
ProviderInfo,
|
||||
RecyclePolicy,
|
||||
} from "@/lib/types";
|
||||
|
||||
@@ -57,10 +57,12 @@ const blockSchema = z.object({
|
||||
type: z.literal("algorithmic"),
|
||||
filter: mediaFilterSchema,
|
||||
strategy: z.enum(["best_fit", "sequential", "random"]),
|
||||
provider_id: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("manual"),
|
||||
items: z.array(z.string()),
|
||||
provider_id: z.string().optional(),
|
||||
}),
|
||||
]),
|
||||
loop_on_finish: z.boolean().optional(),
|
||||
@@ -239,7 +241,8 @@ interface AlgorithmicFilterEditorProps {
|
||||
errors: FieldErrors;
|
||||
setFilter: (patch: Partial<MediaFilter>) => void;
|
||||
setStrategy: (strategy: FillStrategy) => void;
|
||||
capabilities?: ProviderCapabilities;
|
||||
setProviderId: (id: string) => void;
|
||||
providers: ProviderInfo[];
|
||||
}
|
||||
|
||||
function AlgorithmicFilterEditor({
|
||||
@@ -248,16 +251,23 @@ function AlgorithmicFilterEditor({
|
||||
errors,
|
||||
setFilter,
|
||||
setStrategy,
|
||||
capabilities,
|
||||
setProviderId,
|
||||
providers,
|
||||
}: AlgorithmicFilterEditorProps) {
|
||||
const [showGenres, setShowGenres] = useState(false);
|
||||
|
||||
const { data: collections, isLoading: loadingCollections } = useCollections();
|
||||
const providerId = content.provider_id ?? "";
|
||||
const capabilities = providers.find((p) => p.id === providerId)?.capabilities
|
||||
?? providers[0]?.capabilities;
|
||||
|
||||
const { data: collections, isLoading: loadingCollections } = useCollections(providerId || undefined);
|
||||
const { data: series, isLoading: loadingSeries } = useSeries(undefined, {
|
||||
enabled: capabilities?.series !== false,
|
||||
provider: providerId || undefined,
|
||||
});
|
||||
const { data: genreOptions } = useGenres(content.filter.content_type ?? undefined, {
|
||||
enabled: capabilities?.genres !== false,
|
||||
provider: providerId || undefined,
|
||||
});
|
||||
|
||||
const isEpisode = content.filter.content_type === "episode";
|
||||
@@ -270,6 +280,16 @@ function AlgorithmicFilterEditor({
|
||||
<div className="space-y-3 rounded-md border border-zinc-700/50 bg-zinc-800 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">Filter</p>
|
||||
|
||||
{providers.length > 1 && (
|
||||
<Field label="Provider">
|
||||
<NativeSelect value={providerId} onChange={(v) => setProviderId(v)}>
|
||||
{providers.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.id}</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Media type">
|
||||
<NativeSelect
|
||||
@@ -430,7 +450,7 @@ function AlgorithmicFilterEditor({
|
||||
</div>
|
||||
|
||||
{/* Preview — snapshot of current filter+strategy, only fetches on explicit click */}
|
||||
<FilterPreview filter={content.filter} strategy={content.strategy} />
|
||||
<FilterPreview filter={content.filter} strategy={content.strategy} provider={providerId || undefined} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -448,10 +468,10 @@ interface BlockEditorProps {
|
||||
onChange: (block: ProgrammingBlock) => void;
|
||||
onRemove: () => void;
|
||||
onSelect: () => void;
|
||||
capabilities?: ProviderCapabilities;
|
||||
providers: ProviderInfo[];
|
||||
}
|
||||
|
||||
function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemove, onSelect, capabilities }: BlockEditorProps) {
|
||||
function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemove, onSelect, providers }: BlockEditorProps) {
|
||||
const [expanded, setExpanded] = useState(isSelected);
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -470,12 +490,13 @@ function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemo
|
||||
const pfx = `blocks.${index}`;
|
||||
|
||||
const setContentType = (type: "algorithmic" | "manual") => {
|
||||
const pid = content.provider_id ?? "";
|
||||
onChange({
|
||||
...block,
|
||||
content:
|
||||
type === "algorithmic"
|
||||
? { type: "algorithmic", filter: defaultFilter(), strategy: "random" }
|
||||
: { type: "manual", items: [] },
|
||||
? { type: "algorithmic", filter: defaultFilter(), strategy: "random", provider_id: pid }
|
||||
: { type: "manual", items: [], provider_id: pid },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -489,6 +510,10 @@ function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemo
|
||||
onChange({ ...block, content: { ...content, strategy } });
|
||||
};
|
||||
|
||||
const setProviderId = (id: string) => {
|
||||
onChange({ ...block, content: { ...content, provider_id: id } });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={elRef}
|
||||
@@ -569,7 +594,8 @@ function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemo
|
||||
errors={errors}
|
||||
setFilter={setFilter}
|
||||
setStrategy={setStrategy}
|
||||
capabilities={capabilities}
|
||||
setProviderId={setProviderId}
|
||||
providers={providers}
|
||||
/>
|
||||
|
||||
{content.strategy === "sequential" && (
|
||||
@@ -734,7 +760,7 @@ interface EditChannelSheetProps {
|
||||
) => void;
|
||||
isPending: boolean;
|
||||
error?: string | null;
|
||||
capabilities?: ProviderCapabilities;
|
||||
providers?: ProviderInfo[];
|
||||
}
|
||||
|
||||
export function EditChannelSheet({
|
||||
@@ -744,7 +770,7 @@ export function EditChannelSheet({
|
||||
onSubmit,
|
||||
isPending,
|
||||
error,
|
||||
capabilities,
|
||||
providers = [],
|
||||
}: EditChannelSheetProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
@@ -1044,7 +1070,7 @@ export function EditChannelSheet({
|
||||
onChange={(b) => updateBlock(idx, b)}
|
||||
onRemove={() => removeBlock(idx)}
|
||||
onSelect={() => setSelectedBlockId(block.id)}
|
||||
capabilities={capabilities}
|
||||
providers={providers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { MediaFilter, LibraryItemResponse } from "@/lib/types";
|
||||
interface FilterPreviewProps {
|
||||
filter: MediaFilter;
|
||||
strategy?: string;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
function fmtDuration(secs: number): string {
|
||||
@@ -32,10 +33,10 @@ function ItemRow({ item }: { item: LibraryItemResponse }) {
|
||||
);
|
||||
}
|
||||
|
||||
type Snapshot = { filter: MediaFilter; strategy?: string };
|
||||
type Snapshot = { filter: MediaFilter; strategy?: string; provider?: string };
|
||||
|
||||
export function FilterPreview({ filter, strategy }: FilterPreviewProps) {
|
||||
// Capture both filter and strategy at click time so edits don't silently
|
||||
export function FilterPreview({ filter, strategy, provider }: FilterPreviewProps) {
|
||||
// Capture filter, strategy, and provider at click time so edits don't silently
|
||||
// re-fetch while the user is still configuring the block.
|
||||
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
|
||||
|
||||
@@ -43,14 +44,16 @@ export function FilterPreview({ filter, strategy }: FilterPreviewProps) {
|
||||
snapshot?.filter ?? null,
|
||||
!!snapshot,
|
||||
snapshot?.strategy,
|
||||
snapshot?.provider,
|
||||
);
|
||||
|
||||
const handlePreview = () => setSnapshot({ filter: { ...filter }, strategy });
|
||||
const handlePreview = () => setSnapshot({ filter: { ...filter }, strategy, provider });
|
||||
|
||||
const filterChanged =
|
||||
snapshot !== null &&
|
||||
(JSON.stringify(snapshot.filter) !== JSON.stringify(filter) ||
|
||||
snapshot.strategy !== strategy);
|
||||
snapshot.strategy !== strategy ||
|
||||
snapshot.provider !== provider);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
|
||||
Reference in New Issue
Block a user