feat: add local files provider with indexing and rescan functionality
- Implemented LocalFilesProvider to manage local video files. - Added LocalIndex for in-memory and SQLite-backed indexing of video files. - Introduced scanning functionality to detect video files and extract metadata. - Added API endpoints for listing collections, genres, and series based on provider capabilities. - Enhanced existing routes to check for provider capabilities before processing requests. - Updated frontend to utilize provider capabilities for conditional rendering of UI elements. - Implemented rescan functionality to refresh the local files index. - Added database migration for local files index schema.
This commit is contained in:
@@ -19,6 +19,7 @@ import type {
|
||||
FillStrategy,
|
||||
ContentType,
|
||||
MediaFilter,
|
||||
ProviderCapabilities,
|
||||
RecyclePolicy,
|
||||
} from "@/lib/types";
|
||||
|
||||
@@ -238,6 +239,7 @@ interface AlgorithmicFilterEditorProps {
|
||||
errors: FieldErrors;
|
||||
setFilter: (patch: Partial<MediaFilter>) => void;
|
||||
setStrategy: (strategy: FillStrategy) => void;
|
||||
capabilities?: ProviderCapabilities;
|
||||
}
|
||||
|
||||
function AlgorithmicFilterEditor({
|
||||
@@ -246,14 +248,23 @@ function AlgorithmicFilterEditor({
|
||||
errors,
|
||||
setFilter,
|
||||
setStrategy,
|
||||
capabilities,
|
||||
}: AlgorithmicFilterEditorProps) {
|
||||
const [showGenres, setShowGenres] = useState(false);
|
||||
|
||||
const { data: collections, isLoading: loadingCollections } = useCollections();
|
||||
const { data: series, isLoading: loadingSeries } = useSeries();
|
||||
const { data: genreOptions } = useGenres(content.filter.content_type ?? undefined);
|
||||
const { data: series, isLoading: loadingSeries } = useSeries(undefined, {
|
||||
enabled: capabilities?.series !== false,
|
||||
});
|
||||
const { data: genreOptions } = useGenres(content.filter.content_type ?? undefined, {
|
||||
enabled: capabilities?.genres !== false,
|
||||
});
|
||||
|
||||
const isEpisode = content.filter.content_type === "episode";
|
||||
const collectionLabel =
|
||||
capabilities?.collections && !capabilities?.series && !capabilities?.genres
|
||||
? "Directory"
|
||||
: "Library";
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-md border border-zinc-700/50 bg-zinc-800 p-3">
|
||||
@@ -289,8 +300,8 @@ function AlgorithmicFilterEditor({
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{/* Series — only meaningful for episodes */}
|
||||
{isEpisode && (
|
||||
{/* Series — only meaningful for episodes when provider supports it */}
|
||||
{isEpisode && capabilities?.series !== false && (
|
||||
<Field
|
||||
label="Series"
|
||||
hint={
|
||||
@@ -308,15 +319,15 @@ function AlgorithmicFilterEditor({
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{/* Library — real collection names when the provider supports it */}
|
||||
{/* Library/Directory — real collection names when the provider supports it */}
|
||||
<Field
|
||||
label="Library"
|
||||
label={collectionLabel}
|
||||
hint={
|
||||
loadingCollections
|
||||
? "Loading libraries…"
|
||||
? `Loading ${collectionLabel.toLowerCase()}s…`
|
||||
: collections
|
||||
? "Scope this block to one library"
|
||||
: "Enter a provider library ID"
|
||||
? `Scope this block to one ${collectionLabel.toLowerCase()}`
|
||||
: `Enter a provider ${collectionLabel.toLowerCase()} ID`
|
||||
}
|
||||
>
|
||||
{collections && collections.length > 0 ? (
|
||||
@@ -341,7 +352,8 @@ function AlgorithmicFilterEditor({
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{/* Genres with browse-from-library shortcut */}
|
||||
{/* Genres — only shown when provider supports it */}
|
||||
{capabilities?.genres !== false && (
|
||||
<Field label="Genres" hint="Press Enter or comma to add">
|
||||
<TagInput
|
||||
values={content.filter.genres}
|
||||
@@ -376,6 +388,7 @@ function AlgorithmicFilterEditor({
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Tags" hint="Press Enter or comma to add">
|
||||
<TagInput
|
||||
@@ -435,9 +448,10 @@ interface BlockEditorProps {
|
||||
onChange: (block: ProgrammingBlock) => void;
|
||||
onRemove: () => void;
|
||||
onSelect: () => void;
|
||||
capabilities?: ProviderCapabilities;
|
||||
}
|
||||
|
||||
function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemove, onSelect }: BlockEditorProps) {
|
||||
function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemove, onSelect, capabilities }: BlockEditorProps) {
|
||||
const [expanded, setExpanded] = useState(isSelected);
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -555,6 +569,7 @@ function BlockEditor({ block, index, isSelected, color, errors, onChange, onRemo
|
||||
errors={errors}
|
||||
setFilter={setFilter}
|
||||
setStrategy={setStrategy}
|
||||
capabilities={capabilities}
|
||||
/>
|
||||
|
||||
{content.strategy === "sequential" && (
|
||||
@@ -719,6 +734,7 @@ interface EditChannelSheetProps {
|
||||
) => void;
|
||||
isPending: boolean;
|
||||
error?: string | null;
|
||||
capabilities?: ProviderCapabilities;
|
||||
}
|
||||
|
||||
export function EditChannelSheet({
|
||||
@@ -728,6 +744,7 @@ export function EditChannelSheet({
|
||||
onSubmit,
|
||||
isPending,
|
||||
error,
|
||||
capabilities,
|
||||
}: EditChannelSheetProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
@@ -1027,6 +1044,7 @@ export function EditChannelSheet({
|
||||
onChange={(b) => updateBlock(idx, b)}
|
||||
onRemove={() => removeBlock(idx)}
|
||||
onSelect={() => setSelectedBlockId(block.id)}
|
||||
capabilities={capabilities}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user