From dbf9528fea12961567d6d674d8be334c259eada4 Mon Sep 17 00:00:00 2001 From: rmsitz Date: Sat, 22 Aug 2026 09:22:54 -0400 Subject: [PATCH] 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 --- offlineu_core.py | 53 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/offlineu_core.py b/offlineu_core.py index e0c0b74..13aa3fb 100644 --- a/offlineu_core.py +++ b/offlineu_core.py @@ -735,23 +735,52 @@ def get_library_root() -> str: def search_library_courses(dir_path: str, query: str) -> List[Dict[str, Any]]: """ Recursively search the library for courses whose name contains `query` - (case-insensitive). Walks via list_library_directory, so it reuses the - exact same course/directory detection and hidden-path filtering as - normal browsing - a hidden course or folder never shows up in results. + (case-insensitive), respecting hidden paths exactly like normal browsing. + + 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() + hidden_set = set(get_hidden_paths()) results: List[Dict[str, Any]] = [] - def walk(path: str): - level = list_library_directory(path) - for item in level['items']: - if item['type'] == 'course': - if query_lower in item['name'].lower(): - results.append(item) - else: - walk(item['path']) + def walk(directory: Path): + try: + entries = sorted( + (p for p in directory.iterdir() if p.is_dir() and not p.name.startswith('.')), + key=lambda p: p.name.lower() + ) + except (PermissionError, OSError): + 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