Speed up library search on large libraries

search_library_courses was walking via list_library_directory, which
computes a full recursive file count (rglob) and thumbnail lookup for
every course at every level - turning a search into an O(every file in
every course) scan regardless of how few results actually match. Give
search its own lightweight directory-only walk and defer the expensive
per-course lookups until after a name match is confirmed, so cost now
scales with matches found rather than total library size.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 09:22:54 -04:00
co-authored by Claude Sonnet 5
parent 38963b1d55
commit dbf9528fea
+41 -12
View File
@@ -735,23 +735,52 @@ def get_library_root() -> str:
def search_library_courses(dir_path: str, query: str) -> List[Dict[str, Any]]: def search_library_courses(dir_path: str, query: str) -> List[Dict[str, Any]]:
""" """
Recursively search the library for courses whose name contains `query` Recursively search the library for courses whose name contains `query`
(case-insensitive). Walks via list_library_directory, so it reuses the (case-insensitive), respecting hidden paths exactly like normal browsing.
exact same course/directory detection and hidden-path filtering as
normal browsing - a hidden course or folder never shows up in results. Deliberately doesn't reuse list_library_directory for the walk itself:
that function computes media_count (a full recursive file count via
rglob) and thumbnail presence for *every* course at each level, which is
fine for showing one directory's worth of courses but turns a full-
library search into an O(every file in every course) scan. Here, the
directory walk only touches directory entries (cheap - iterdir, no file
stats), and the expensive per-course lookups only run for the handful of
courses whose name actually matches.
""" """
query_lower = query.lower() query_lower = query.lower()
hidden_set = set(get_hidden_paths())
results: List[Dict[str, Any]] = [] results: List[Dict[str, Any]] = []
def walk(path: str): def walk(directory: Path):
level = list_library_directory(path) try:
for item in level['items']: entries = sorted(
if item['type'] == 'course': (p for p in directory.iterdir() if p.is_dir() and not p.name.startswith('.')),
if query_lower in item['name'].lower(): key=lambda p: p.name.lower()
results.append(item) )
else: except (PermissionError, OSError):
walk(item['path']) return
walk(dir_path) for entry in entries:
if os.path.abspath(str(entry)) in hidden_set:
continue
if _looks_like_course(entry):
if query_lower in entry.name.lower():
media_count = len([
f for f in entry.rglob('*')
if f.is_file() and f.suffix.lower() in VIDEO_EXTENSIONS | AUDIO_EXTENSIONS
])
results.append({
'type': 'course',
'name': entry.name,
'path': str(entry),
'media_files': media_count,
'hidden': False,
'has_thumbnail': find_course_thumbnail(str(entry)) is not None
})
else:
walk(entry)
walk(Path(dir_path))
return results return results