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:
2026-03-14 03:44:32 +01:00
parent 9b6bcfc566
commit 8f42164bce
30 changed files with 1033 additions and 59 deletions

View File

@@ -17,6 +17,8 @@ interface VideoPlayerProps {
/** Active subtitle track index, or -1 to disable. */
subtitleTrack?: number;
muted?: boolean;
/** Force direct-file mode (skips hls.js even for .m3u8 URLs). */
streamingProtocol?: "hls" | "direct_file";
onStreamError?: () => void;
onSubtitleTracksChange?: (tracks: SubtitleTrack[]) => void;
/** Called when the browser blocks autoplay and user interaction is required. */
@@ -34,6 +36,7 @@ const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
initialOffset = 0,
subtitleTrack = -1,
muted = false,
streamingProtocol,
onStreamError,
onSubtitleTracksChange,
onNeedsInteraction,
@@ -75,7 +78,7 @@ const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
onSubtitleTracksChange?.([]);
setIsBuffering(true);
const isHls = src.includes(".m3u8");
const isHls = streamingProtocol !== "direct_file" && src.includes(".m3u8");
if (isHls && Hls.isSupported()) {
const hls = new Hls({
@@ -117,10 +120,18 @@ const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
{ once: true },
);
} else {
// Plain MP4 fallback
// Plain MP4 / direct file: seek to offset after metadata loads.
video.src = src;
video.addEventListener(
"loadedmetadata",
() => {
if (initialOffset > 0) video.currentTime = initialOffset;
video.muted = mutedRef.current;
video.play().catch(() => onNeedsInteraction?.());
},
{ once: true },
);
video.load();
video.play().catch(() => {});
}
return () => {