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
+16 -20
View File
@@ -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
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')