Skip to content

fix(scan): don't attribute every book by an author to whichever one is scanned#766

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/765-scan-author-overmatch
Open

fix(scan): don't attribute every book by an author to whichever one is scanned#766
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/765-scan-author-overmatch

Conversation

@m4bard

@m4bard m4bard commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Fixes #765.

ScanFileDiscovery.Matches accepted "the path contains the author" as sufficient to attribute a file to a specific audiobook:

var filePathMatchesAuthor = !string.IsNullOrEmpty(authorToken)
    && file.Contains(authorToken, StringComparison.OrdinalIgnoreCase);

return fileNameMatchesTitle || filePathMatchesAuthor || directoryMatchesTitle;

In the usual {Author}/... layout, every file beneath an author's folder contains that author's name — so the clause is true for all of them. FindMatchingAudioFiles takes all files in any directory group where any file matches, so each of the author's book folders is claimed whole, and ScanPathPlanner.CalculateBasePath reduces the result to its common parent: the author folder. That is then stored as the audiobook's BasePath — which is what rename and move operate on.

Reached whenever an audiobook has no BasePath, because the scan root then falls back to the configured OutputPath and the whole library becomes candidates. A BasePath is empty in normal use: it's only assigned on add when a destination path is supplied, and ScanJobProcessor sets it to null when the stored folder no longer exists.

Reproduction

Three public-domain books by one author, conventional layout:

{root}/H. Rider Haggard/H. Rider Haggard - She/She.m4b
{root}/H. Rider Haggard/H. Rider Haggard - Allan Quatermain/Allan Quatermain.m4b
{root}/H. Rider Haggard/H. Rider Haggard - King Solomon's Mines/King Solomon's Mines.m4b

Scanning the audiobook She:

expected: ["She.m4b"]
actual:   ["Allan Quatermain.m4b", "King Solomon's Mines.m4b", "She.m4b"]

BasePath expected: .../H. Rider Haggard - She
BasePath actual:   .../H. Rider Haggard          <-- the author folder

Changes

Changed

  • ScanFileDiscovery.Matches now attributes a file to an audiobook only when the title appears — in the filename or in the containing directory. The author is no longer a match on its own.

Fixed

  • A scan no longer claims every book by an author for whichever one is being scanned.
  • An audiobook's BasePath is no longer collapsed to its author's folder.

Testing

ScanFileDiscoveryReproTests — 7 new cases against real temp directory trees (FindMatchingAudioFiles is a static taking a root path and an Audiobook, so this needs no database or container):

  • scanning one book does not claim the other books by the same author
  • BasePath resolves to the book folder, not the author folder
  • title in the folder name still matches (the common {Author}/{Author} - {Series} - {Title} shape)
  • title in the filename still matches when the folder does not carry it
  • every file in a matched book folder is still claimed (multi-part books whose part filenames don't repeat the title)
  • a sibling book in the same series folder is not claimed
  • a path carrying neither title nor anything identifying is left unmatched

dotnet test: 1196 passing, 0 failing (1189 existing + 7 new). No existing test changed — nothing in the suite depended on author-only matching.

Notes

The trade-off, stated plainly: layouts that encode neither the title nor the author in the path — AudioBookShelf-style series folders with numbered episode files — go from wrongly matched to unmatched.

That is the right direction. A miss is recoverable and visible to the user; a wrong link is neither, because the file is silently attached to a book the user has no reason to go and check. And those layouts are exactly what an embedded-tag pass should claim, which is what #688 proposes — the two changes complement each other rather than overlap.

Books cited are public domain with real Audible editions, so the case is reproducible by anyone.

…s scanned

ScanFileDiscovery.Matches accepted "the path contains the author" as
sufficient to attribute a file to a specific audiobook. In the usual
{Author}/... layout every file beneath an author's folder contains that
author's name, so the clause was true for all of them.

FindMatchingAudioFiles takes every file in any directory group where any
file matches, so each of the author's book folders was claimed whole, and
ScanPathPlanner.CalculateBasePath then reduced the result to its common
parent -- the author folder. That became the audiobook's BasePath, which is
what rename and move operate on.

This is reached whenever an audiobook has no BasePath, because the scan root
then falls back to the configured OutputPath and the whole library becomes
candidates. A BasePath is empty in normal use: it is only assigned on add
when a destination path is supplied, and ScanJobProcessor sets it to null
when the stored folder no longer exists.

The author names a shelf, not a book. Attribution by path now requires the
title, in the filename or in the containing directory. A path carrying
neither the title nor anything else identifying is left unmatched rather
than attached to an arbitrary book by the same author -- a miss is
recoverable and visible, a wrong link is neither.

Layouts that encode neither title nor author in the path go from wrongly
matched to unmatched. Those are what an embedded-tag pass should claim.

No existing test depended on author-only matching.

Fixes Listenarrs#765
@m4bard
m4bard requested a review from a team July 14, 2026 20:06
@m4bard

m4bard commented Jul 15, 2026

Copy link
Copy Markdown
Author

Followed up with a runtime before/after against the published image, since the unit tests above only cover the matcher in isolation.

Setup: a synthetic library generated from public-domain metadata in the common {Author}/{Author} - {Series} - {Title} layout, with each book's files multiplied out (20 copies × 40 chapters = 800 files/book) to give the scan something to over-claim. Books are added with BasePath cleared — the state where the scan root falls back to the whole library — then scanned. Linked files are compared to a manifest of each file's true owner. Same library, two images: ghcr.io/listenarrs/listenarr:canary vs a build of this branch.

Each scanned book has a same-author sibling elsewhere in the library, so the author-in-path clause has something to wrongly grab:

scan of :canary this branch
The Valley of Fear (Doyle) 1,600 files across 2 books — claimed a Doyle sibling 800 files, 1 book — only its own
Stalky and Co. (Kipling) 1,554 files across 2 books — claimed a Kipling sibling 745 files, 1 book — only its own

So the over-attribution is present on canary and gone on the branch. One honest caveat: on the branch, Stalky and Co. links 745 of its own 800 files — a slight under-match on that period-titled book. That's a separate matching-completeness question (title-token matching on an unusual title), not something this change introduced — canary over-links it rather than under-linking, so there was no clean baseline to regress from. Worth a separate look, but orthogonal to the over-match this PR fixes.

One thing that surprised me: the stored BasePath string ends up as the author folder in both runs, so reading BasePath alone doesn't reveal the bug — it's only visible in which files get linked. That's why this is measured against true ownership rather than by inspecting BasePath.

The harness is a small generator + scan-diff script; happy to open-source it if that'd be useful for regression-testing scan behaviour more generally.

therobbiedavis added a commit that referenced this pull request Jul 20, 2026
Adapt the seven path-attribution scenarios from #766 to the current
ScanFileDiscovery.Discover API and boundary-aware BasePath planning.

Co-authored-by: m4bard <[email protected]>
kevinheneveld added a commit to kevinheneveld/Listenarr that referenced this pull request Jul 21, 2026
… whole shelf to the scanned book

Cherry-pick of m4bard's fix (Listenarrs#766, slated for
upstream via Listenarrs#717): ScanFileDiscovery.Matches treated 'path contains
the author' as attribution to a SPECIFIC audiobook, which is true for
every file under the author's folder in the {Author}/... layout — one
book's scan could swallow its shelf-mates' files and re-root BasePath
at the author folder. Only the title attributes by path now; repro
tests included from the upstream PR.

Live-relevant: every organize/move enqueues a post-move scan, and ~90
descend-move scans ran on 2026-07-13. Likely origin of multi-book
records (Dark Journey holding 'The Shadow'/'Journey of Fate' files).

Co-authored-by: m4bard <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Library scan attributes every book by an author to whichever one is scanned, and sets the author folder as its BasePath

1 participant