Skip to content

fix(naming): key the remaining naming tables case insensitively - #984

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/naming-table-casing
Open

m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/naming-table-casing

Conversation

@m4bard

@m4bard m4bard commented Sep 14, 2026

Copy link
Copy Markdown

Fixes #976.

Six places in the codebase build a book's naming-variable dictionary. ApplyNamingPattern's token regex matches case-insensitively, so {author} reaches the lookup as "author". Five of the six dictionaries key on the default case-sensitive comparer, so a lowercase-written pattern that works fine at rename time (RenameService is the one table already using OrdinalIgnoreCase) fails silently on import: the token misses, takes the not-found path, and gets stripped along with its separators. A file imports as Unknown Title.m4b at the library root, with nothing logged above a per-file warning that reads like ordinary noise.

This branch is StringComparer.OrdinalIgnoreCase on the four remaining dictionaries not already covered by #869 (ManualImportPathPlanner, fixed separately for #816): FileNamingService.Helpers.cs (both metadata overloads), DownloadImportService.cs, and LibraryPathPlanner.cs. Ordinal, not culture-aware, since CurrentCultureIgnoreCase breaks under tr-TR where I and i are different letters, and it matches what RenameService already does.

Twelve test cases, one casing theory per table, three of them through a real import path. Reverting the four comparer changes fails eight of the twelve.

Suite run against current canary (upstream/canary at a630572e9, which this branch is built on): targeted filter across ImportNamingTableCasingTests and NamingTableCasingParityTests, 12 passed, 0 failed.

DownloadImportService.cs sits exactly at the 500-line architecture cap after this change, so it is line-neutral. The issue notes the longer-term fix is consolidating six copies of one table into one, which crosses an assembly boundary and is a separate decision.

Disclosure: drafted with Claude Code at my direction; I read the cited code at commit a630572 and reviewed this before posting.

Six places build a book's naming variables. The token regex in
FileNamingService.ApplyNamingPattern carries RegexOptions.IgnoreCase, so a
pattern written {author} reaches the dictionary lookup as "author". Only
RenameService keyed its dictionary with StringComparer.OrdinalIgnoreCase, so a
lowercase pattern resolved under rename and missed everywhere else: the lookup
failed, the not-found path emitted the empty sentinel, and the cleanup removed
the segment along with its separators.

What a user sees is a library renamed successfully with {author}/{series}, and
then every new download importing as "Unknown Title.m4b" at the root of the
library, because DownloadImportService builds its own table and that one missed.

Four tables here: both FileNamingService overloads, DownloadImportService and
LibraryPathPlanner. The fifth, ManualImportPathPlanner, is Listenarrs#816 and is fixed by
PR 869; this branch deliberately leaves it alone so the two do not overlap.

Ordinal rather than culture-aware, because under tr-TR 'I' and 'i' are different
letters and CurrentCultureIgnoreCase would break {TITLE} against the key "Title".
Readarr's FileNameBuilderTokenEqualityComparer calls the culture-sensitive
ToLower() for this and has that trap open.

Twelve cases, one casing theory per table, three of them through a real import.
Eight fail with the four comparers reverted. DownloadImportService.cs is at the
500 line architecture cap, so its change is line neutral.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@m4bard
m4bard requested a review from a team September 14, 2026 09:20
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.

A naming pattern written in lowercase resolves on rename and is dropped on import

1 participant