On canary a630572e, six places build a book's naming-variable dictionary. Five of them key it with the default, case-sensitive comparer. The token regex in FileNamingService.ApplyNamingPattern carries RegexOptions.IgnoreCase, so {author} matches the regex, arrives at the lookup as "author", misses, takes the not-found path, and the cleanup removes the segment together with its separators. The token renders as nothing.
What a user sees
Set the folder pattern to {author}/{series} and the file pattern to {title}. Rename the library: it works, because RenameService is the one table with StringComparer.OrdinalIgnoreCase. Then let a download complete. It imports as Unknown Title.m4b at the root of the library, because DownloadImportService builds its own table and that one is case-sensitive.
The pattern is valid, the rename proved it, and nothing is logged above a per-file Variable {VariableName} not found in naming pattern warning that reads like ordinary noise.
The six tables
| site |
comparer on a630572e |
listenarr.application/Audiobooks/Renaming/RenameService.Helpers.cs:193 |
OrdinalIgnoreCase |
listenarr.api/Features/Downloads/ManualImportPathPlanner.cs:261 |
default, case-sensitive |
listenarr.application/Common/FileNamingService.Helpers.cs:75 (AudioMetadata) |
default, case-sensitive |
listenarr.application/Common/FileNamingService.Helpers.cs:106 (AudibleBookMetadata) |
default, case-sensitive |
listenarr.application/Downloads/Import/DownloadImportService.cs:305 |
default, case-sensitive |
listenarr.api/Features/Library/LibraryPathPlanner.cs:80 |
default, case-sensitive |
That is six builders with five of them defective. I counted the six by taking every new Dictionary<string, object> that is passed to ApplyNamingPattern; LibraryAddService.Destination.cs:72 passes a metadata object rather than a table of its own, so it routes through the FileNamingService overloads and is not a seventh. This is the same set of six I described on #945, where the question was whether they agree on combining Title and Subtitle. The count there was six builders; the count here is five defective tables, which is those six minus RenameService. Same population, different slice of it. |
|
PR #869 fixes ManualImportPathPlanner, which is what #816 reported. That leaves four, including the import path, which is the one a user is most likely to hit because it runs unattended on every download. |
|
Reproduction
No indexer or download client needed.
- Settings, folder naming pattern
{author}/{title}, file naming pattern {title}.
- Add a book and rename the library. The files land under
Author Name/Book Title/.
- Import a file for that book, manually or by letting a download complete.
- The imported file is named from none of the tokens.
The bug is that the same pattern works at step 2 and fails at step 4.
Fix
StringComparer.OrdinalIgnoreCase on the four remaining dictionaries. Ordinal, not culture-aware: under tr-TR I and i are different letters, so CurrentCultureIgnoreCase would break {TITLE} against the key Title. Readarr's FileNameBuilderTokenEqualityComparer calls the culture-sensitive ToLower() for this and has that trap open; StringComparer.OrdinalIgnoreCase is strictly safer and is what RenameService already uses.
I have this on a branch with twelve test cases, one casing theory per table, three of them through a real import. Eight of the twelve fail with the four comparers reverted. DownloadImportService.cs is at exactly the 500-line architecture cap, so its change is line neutral. Happy to open it as a PR, or to fold it into whatever shape you would rather have; I have kept it off #869 so that one stays matched to the issue it closes.
The longer-term answer is to stop having six copies of one table at all. That crosses an assembly boundary, since ManualImportPathPlanner and LibraryPathPlanner are in listenarr.api and the rest are in listenarr.application, so it is a bigger decision than this one.
Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.
On canary
a630572e, six places build a book's naming-variable dictionary. Five of them key it with the default, case-sensitive comparer. The token regex inFileNamingService.ApplyNamingPatterncarriesRegexOptions.IgnoreCase, so{author}matches the regex, arrives at the lookup as"author", misses, takes the not-found path, and the cleanup removes the segment together with its separators. The token renders as nothing.What a user sees
Set the folder pattern to
{author}/{series}and the file pattern to{title}. Rename the library: it works, becauseRenameServiceis the one table withStringComparer.OrdinalIgnoreCase. Then let a download complete. It imports asUnknown Title.m4bat the root of the library, becauseDownloadImportServicebuilds its own table and that one is case-sensitive.The pattern is valid, the rename proved it, and nothing is logged above a per-file
Variable {VariableName} not found in naming patternwarning that reads like ordinary noise.The six tables
a630572elistenarr.application/Audiobooks/Renaming/RenameService.Helpers.cs:193OrdinalIgnoreCaselistenarr.api/Features/Downloads/ManualImportPathPlanner.cs:261listenarr.application/Common/FileNamingService.Helpers.cs:75(AudioMetadata)listenarr.application/Common/FileNamingService.Helpers.cs:106(AudibleBookMetadata)listenarr.application/Downloads/Import/DownloadImportService.cs:305listenarr.api/Features/Library/LibraryPathPlanner.cs:80new Dictionary<string, object>that is passed toApplyNamingPattern;LibraryAddService.Destination.cs:72passes a metadata object rather than a table of its own, so it routes through theFileNamingServiceoverloads and is not a seventh. This is the same set of six I described on #945, where the question was whether they agree on combining Title and Subtitle. The count there was six builders; the count here is five defective tables, which is those six minusRenameService. Same population, different slice of it.ManualImportPathPlanner, which is what #816 reported. That leaves four, including the import path, which is the one a user is most likely to hit because it runs unattended on every download.Reproduction
No indexer or download client needed.
{author}/{title}, file naming pattern{title}.Author Name/Book Title/.The bug is that the same pattern works at step 2 and fails at step 4.
Fix
StringComparer.OrdinalIgnoreCaseon the four remaining dictionaries.Ordinal, not culture-aware: under tr-TRIandiare different letters, soCurrentCultureIgnoreCasewould break{TITLE}against the keyTitle. Readarr'sFileNameBuilderTokenEqualityComparercalls the culture-sensitiveToLower()for this and has that trap open;StringComparer.OrdinalIgnoreCaseis strictly safer and is whatRenameServicealready uses.I have this on a branch with twelve test cases, one casing theory per table, three of them through a real import. Eight of the twelve fail with the four comparers reverted.
DownloadImportService.csis at exactly the 500-line architecture cap, so its change is line neutral. Happy to open it as a PR, or to fold it into whatever shape you would rather have; I have kept it off #869 so that one stays matched to the issue it closes.The longer-term answer is to stop having six copies of one table at all. That crosses an assembly boundary, since
ManualImportPathPlannerandLibraryPathPlannerare inlistenarr.apiand the rest are inlistenarr.application, so it is a bigger decision than this one.Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.