Skip to content

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

Description

@m4bard

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.

  1. Settings, folder naming pattern {author}/{title}, file naming pattern {title}.
  2. Add a book and rename the library. The files land under Author Name/Book Title/.
  3. Import a file for that book, manually or by letting a download complete.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions