From 21a468fb7f37ccf756e91408dae63ee898e02599 Mon Sep 17 00:00:00 2001 From: rmsitz Date: Sat, 22 Aug 2026 20:33:02 -0400 Subject: [PATCH] Fix course-detection heuristic swallowing category folders as one course _looks_like_course() treated any folder with 2+ media-holding subfolders as a single multi-section course, with no check that those subfolders actually looked like sections. A category folder holding many unrelated courses (e.g. Claude/, General/) could accidentally satisfy that if several of its courses happened to keep episodes directly at their own top level rather than nested under a chapter folder, collapsing the whole category into one bogus course. Now every media-holding subfolder must read as a chapter/section label (Section 1, Module 2, or a numbered chapter like 01-Introduction) for the parent to count as one course - verified against the real library mount that ChatGPT/Claude/Copilot/General all correctly show as folders again, while individually-structured courses still detect correctly. Co-Authored-By: Claude Sonnet 5 --- offlineu_core.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/offlineu_core.py b/offlineu_core.py index 278bed2..7305fd3 100644 --- a/offlineu_core.py +++ b/offlineu_core.py @@ -526,25 +526,26 @@ def find_course_thumbnail(course_path: str) -> Optional[str]: _SECTION_NAME_RE = re.compile( - r'^(section|module|chapter|part|unit|lesson)\b', re.IGNORECASE + r'^(section|module|chapter|part|unit|lesson)\b|^\d+[\s_.\-]', re.IGNORECASE ) def _looks_like_course(directory: Path) -> bool: """ - Heuristic for 'this folder is a course, stop recursing into it': - it has media files directly, or at least two of its immediate - subfolders do (covers the common Section 1/, Section 2/... layout). + Heuristic for 'this folder is a course, stop recursing into it': it has + media files directly, or its media-holding immediate subfolders all + read as chapter/section labels ("Section 1", "Module 2", "01 - + Introduction", ...) rather than course titles - covers the common + Section 1/, Section 2/... (or numbered-chapter) layout. - Requiring 2+ matching subfolders (rather than just 1) avoids mistaking - a publisher/grouping folder that holds a single course - e.g. - Pluralsight/Docker Deep Dive/lesson.mp4 - for the course itself. - - The one exception: a directory with exactly one media-holding subfolder - whose name reads as a section label ("Section 1", "Module 2", ...) - rather than a course title. That's still a course with just one - section, not a publisher folder - Pluralsight/Docker Deep Dive doesn't - get named "Section 1", so this doesn't reopen the ambiguity above. + Requiring *every* media-holding subfolder to match, not just one, is + what keeps a publisher/category folder holding several unrelated + courses - e.g. Claude/Pluralsight.X.../*.mp4 next to + Claude/Linkedin.Learning.Y.../*.mp4 - from being mistaken for a single + course just because more than one of its subfolders happens to store + episodes directly rather than nested under their own chapter folder. + Real course titles ("Pluralsight.Docker.Deep.Dive...") don't read as + chapter labels, so this doesn't reopen that ambiguity. """ if _has_direct_media(directory): return True @@ -556,14 +557,9 @@ def _looks_like_course(directory: Path) -> bool: except (PermissionError, OSError): return False children_with_media = [child for child in children if _has_direct_media(child)] - if len(children_with_media) >= 2: - return True - if ( - len(children_with_media) == 1 - and _SECTION_NAME_RE.match(children_with_media[0].name.strip()) - ): - return True - return False + if not children_with_media: + return False + return all(_SECTION_NAME_RE.match(child.name.strip()) for child in children_with_media) HIDDEN_PATHS_FILE = os.path.join(DATA_DIR, 'hidden_paths.json')