From 38651241d7d6a13bac9a53ee3e60bd20024cf05f Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 14 Feb 2026 05:03:04 +0000 Subject: [PATCH] Scanner improvements. --- frontend/src/App.vue | 21 ++++++++++++++- mediahive/hivescan/scanner.py | 49 ++++++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 59d9ab7..22805a0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -764,7 +764,7 @@ function formatMatchedPeople(people: PersonMatch[]): MatchedPerson[] { let searchTimeout: ReturnType | null = null; const MAX_RESULTS = 100; -// Watch for detail page entry to focus the first interactive element +// Watch for detail page entry/exit to manage focus watch(selectedItem, (item, oldItem) => { if (item && !oldItem) { // Skip auto-focus if we have a specific episode to focus on (from search) @@ -773,6 +773,25 @@ watch(selectedItem, (item, oldItem) => { } // Entering detail page - focus Play button (row 2, col 0) after transition focusAt(2, 0, 150); + } else if (!item && oldItem) { + // Leaving detail page (browser back, Escape, etc.) - restore focus to the item card + const page = currentView.value === 'series' ? 'series' : 'movies'; + restoreFocusForPage(page); + } +}); + +// Focus search input by default on initial movies page load +let initialFocusDone = false; +watch([mediaIndex, currentView, searchQuery, selectedItem], ([index, view, query, item]) => { + if (!initialFocusDone && index && view === 'movies' && !query && !item) { + initialFocusDone = true; + // Focus search input on first movies page load + setTimeout(() => { + const searchInput = document.querySelector('.search-input') as HTMLInputElement; + if (searchInput) { + searchInput.focus(); + } + }, 100); } }); diff --git a/mediahive/hivescan/scanner.py b/mediahive/hivescan/scanner.py index 43ca2e5..92c6657 100644 --- a/mediahive/hivescan/scanner.py +++ b/mediahive/hivescan/scanner.py @@ -174,15 +174,30 @@ async def _discover_downloads(task_id: str) -> List[ParsedContent]: ) ) + # Media container folder names (indicates the parent is a single media item) + MEDIA_CONTAINER_DIRS = {"BDMV", "VIDEO_TS", "HVDVD_TS"} + VIDEO_EXTENSIONS = { + ".mkv", + ".mp4", + ".avi", + ".m4v", + ".mov", + ".wmv", + ".flv", + ".webm", + ".ts", + ".m2ts", + } + async def _walk(directory: Path) -> None: nonlocal dirs_visited ap = AsyncPath(directory) if not await ap.is_dir(): return - has_child_dirs = False child_dirs: list[Path] = [] child_files: list[Path] = [] + is_media_container = False try: for item_async in ap.iterdir(): @@ -193,7 +208,9 @@ async def _discover_downloads(task_id: str) -> List[ParsedContent]: continue if await AsyncPath(item).is_dir(): - has_child_dirs = True + # Check if this is a BluRay/DVD structure + if item.name.upper() in MEDIA_CONTAINER_DIRS: + is_media_container = True child_dirs.append(item) else: child_files.append(item) @@ -201,7 +218,20 @@ async def _discover_downloads(task_id: str) -> List[ParsedContent]: logger.debug("Cannot list directory: %s", directory) return - if has_child_dirs: + # If directory contains BDMV/VIDEO_TS, treat entire directory as a single download + if is_media_container: + relpath = make_relative_path(str(directory), media_root_str) + try: + stat_info = await ap.stat() + mtime = int(stat_info.st_mtime) + except OSError: + return + if relpath not in _seen_mtimes or _seen_mtimes[relpath] != mtime: + _seen_mtimes[relpath] = mtime + downloads.append(await parse_download(directory)) + return + + if child_dirs: # Branch directory — log it and recurse into subdirectories dirs_visited += 1 rel = make_relative_path(str(directory), media_root_str) or str(directory) @@ -212,6 +242,19 @@ async def _discover_downloads(task_id: str) -> List[ParsedContent]: await _walk(child) # Yield control periodically so WS messages flush await asyncio.sleep(0) + # Also process any video files directly in this directory + for child_file in child_files: + if child_file.suffix.lower() in VIDEO_EXTENSIONS: + relpath = make_relative_path(str(child_file), media_root_str) + try: + stat_info = await AsyncPath(child_file).stat() + mtime = int(stat_info.st_mtime) + except OSError: + continue + if relpath in _seen_mtimes and _seen_mtimes[relpath] == mtime: + continue + _seen_mtimes[relpath] = mtime + downloads.append(await parse_download(child_file)) else: # Leaf directory — treat the directory itself as a download relpath = make_relative_path(str(directory), media_root_str)