Skip to content

fix(naming): don't lose a series position that isn't a plain number - #763

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/series-position-not-lost
Open

fix(naming): don't lose a series position that isn't a plain number#763
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/series-position-not-lost

Conversation

@m4bard

@m4bard m4bard commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Fixes #764.

Audnexus reports a series position as a string, and it is not always a number. Squeezing it through a decimal lost it in two ways, and the loss reached the filename.

Real positions from the catalogue (public domain, verifiable against api.audnex.us):

Book ASIN position
The Father Brown Collection: Books 1-4 B0F84DFZ66 "1-4" — one ASIN, four books
The Thirty-Nine Steps B002V1PLZK "1-2" — the product also ships Greenmantle
She and Allan B00CQ5WAXW "0" — a prequel slot
a novella between two books "1.5"

The parse used the server's culture. decimal.TryParse was called with no CultureInfo, while FileNamingService formatted the result back with InvariantCulture. The source always uses . as the decimal separator, so where . is the group separator the number changed:

en-US      "1.5" -> 1.5     "0.5" -> 0.5
de-DE      "1.5" -> 15      "0.5" -> 5
fr-FR      "1.5" -> null    "0.5" -> null
invariant  "1.5" -> 1.5     "0.5" -> 0.5

A position that didn't parse was silently discarded. It became null — indistinguishable from a book with no position — so naming fell through to the track number and wrote that into the filename instead. No error, nothing in the log.

Changes

Added

  • AudioMetadata.SeriesPositionRaw — the series position exactly as the metadata source gave it. SeriesPosition (decimal?) is kept for callers that sort or compare, but it cannot hold "1-4" and no longer pretends to.
  • SeriesPositionReproTests — 10 cases covering culture handling and non-numeric positions.

Changed

  • Audiobook.CreateBasicAudioMetadata and DownloadImportService now parse the position with InvariantCulture.
  • Both naming sites (FileNamingService.Helpers, DownloadImportService) prefer SeriesPositionRaw, then the invariant-formatted decimal, then the track number. DownloadImportService previously formatted with a bare .ToString(), which would write 1,5 — with a comma — into a filename.

Fixed

  • A non-numeric but real series position is no longer replaced by the track number when a file is renamed.
  • A decimal position no longer changes value depending on the server's locale.

Testing

SeriesPositionReproTests (10 new cases):

  • a decimal position survives under en-US, de-DE and fr-FR
  • a decimal position is written invariantly, so no locale decimal-comma reaches a filename
  • "1-4" and "1-2" are preserved and reach the filename rather than being replaced by the track number
  • a book with no position still falls back to the track number, as before
  • "0" survives

dotnet test: 1199 passing, 0 failing (1189 existing + 10 new). No existing test was changed.

Notes

The fall back to the track number is deliberate and is preserved — a book with genuinely no series position still gets it. The bug was that a real position was being treated as an absent one.

Apologies for opening this before the issue; #764 has the write-up. I've read CONTRIBUTING.md properly now.

Out of scope but worth flagging while it's in view: a book can hold two series memberships at once. Audnexus gives She and Allan (B00CQ5WAXW) seriesPrimary = Ayesha #0 and seriesSecondary = Allan Quatermain #7. MetadataConverters reads both, but AudiobookSeriesMembership is never constructed outside tests, so neither is persisted. Happy to open that separately if useful.

@m4bard

m4bard commented Aug 2, 2026

Copy link
Copy Markdown
Author

Rebased this onto #717 locally. Half of it needs attention sooner than the rebase does.

On unmodified #717 head, under the invariant culture a container runs with by default, a book whose series position is "1-4" gets the track number written into its filename:

position "1-4", track number 7
expected: 1-4
actual:   7

No locale involved. decimal.TryParse fails on "1-4" under every culture, SeriesPosition becomes null, and naming cannot tell that apart from a book with no series position, so it falls through to the track number. "1-4" is a real Audible position rather than bad data: the Father Brown collection sits at it, and The Thirty-Nine Steps at "1-2".

The other half is the culture-dependent parse, which only shows up with LANG or LC_ALL set, so it is the less pressing one.

Both come back in the new file:

listenarr.application/Downloads/Import/DownloadImportService.Naming.cs

SeriesPosition = !string.IsNullOrWhiteSpace(audiobook.SeriesNumber)
    && decimal.TryParse(audiobook.SeriesNumber, out var seriesPosition)
        ? seriesPosition
        : extractedMetadata?.SeriesPosition,

SeriesPositionRaw appears nowhere in #717's diff, so nothing there supersedes this PR.

The port comes to five sites against four on canary. Splitting DownloadImportService.cs into partials moved BuildNamingMetadata into DownloadImportService.Naming.cs, and that file has no using directives, so it also needs using System.Globalization;.

  • listenarr.domain/Audiobooks/AudioMetadata.cs, add SeriesPositionRaw and the Update() merge
  • listenarr.domain/Audiobooks/Audiobook.cs:101, invariant parse, set raw
  • listenarr.application/Common/FileNamingService.Helpers.cs:89, prefer raw
  • listenarr.application/Downloads/Import/DownloadImportService.Naming.cs:53, invariant parse, set raw
  • listenarr.application/Downloads/Import/DownloadImportService.cs:240, prefer raw, format invariant

The test needed adapting too. TestClasses_FollowRepositoryConventions wants BaseTests, an exact Name trait and a non-empty Category trait, which the test file on this PR predates.

I ran the full suite on 379bb9d with and without the port and diffed the failing tests. Identical set either way, so the port adds none. I am not claiming a green suite on your branch.

It is one commit on top of 379bb9d if you want to cherry-pick it rather than wait for the rebase: m4bard/Listenarr, branch port/763-onto-717. Same arrangement as #766 if that suits. Otherwise I will leave this PR as it is and rebase once #717 lands.

@m4bard

m4bard commented Aug 20, 2026

Copy link
Copy Markdown
Author

Rebased onto canary 03958c15. The refactor this was waiting on merged as #717 on 2026-08-12 and #819 has landed since, so the base this was cut from is long gone.

Before rebasing I checked whether the fix still lands where the value is used, since a clean merge only says no lines collide. There are five places that put a SeriesNumber into a naming token:

site source through decimal?
LibraryPathPlanner.cs:91 audiobook.SeriesNumber no
RenameService.Helpers.cs:204 audiobook.SeriesNumber no
FileNamingService.Helpers.cs:117 (AudibleBookMetadata) metadata.SeriesNumber no
FileNamingService.Helpers.cs:89 (AudioMetadata) SeriesPosition then TrackNumber yes
DownloadImportService.cs:297 SeriesPosition then chapter number yes

Three of them already carry the position as the string it arrived as. The two that round-trip it through a decimal are the two this touches, so the change is narrower than I described it originally: it is not introducing a way of carrying a non-numeric position, it is making two sites agree with the three that already do.

Full suite on the rebased branch: 3,039 passed, 0 failed, 125 skipped, against a 3,029 baseline. Removing just the raw-position preference in FileNamingService fails RangePosition_ReachesTheFilename_AndIsNotReplacedByTheTrackNumber for "1-2" and "1-4", which is the omnibus case, so the consumer half is load-bearing rather than defensive.

Ported onto Listenarrs#717, where BuildNamingMetadata has moved into
DownloadImportService.Naming.cs and the culture-dependent parse is
reintroduced there.

A position arrives from Audnexus as a string and is not always a decimal:
an omnibus sits at "1-4", a prequel at "0", a novella at "1.5". Squeezing
it through decimal.TryParse loses the ones that do not parse, and naming
then falls through to the track number and writes that into the filename
as if it were the series number. The parse also used the server's culture,
so "1.5" read as 15 wherever '.' is the group separator.

SeriesPositionRaw keeps the original string, naming prefers it, and the
remaining decimal parses are pinned to InvariantCulture.

On the import side the {SeriesNumber} token is built by a small helper in
DownloadImportService.Naming.cs rather than written inline.
DownloadImportService.cs is already at the 500 line architecture cap, and
the naming partial is where the rest of this logic lives.
@m4bard
m4bard force-pushed the fix/series-position-not-lost branch from b5ad3d9 to ce39124 Compare September 10, 2026 18:52
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.

Series position from Audnexus is lost when it isn't a plain number (locale-dependent parse; non-numeric positions dropped into the track number)

1 participant