diff --git a/scripts/media-audiobooks.py b/scripts/media-audiobooks.py index bc7e040..3994137 100755 --- a/scripts/media-audiobooks.py +++ b/scripts/media-audiobooks.py @@ -35,11 +35,11 @@ by what the rows actually hold: rest. One rule that always resolves beats a shelf that is half author and half narrator; the author stays visible in the book folder's own name, which is the title Plex shows. - * Nothing is guessed at. A chapter row with no book or no narrator parks — and so - does one whose names are a tag-scan placeholder (`[Unknown Album]`), which - unrelated books share: filing on it would merge them onto one shelf. An audio - file Plex never matched parks, a video container parks, and a loose file in a - folder holding two books parks: nothing says which book owns it. + * Nothing is guessed at. A chapter row with no book or no narrator parks — a + tag-scan placeholder (`[Unknown Album]`) names neither, since unrelated books + share it and filing on one merges them, and so does a chapter number the ripper + mistagged as the artist (`0.4`). An audio file Plex never matched parks, a + video container parks, and a loose file in a folder holding two books parks. Nothing is deleted and nothing is overwritten. The output is a list of rename(2) intents for `scripts/media-apply.py`, whose preflight refuses any target that is @@ -75,15 +75,21 @@ PLACEHOLDER_NARRATOR = {"[unknown artist]", "unknown artist", "unknown author", "", "[unknown]", "unknown"} -def identity(value, placeholders): +def identity(value, placeholders, needs_letter=False): """The tag value Plex matched, when it names something; '' when it does not. A trailing parenthesized group is stripped for the test only, because rippers glue timestamps to the placeholder (`Unknown album (03/03/2015 21:39:55)`). + + `needs_letter` is for a NARRATOR alone: a shelf is a person's name, and 272 + chapters here carry a chapter number in the artist tag (`0.4`, `31`). The same + test must never judge a BOOK title, which may honestly be `1984`. """ v = (value or "").strip() bare = re.sub(r"\s*\([^()]*\)\s*$", "", v).strip().casefold() - return "" if bare in placeholders else v + if bare in placeholders or (needs_letter and not any(c.isalpha() for c in v)): + return "" + return v # The characters Plex documents as breaking playback. They are dropped, not @@ -197,7 +203,7 @@ def group_books(): d = os.path.dirname(path) AUDIO_BY_DIR[d].add(path) CHILD_OF[os.path.dirname(d)].add(d) # so a cover above Part 1/Part 2 sees both - narrator = identity(r["narrator"], PLACEHOLDER_NARRATOR) + narrator = identity(r["narrator"], PLACEHOLDER_NARRATOR, needs_letter=True) book = identity(r["book"], PLACEHOLDER_BOOK) if not book: park(path, "a chapter row that names no book — Plex's album title is a " @@ -205,9 +211,9 @@ def group_books(): "merge unrelated books onto one shelf") continue if not narrator: - park(path, "a chapter row that names no narrator — the album's artist " - "row is a placeholder like [Unknown Artist], so no shelf " - "owns it") + park(path, "a chapter row that names no narrator — the artist row is a " + "placeholder like [Unknown Artist] or a chapter number a " + "ripper mistagged as the artist, so no shelf owns it") continue key = (narrator.casefold(), book.casefold(), (r["year"] or "").strip()) g = BOOKS.setdefault(key, {"narrators": [], "books": [], "year": key[2], diff --git a/serverAppConfigs-docs/Dankserver/Media Relayout.md b/serverAppConfigs-docs/Dankserver/Media Relayout.md index ae29bc5..2fb2ba3 100644 --- a/serverAppConfigs-docs/Dankserver/Media Relayout.md +++ b/serverAppConfigs-docs/Dankserver/Media Relayout.md @@ -352,9 +352,9 @@ Two laws are forced by what the rows actually hold: What it refuses, the music arm's list with one addition: a video container, audio no chapter row names, and a loose file in a folder holding two books all park under `_unsorted` with the path they have today — and so does a chapter whose row names -**no book or no narrator**, placeholder included (`[Unknown Album]`, `no title`, -`[Unknown Artist]`): unrelated books share those strings, so filing on one merges -them. `Various Artists` is not a placeholder — that is a multi-cast read. +**no book or no narrator**: a placeholder (`[Unknown Album]`, `[Unknown Artist]`) +that unrelated books share, or a chapter number mistagged as the artist (`0.4`). +`Various Artists` is not a placeholder — that is a multi-cast read. ## Laws diff --git a/tests/media_audiobooks.bats b/tests/media_audiobooks.bats index d27b1b9..6f50db0 100755 --- a/tests/media_audiobooks.bats +++ b/tests/media_audiobooks.bats @@ -142,6 +142,10 @@ setup() { row "$H/Rip/tape.mp3" "Ann Reader" "Unknown album (03/03/2015 21:39:55)" "" 1 "One" row "$H/NoWho/Real Book.mp3" "[Unknown Artist]" "A Named Book" 2001 1 "One" row "$H/Multicast/Genesis.mp3" "Various Artists" "The Bible" "" 1 "Genesis" + at "$H/Numbered/01 - Grimoire.mp3" 9000 + at "$H/Digits/1984.mp3" 9000 + row "$H/Numbered/01 - Grimoire.mp3" "0.4" "Grimoire of the Lamb" "" 1 "One" + row "$H/Digits/1984.mp3" "Ann Reader" "1984" 1949 1 "One" # The enum TSV is what scripts/media-census.py enumerate writes: bytes, ext, # path, no header. @@ -358,6 +362,16 @@ PY "$AB/Various Artists/The Bible/Genesis.mp3" ] } +@test "a chapter number mistagged as the narrator parks" { + plan + [ "$(kind "$H/Numbered/01 - Grimoire.mp3")" = "park" ] +} + +@test "a book whose title is digits still files, because only a name must have a letter" { + plan + [ "$(dst "$H/Digits/1984.mp3")" = "$AB/Ann Reader/1984 (1949)/1984.mp3" ] +} + @test "the row, not the extension, decides what a chapter is" { plan [ "$(kind "$G/NoExt/part01")" = "chapter" ]