GetTypedBooksBySeriesAsinAsync orders a series by parsing the Audible position into a decimal:
|
private static decimal ParseSeriesPosition(string? rawPosition) |
|
{ |
|
return decimal.TryParse(rawPosition, out var parsed) ? parsed : decimal.MaxValue; |
|
} |
private static decimal ParseSeriesPosition(string? rawPosition)
{
return decimal.TryParse(rawPosition, out var parsed) ? parsed : decimal.MaxValue;
}
feeding the sort at line 165:
.OrderBy(item => ParseSeriesPosition(item.Position))
Position comes from Audible's sequence or sort field, which is a string and always uses . as the decimal separator. The parse takes no IFormatProvider, so it runs under whatever culture the process happens to have.
Who this affects
Worth stating up front, because it is narrower than it first looks. I ran a probe inside mcr.microsoft.com/dotnet/aspnet:10.0, the base image the Dockerfile uses:
| environment |
CurrentCulture |
ParseSeriesPosition("1.5") |
| default, as the Dockerfile leaves it |
invariant (LANG unset) |
1.5, correct |
-e LANG=de_DE.UTF-8 |
de-DE |
15 |
So a stock container is fine. It needs a real culture to reach the process, which happens when LANG or LC_ALL is set, and on a desktop install that takes its locale from the OS. I have not tested the Windows or macOS builds, so I am not claiming anything about those.
What it does when it fires
Calling the real method on unmodified canary, over ["1", "1.5", "2", "10", "1-4"]:
| position |
en-US |
de-DE |
fr-FR |
1 |
1 |
1 |
1 |
1.5 |
1.5 |
15 |
MaxValue |
2 |
2 |
2 |
2 |
10 |
10 |
10 |
10 |
1-4 |
MaxValue |
MaxValue |
MaxValue |
en-US: 1, 1.5, 2, 10, 1-4 (correct)
de-DE: 1, 2, 10, 1.5, 1-4 ('.' is the group separator, so 1.5 parses as 15)
fr-FR: 1, 2, 10, 1.5, 1-4 ('.' parses as nothing, so 1.5 falls to MaxValue)
A novella at 1.5 lands after book 10 on a German server and after every numbered book on a French one, and the series is returned in that order.
Fix
decimal.TryParse(rawPosition, NumberStyles.Number, CultureInfo.InvariantCulture, out var parsed)
The change is one line. I have it working locally with tests over all four cultures.
Prior art
The convention across the *arr projects is to parse machine-format values invariantly at the call site and take display language from an explicit user setting rather than from CurrentCulture. A few that are directly comparable:
- Sonarr parses fractional episode numbers, the same shape of data, invariantly:
Parser.cs:1268
- Readarr does the same in
RssParser.cs:388 and in the shared helper TryParseExtensions.cs:29
- Sonarr fixed this exact bug in
bd601332 (2014), "Framerate in mediainfo is now parsed culture invariant". The diff is a bare Decimal.TryParse gaining NumberStyles and CultureInfo.InvariantCulture, which is the same change proposed above.
Worth being accurate about how it is upheld: there is no analyzer enforcing it (CA1305 is set to suggestion in those repos) and no written policy. It is held up by call-site habit and by tests parameterized over several cultures.
One thing worth deciding rather than assuming
Every position that fails to parse collapses to decimal.MaxValue, so 1-4 and any other non-numeric value sort as equal and their order among themselves falls out of the input order. Invariant parsing does not change that, and 1-4 is a real Audible position for an omnibus rather than bad data.
That is the only thing that alters the shape of the fix. If non-numeric positions should simply stay last, the PR is the parse change and nothing else. If you want them ordered among themselves, that is a second decision and I would rather you made it than guess at it.
Say which and I will open the PR against canary.
Same root cause as #764, different code path: that one is about a position reaching the filename, this one about the order a series is listed in. They share no code, so I have kept them separate.
Environment: canary at 4555ad2, .NET 10, Linux.
GetTypedBooksBySeriesAsinAsyncorders a series by parsing the Audible position into a decimal:Listenarr/listenarr.application/Metadata/Audible/AudibleSeriesWorkflow.cs
Lines 340 to 343 in 4555ad2
feeding the sort at line 165:
Positioncomes from Audible'ssequenceorsortfield, which is a string and always uses.as the decimal separator. The parse takes noIFormatProvider, so it runs under whatever culture the process happens to have.Who this affects
Worth stating up front, because it is narrower than it first looks. I ran a probe inside
mcr.microsoft.com/dotnet/aspnet:10.0, the base image the Dockerfile uses:ParseSeriesPosition("1.5")LANGunset)-e LANG=de_DE.UTF-8So a stock container is fine. It needs a real culture to reach the process, which happens when
LANGorLC_ALLis set, and on a desktop install that takes its locale from the OS. I have not tested the Windows or macOS builds, so I am not claiming anything about those.What it does when it fires
Calling the real method on unmodified canary, over
["1", "1.5", "2", "10", "1-4"]:11.52101-4A novella at 1.5 lands after book 10 on a German server and after every numbered book on a French one, and the series is returned in that order.
Fix
The change is one line. I have it working locally with tests over all four cultures.
Prior art
The convention across the *arr projects is to parse machine-format values invariantly at the call site and take display language from an explicit user setting rather than from
CurrentCulture. A few that are directly comparable:Parser.cs:1268RssParser.cs:388and in the shared helperTryParseExtensions.cs:29bd601332(2014), "Framerate in mediainfo is now parsed culture invariant". The diff is a bareDecimal.TryParsegainingNumberStylesandCultureInfo.InvariantCulture, which is the same change proposed above.Worth being accurate about how it is upheld: there is no analyzer enforcing it (CA1305 is set to
suggestionin those repos) and no written policy. It is held up by call-site habit and by tests parameterized over several cultures.One thing worth deciding rather than assuming
Every position that fails to parse collapses to
decimal.MaxValue, so1-4and any other non-numeric value sort as equal and their order among themselves falls out of the input order. Invariant parsing does not change that, and1-4is a real Audible position for an omnibus rather than bad data.That is the only thing that alters the shape of the fix. If non-numeric positions should simply stay last, the PR is the parse change and nothing else. If you want them ordered among themselves, that is a second decision and I would rather you made it than guess at it.
Say which and I will open the PR against canary.
Same root cause as #764, different code path: that one is about a position reaching the filename, this one about the order a series is listed in. They share no code, so I have kept them separate.
Environment: canary at 4555ad2, .NET 10, Linux.