Summary
When an audiobook has no BasePath, a library scan attributes every audio file by that author to it, and then stores the author folder as the book's BasePath.
Because BasePath is what rename and move operate on, a book in this state has an entire author's shelf as its folder.
Cause
ScanFileDiscovery.Matches accepts "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 an {Author}/... layout — which is most of them — every file beneath an author's folder contains that author's name. So the author clause is true for every one of them.
FindMatchingAudioFiles groups candidates by directory and takes all files in any group where any file matches, so each of the author's book folders is claimed whole. ScanPathPlanner.CalculateBasePath then reduces the result to its common parent — the author folder.
The author identifies a shelf, not a book.
How it is reached
ScanJobProcessor uses audiobook.BasePath as the scan root when it is set. When it is empty, the root falls back to the configured OutputPath — the whole library — and every audio file under it becomes a candidate.
A BasePath can be empty in normal use: it is only assigned on add when a destination path is supplied, and ScanJobProcessor explicitly sets audiobook.BasePath = null when the stored folder no longer exists. The next scan of that book then runs against the whole library.
Reproduction
A conventional layout, three public-domain books by one author:
{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 (no BasePath, so the scan root is the library root):
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
ScanFileDiscovery.FindMatchingAudioFiles is a static taking a root path and an Audiobook, so this reproduces in a unit test against a temp directory — no database or container needed.
Suggested fix
Only the title can attribute a file to a specific book. Author-in-path should not produce a match on its own.
A path carrying neither the title nor anything else identifying is then simply not attributable by path, and is left unmatched rather than attached to an arbitrary book by the same author. That is the right outcome: a miss is recoverable, a wrong link is not — and the user has no reason to go looking for it.
This does mean 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. Those are exactly what an embedded-tag pass should claim, which is what #688 proposes — the two changes complement each other.
Notes
No existing test depends on author-only matching; the full suite passes unchanged with the author clause removed.
Books cited are public domain with real Audible editions, so the case is reproducible by anyone.
Summary
When an audiobook has no
BasePath, a library scan attributes every audio file by that author to it, and then stores the author folder as the book'sBasePath.Because
BasePathis what rename and move operate on, a book in this state has an entire author's shelf as its folder.Cause
ScanFileDiscovery.Matchesaccepts "the path contains the author" as sufficient to attribute a file to a specific audiobook:In an
{Author}/...layout — which is most of them — every file beneath an author's folder contains that author's name. So the author clause is true for every one of them.FindMatchingAudioFilesgroups candidates by directory and takes all files in any group where any file matches, so each of the author's book folders is claimed whole.ScanPathPlanner.CalculateBasePaththen reduces the result to its common parent — the author folder.The author identifies a shelf, not a book.
How it is reached
ScanJobProcessorusesaudiobook.BasePathas the scan root when it is set. When it is empty, the root falls back to the configuredOutputPath— the whole library — and every audio file under it becomes a candidate.A
BasePathcan be empty in normal use: it is only assigned on add when a destination path is supplied, andScanJobProcessorexplicitly setsaudiobook.BasePath = nullwhen the stored folder no longer exists. The next scan of that book then runs against the whole library.Reproduction
A conventional layout, three public-domain books by one author:
Scanning the audiobook She (no
BasePath, so the scan root is the library root):ScanFileDiscovery.FindMatchingAudioFilesis a static taking a root path and anAudiobook, so this reproduces in a unit test against a temp directory — no database or container needed.Suggested fix
Only the title can attribute a file to a specific book. Author-in-path should not produce a match on its own.
A path carrying neither the title nor anything else identifying is then simply not attributable by path, and is left unmatched rather than attached to an arbitrary book by the same author. That is the right outcome: a miss is recoverable, a wrong link is not — and the user has no reason to go looking for it.
This does mean 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. Those are exactly what an embedded-tag pass should claim, which is what #688 proposes — the two changes complement each other.
Notes
No existing test depends on author-only matching; the full suite passes unchanged with the author clause removed.
Books cited are public domain with real Audible editions, so the case is reproducible by anyone.