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 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 20:33:02 -04:00
co-authored by Claude Sonnet 5
parent 8b365c4463
commit 21a468fb7f
+17 -21
View File
@@ -526,25 +526,26 @@ def find_course_thumbnail(course_path: str) -> Optional[str]:
_SECTION_NAME_RE = re.compile( _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: def _looks_like_course(directory: Path) -> bool:
""" """
Heuristic for 'this folder is a course, stop recursing into it': Heuristic for 'this folder is a course, stop recursing into it': it has
it has media files directly, or at least two of its immediate media files directly, or its media-holding immediate subfolders all
subfolders do (covers the common Section 1/, Section 2/... layout). 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 Requiring *every* media-holding subfolder to match, not just one, is
a publisher/grouping folder that holds a single course - e.g. what keeps a publisher/category folder holding several unrelated
Pluralsight/Docker Deep Dive/lesson.mp4 - for the course itself. courses - e.g. Claude/Pluralsight.X.../*.mp4 next to
Claude/Linkedin.Learning.Y.../*.mp4 - from being mistaken for a single
The one exception: a directory with exactly one media-holding subfolder course just because more than one of its subfolders happens to store
whose name reads as a section label ("Section 1", "Module 2", ...) episodes directly rather than nested under their own chapter folder.
rather than a course title. That's still a course with just one Real course titles ("Pluralsight.Docker.Deep.Dive...") don't read as
section, not a publisher folder - Pluralsight/Docker Deep Dive doesn't chapter labels, so this doesn't reopen that ambiguity.
get named "Section 1", so this doesn't reopen the ambiguity above.
""" """
if _has_direct_media(directory): if _has_direct_media(directory):
return True return True
@@ -556,14 +557,9 @@ def _looks_like_course(directory: Path) -> bool:
except (PermissionError, OSError): except (PermissionError, OSError):
return False return False
children_with_media = [child for child in children if _has_direct_media(child)] children_with_media = [child for child in children if _has_direct_media(child)]
if len(children_with_media) >= 2: if not children_with_media:
return True return False
if ( return all(_SECTION_NAME_RE.match(child.name.strip()) for child in children_with_media)
len(children_with_media) == 1
and _SECTION_NAME_RE.match(children_with_media[0].name.strip())
):
return True
return False
HIDDEN_PATHS_FILE = os.path.join(DATA_DIR, 'hidden_paths.json') HIDDEN_PATHS_FILE = os.path.join(DATA_DIR, 'hidden_paths.json')