fix: apply naming patterns exactly and unify all naming flows (#571)#657
fix: apply naming patterns exactly and unify all naming flows (#571)#657s3ntin3l8 wants to merge 12 commits into
Conversation
therobbiedavis
left a comment
There was a problem hiding this comment.
Overall this looks good. I found one edge case in the path cleanup logic though. Once that's buttoned up this should be good to merge. Just FYI we're reviewing multiple PRs at once and haven't put this on our roadmap just yet so you may need to rebase while we review and merge other PRs before yours. Thanks!
T4g1
left a comment
There was a problem hiding this comment.
I believe we need more that that, the original issue arise from the fact that each service kind of handle the pattern naming in its own way, the fix should focus first on reconciliating the pattern handling to a single service or from a centralized place at least
|
@T4g1: You're right that the path handling is duplicated. I agree consolidating onto one orchestrator is the right move. Should we fold it into this PR (which is just a small bug fix), or adress in a seperate PR? |
therobbiedavis
left a comment
There was a problem hiding this comment.
@s3ntin3l8, I am more prone to expanding the scope slightly to go ahead and do things the proper way than I am separating out things into subtasks/prs.
…s/Subtitle (Listenarrs#571) ComputeAudiobookBaseDirectoryFromPattern injected a {Series} directory level into the configured folder pattern whenever the audiobook had series metadata, even when the user deliberately omitted {Series}. The injected path drives the Add-book destination preview and the bulk root-folder change, and is persisted as the audiobook BasePath — so imported files really landed in an unconfigured series folder. RenameService and ManualImportController similarly rewrote the {Title} variable to "Title: Subtitle" when the patterns lacked {Subtitle} (sanitized to "Title - Subtitle" in paths). Patterns are now applied verbatim; users who want series folders or subtitles add the {Series}/{Subtitle} tokens explicitly. The default folder pattern already contains {Series}, so default configurations are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tation (Listenarrs#571) A deliberately flat folder pattern (e.g. "{Title}") was silently replaced with "{Author}/{Title}" when computing an audiobook's base directory for the destination preview and bulk root-folder change. The import pipeline (FileNamingService) already applies such patterns exactly, so the preview did not match where files actually went. The "{Author}/{Title}" fallback now only applies when no pattern is configured at all (or token stripping leaves it empty). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…enarrs#571) The "remove {Series} when the book has no series" cleanup in ComputeAudiobookBaseDirectoryFromPattern used the regex \{Series[^}]*\}, which also matched {SeriesNumber}/{SeriesNumber:00} — so a configured pattern like {Author}/{SeriesNumber}/{Title} lost its number for non-series books. Remove the textual strip block entirely and let ApplyNamingPattern handle the empty {Series} value (it already substitutes empty tokens and collapses the surrounding separators — the single cleanup path the rest of this change relies on). {SeriesNumber} is now preserved; the existing non-series test ({Author}/{Series}/{Title} -> Author/Title) still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
719d9d7 to
57b898e
Compare
…#571) Introduce a single path-assembly orchestrator in FileNamingService — BuildDirectory/BuildPath over a normalized NamingContext — so the per-flow naming duplication can be consolidated. NamingContext.From() factories map Audiobook/AudioMetadata/AudibleBookMetadata into one shape; source-specific quirks (noisy-tag author selection) live in the mappers, while sanitization, token substitution, the folder/file/legacy branch and path-length limits live once in the service. GenerateFilePathAsync now delegates to BuildPath, so its existing ~40 tests exercise the orchestrator. No behavior change yet — the individual flows are rerouted onto BuildDirectory/BuildPath in follow-up commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#571) Replace the per-flow variable builders and the duplicated folder/file/legacy branch with the single NamingContext + BuildDirectory/BuildPath orchestrator: - LibraryAddService + LibraryController.ComputeAudiobookBaseDirectoryFromPattern -> BuildDirectory / ApplyNamingPattern(NamingContext) - RenameService.BuildExpectedPath -> BuildPath - ManualImportController -> BuildPath - DownloadImportService -> ApplyNamingPattern(NamingContext), dropping its private invalid-char sanitizer (the shared sanitizer covers it) Delete the now-dead duplicates: SanitizeDirectoryName, per-flow BuildNamingVariables/PatternAllowsSubfolders copies, and the unused AudioMetadata/AudibleBookMetadata ApplyNamingPattern + BuildVariables overloads and ChooseAuthor heuristic (folded into NamingContext). Canonical behavior is now uniform across flows: one strong SanitizePathComponent (":" -> " - "), case-insensitive tokens, "Unknown Author" fallback, and consistent legacy/custom-base/folder+file handling. Manual import now supports {SeriesNumber}/{Quality} and uses the "Unknown Author" folder for author-less books (its multi-file ordering test updated to match). Full backend suite green (705 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Direct BuildDirectory/BuildPath tests covering the canonical decisions:
case-insensitive tokens, ":" -> " - " sanitization, "Unknown Author"
fallback, empty-{Series} separator collapse, multi-file sequence suffix,
and custom-base-path file-only naming.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rrs#571) The consolidation routed DownloadImportService through NamingContext.From(AudioMetadata), which had folded in the narrator-avoidance heuristic from the (dead-in-prod) GenerateFilePathAsync. That heuristic drops the author when Artist == Narrator — common for self-narrated memoirs — sending those imports to "Unknown Author/". Take the author straight from the Artist tag (matching the historical auto-import behavior) and drop the heuristic. Adds a regression test for the author-narrated case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review finding #1: DownloadImport had consolidated its variable building onto NamingContext but still assembled the destination with direct ApplyNamingPattern calls, bypassing the shared orchestrator. Route it through BuildPath like the other flows — a set BasePath means file-only naming (IsCustomBasePath), an empty BasePath applies the folder pattern. This also brings EnsurePathWithinLimits to the auto-import path and adopts the multi-file sequence suffix, fixing a latent overwrite when a multi-file download uses a pattern without {DiskNumber}/{ChapterNumber}. Remove the now-dead folderPattern/destDirForFile/baseFilePattern/ patternAllowsSubfolders/treatAsFilename locals. Add a regression test for the suffix collision fix, and a doc note on the AudibleBookMetadata -> ToAudiobook coupling (review finding Listenarrs#5). Full backend suite green (713 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…enarrs#571) Review item 7: with an empty FolderNamingPattern, BuildDirectory rendered the whole FileNamingPattern as a directory (over-nesting the filename segment as a folder), while BuildPath's legacy branch treats it as a full relative path. Derive the directory portion in BuildDirectory's fallback so the two agree: BuildDirectory(ctx) == Path.GetDirectoryName(BuildPath(ctx).RelativePath) Only LibraryAddService uses BuildDirectory, and only the empty-pattern edge case changes. Adds tests for the consistency invariant plus the review's flagged gaps: BuildPath legacy fallback, multi-file + {DiskNumber} token, From(Authors=null), and From(AudibleBookMetadata) mapping. Full suite green (719 tests; Category=FileNamingService = 36). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) - Remove the two Listenarrs#571 CHANGELOG entries — changelog entries are no longer wanted on upstream PRs (maintainer confirmed on Listenarrs#659/Listenarrs#660); they only cause [Unreleased] conflicts. Keeps the existing Authentication entries. - Unify LibraryController.ComputeAudiobookBaseDirectoryFromPattern's no-pattern fallback from {Author}/{Title} to {Author}/{Series}/{Title}, matching the orchestrator's legacy default (review follow-up). Reachable only when no folder pattern is configured at all; empty {Series} collapses away, so non-series books are unaffected. 719 backend tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stenarrs#571) Review follow-ups: the {Author}/{Series}/{Title} no-pattern fallback in ComputeAudiobookBaseDirectoryFromPattern (unified in the previous commit) had no test — every existing case passed an explicit pattern. Also adds a service-level test for LibraryAddService deriving BasePath from the file naming pattern's directory portion when no folder pattern is configured (BuildDirectory's legacy branch, previously only covered at unit level). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stable (Listenarrs#571) Pull the MAX_PATH truncation logic behind a testable seam (EnforceWindowsPathLimits) that uses explicit Windows path semantics — separators and drive/UNC root parsing — instead of the platform-dependent System.IO.Path helpers. The previously self-skipping path-length tests now run on any OS (including the ubuntu-only CI), and UNC (NAS share) inputs are covered. Writing the UNC coverage exposed a real bug: on Windows, Path.GetPathRoot(@"\\server\share\dir") returns the root without a trailing separator, and the function always returns the rebuilt root + join(parts) string — so every UNC path it touched came back as \\server\sharedir\..., even when under the limit. This was dormant while EnsurePathWithinLimits had no production callers, but routing manual import, rename, and download import through BuildPath made it reachable; fixed by including the separator after the share in the parsed root. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@T4g1 @therobbiedavis The consolidation discussed above is implemented and pushed. What changed:
Consolidation flushed out two latent bugs, both fixed with regression tests:
Full backend suite is 735/735 green. The PR description is rewritten to the template and lists the (opt-in) behavior changes - happy to adjust anything that doesn't match what you had in mind. |
Fixes #571
Summary
Naming patterns are now applied exactly as configured — no implicit
{Series}folder level and no implicit"Title: Subtitle"combining ("apply given naming patterns exactly", as suggested in the issue). Previously,LibraryController.ComputeAudiobookBaseDirectoryFromPatterninjected{Series}into folder patterns that omitted it (and the result was persisted as the audiobook'sBasePath, so imported files really landed in the unconfigured series folder), whileRenameServiceandManualImportControllerrewrote{Title}to"Title: Subtitle"when the patterns didn't use{Subtitle}(rendered asTitle - Subtitle, the originally reported symptom).As discussed in the issue, the fix expanded into consolidating the previously divergent naming flows behind a single orchestrator, so the per-flow drift that caused this can't recur.
Changes
Added
NamingContext(domain record): normalized, source-agnostic naming metadata withFrom(Audiobook)/From(AudioMetadata)/From(AudibleBookMetadata)factories. Source-specific quirks live in the factories; processing lives in the naming service.IFileNamingService.BuildDirectory/BuildPath: the single naming orchestrator — folder/file/legacy/custom-base pattern application, multi-file sequence suffixes, sanitization, and path-length limits in one place.Changed
LibraryController(POST /library/preview-path, bulk root-folder change),ManualImportController,RenameService,LibraryAddService,DownloadImportService. Five hand-rolled variable dictionaries and four copies of the pattern-branching logic are gone.LibraryController's no-pattern fallback is now{Author}/{Series}/{Title}, aligned with the orchestrator's legacy default (was{Author}/{Title}). Only reachable when no folder pattern is configured at all; an empty{Series}collapses away, so non-series books are unaffected.Fixed
{Series}injection inComputeAudiobookBaseDirectoryFromPattern— e.g.{Author}/{Title} ({Year}) [{Asin}]no longer previews/persistsTom Clancy/A Jack Ryan Novel (chronological order)/Executive Orders (2010) [B004ESTSSO]."Title: Subtitle"combining inRenameServiceandManualImportController. The explicit{Subtitle}token keeps working.{Title}) is applied exactly instead of being replaced with{Author}/{Title}— matching how the import pipeline already applied such patterns.{DiskNumber}/{ChapterNumber}token no longer resolve every file to the same name (overwriting each other) —BuildPathappends the stable-NNsequence suffix, matching rename/manual-import behavior.Unknown Author.Path.GetPathRoot(@"\\server\share\dir")has no trailing separator andEnsurePathWithinLimitsalways returns the rebuiltroot + join(parts)string, so every UNC path it touched came back as\\server\sharedir\...— even under the limit. Dormant while the method had no production callers, but reachable onceBuildPath(which the import/rename flows now use) started calling it. The MAX_PATH logic now sits behind a testable seam with explicit Windows root parsing, so the previously self-skipping path-length tests (and new UNC cases) run on the ubuntu CI runners too.Removed
SanitizeDirectoryName, theApplyNamingPattern(AudioMetadata)/ApplyNamingPattern(AudibleBookMetadata)overloads, andFileNamingService'sChooseAuthorheuristic (the noisy-tag author heuristic remains where it's used, inDownloadImportService.BuildNamingMetadata).Testing
{Series}(incl. the literal reported pattern), slash-less pattern,{SeriesNumber}preserved for series-less books, subtitle not combined in rename + manual import, explicit{Subtitle}still renders, multi-file no-token suffix collision fix, 13 orchestrator tests locking in canonicalBuildDirectory/BuildPathbehavior, the no-pattern{Author}/{Series}/{Title}fallback (controller + add-to-library service level), and Windows MAX_PATH/UNC handling via the new seam (drive, UNC, and root-parsing cases that previously self-skipped on non-Windows). Full backend suite: 735/735 green.{Series}previewsTom Clancy/Executive Orders (2010) [B004ESTSSO]; switching to{Author}/{Series}/{Title}still produces the series folder.Notes
Behavior changes to be aware of:
{Series}/{Subtitle}to their patterns. The default folder pattern{Author}/{Series}/{Title}already contains{Series}, so default configurations are unaffected.BasePathvalues are not rewritten retroactively.Unknown Authorfolder for author-less books: the unified builder includes theAuthortoken with the standard"Unknown Author"fallback (matching every other flow); manual import previously omitted blank variables, so author-less batches landed directly under the base path.:) in titles now renders as-in the Add-book destination preview / persistedBasePath(the consolidatedSanitizePathComponentreplaces the removed controller-local sanitizer, which mapped:→_).🤖 Generated with Claude Code