-
-
Notifications
You must be signed in to change notification settings - Fork 39
Fix M4B re-grab loop: emit codec-prefixed quality labels #581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,10 @@ public static class AudiobookStatusEvaluator | |
| public const string QualityMismatch = "quality-mismatch"; | ||
| public const string QualityMatch = "quality-match"; | ||
|
|
||
| // Bitrate rungs seeded by QualityProfileService.EnsureProfileHasRequiredQualitiesAsync. | ||
| // Ordered high → low so BucketBitrate returns the largest rung the file meets. | ||
| private static readonly int[] BitrateBuckets = { 320, 256, 192, 128, 64 }; | ||
|
|
||
| public static string ComputeStatus( | ||
| bool isDownloading, | ||
| bool hasAnyFile, | ||
|
|
@@ -135,55 +139,153 @@ public static string ComputeStatus( | |
|
|
||
| private static string DeriveQualityLabel(AudiobookFileStatusInfo? file, string? audiobookQuality) | ||
| { | ||
| var normalizedAudiobookQuality = Normalize(audiobookQuality); | ||
| if (normalizedAudiobookQuality.Length > 0) | ||
| // Only trust the audiobookQuality hint if it already matches a production QP key | ||
| // shape (e.g. "MP3 320kbps", "AAC 64kbps", "FLAC"). Legacy values like "M4B" must | ||
| // fall through so we re-derive from the file's codec/container/bitrate fields and | ||
| // produce a label that can actually round-trip with QualityProfile.Qualities keys. | ||
| var hint = Normalize(audiobookQuality); | ||
| if (IsCodecPrefixedLabel(hint)) | ||
| { | ||
| return normalizedAudiobookQuality; | ||
| return hint; | ||
| } | ||
|
|
||
| if (file?.Bitrate is int bitrate) | ||
| var codec = DetectCodec(file); | ||
|
|
||
| if (IsLosslessCodec(codec)) | ||
| { | ||
| var bitrateKbps = bitrate >= 1000 ? bitrate / 1000d : bitrate; | ||
| return "lossless"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The normalization here is fine, but I think the returned label may still not match real FLAC profiles. A file with codec FLAC gets normalized to flac, passes IsLosslessCodec, and then returns lossless, but the profile model documents quality IDs like FLAC in QualityProfile.cs, and the quality profile UI parses/adds FLAC as FLAC in QualityProfileFormModal.vue. So the lookup can still miss unless the profile has a custom lossless quality. There are two options: Revert back to the quality label or add a compatibility path and keep lossless: For the former we'd need to use the profile quality IDs instead of using the a generic category to match. For FLAC that means returning flac/FLAC instead of lossless, or better, deriving against the current profile’s Qualities so custom lossless labels and future codec names don’t need special-case branches. If we want to keep lossless as a generic label, I think the comparison needs to treat it as equivalent to the actual lossless profile keys. For example, when derived quality is lossless, check whether the profile contains FLAC, ALAC, WAV, etc., or map the detected codec to a profile key first and fall back to lossless only for legacy/custom profiles. What do you think? |
||
| } | ||
|
|
||
| if (bitrateKbps >= 320) | ||
| if (!string.IsNullOrEmpty(codec) && file?.Bitrate is int bitrate) | ||
| { | ||
| var bitrateKbps = bitrate >= 1000 ? bitrate / 1000d : bitrate; | ||
| var bucket = BucketBitrate(bitrateKbps); | ||
| if (bucket > 0) | ||
| { | ||
| return "320kbps"; | ||
| return $"{codec} {bucket}kbps"; | ||
| } | ||
| } | ||
|
|
||
| if (bitrateKbps >= 256) | ||
| { | ||
| return "256kbps"; | ||
| } | ||
| return Normalize(file?.Format); | ||
| } | ||
|
|
||
| private static string DetectCodec(AudiobookFileStatusInfo? file) | ||
| { | ||
| var codec = Normalize(file?.Codec); | ||
| var container = Normalize(file?.Container); | ||
| var format = Normalize(file?.Format); | ||
|
|
||
| if (codec.Contains("mp3", StringComparison.Ordinal) | ||
| || container.Contains("mp3", StringComparison.Ordinal) | ||
| || format == "mp3") | ||
| { | ||
| return "mp3"; | ||
| } | ||
|
|
||
| if (codec.Contains("flac", StringComparison.Ordinal) | ||
| || container.Contains("flac", StringComparison.Ordinal) | ||
| || format == "flac") | ||
| { | ||
| return "flac"; | ||
| } | ||
|
|
||
| if (codec.Contains("alac", StringComparison.Ordinal) | ||
| || container.Contains("alac", StringComparison.Ordinal)) | ||
| { | ||
| return "alac"; | ||
| } | ||
|
|
||
| if (codec.Contains("opus", StringComparison.Ordinal) | ||
| || container.Contains("opus", StringComparison.Ordinal) | ||
| || format == "opus") | ||
| { | ||
| return "opus"; | ||
| } | ||
|
|
||
| if (codec.Contains("vorbis", StringComparison.Ordinal) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small naming mismatch here: this returns vorbis, which I think is technically correct, but the UI/profile shape looks like OGG Vorbis. That means OGG Vorbis profiles still won’t match correctly. |
||
| || container.Contains("ogg", StringComparison.Ordinal) | ||
| || format == "ogg") | ||
| { | ||
| return "vorbis"; | ||
| } | ||
|
|
||
| if (codec.Contains("aac", StringComparison.Ordinal) | ||
| || codec.Contains("mp4a", StringComparison.Ordinal)) | ||
| { | ||
| return "aac"; | ||
| } | ||
|
|
||
| if (bitrateKbps >= 192) | ||
| if (codec.Contains("aiff", StringComparison.Ordinal) | ||
| || container.Contains("aiff", StringComparison.Ordinal)) | ||
| { | ||
| return "aiff"; | ||
| } | ||
|
|
||
| if (codec.Contains("ape", StringComparison.Ordinal) | ||
| || container.Contains("ape", StringComparison.Ordinal)) | ||
| { | ||
| return "ape"; | ||
| } | ||
|
|
||
| if (codec.Contains("dsd", StringComparison.Ordinal) | ||
| || container.Contains("dsd", StringComparison.Ordinal)) | ||
| { | ||
| return "dsd"; | ||
| } | ||
|
|
||
| if (codec.Contains("wavpack", StringComparison.Ordinal) | ||
| || container == "wv") | ||
| { | ||
| return "wavpack"; | ||
| } | ||
|
|
||
| if (codec.Contains("wav", StringComparison.Ordinal) | ||
| || container == "wav" | ||
| || format == "wav") | ||
| { | ||
| return "wav"; | ||
| } | ||
|
|
||
| // M4B/M4A/MP4 containers carry AAC for virtually all audiobooks. | ||
| if (container is "m4b" or "m4a" or "mp4" | ||
| || format is "m4b" or "m4a" or "mp4") | ||
| { | ||
| return "aac"; | ||
| } | ||
|
|
||
| return string.Empty; | ||
| } | ||
|
|
||
| private static bool IsLosslessCodec(string codec) | ||
| { | ||
| return codec is "flac" or "alac" or "wav" or "aiff" or "ape" or "dsd" or "wavpack"; | ||
| } | ||
|
|
||
| private static int BucketBitrate(double bitrateKbps) | ||
| { | ||
| foreach (var bucket in BitrateBuckets) | ||
| { | ||
| if (bitrateKbps >= bucket) | ||
| { | ||
| return "192kbps"; | ||
| return bucket; | ||
| } | ||
|
|
||
| return $"{Math.Round(bitrateKbps)}kbps"; | ||
| } | ||
| // Below the lowest seeded rung — map to it so we don't trigger | ||
| // a perpetual re-grab loop for unusually low-bitrate sources. | ||
| return BitrateBuckets[^1]; | ||
| } | ||
|
|
||
| var container = Normalize(file?.Container); | ||
| var codec = Normalize(file?.Codec); | ||
| if (container.Contains("flac", StringComparison.Ordinal) | ||
| || codec.Contains("flac", StringComparison.Ordinal) | ||
| || container.Contains("alac", StringComparison.Ordinal) | ||
| || codec.Contains("alac", StringComparison.Ordinal) | ||
| || container.Contains("aiff", StringComparison.Ordinal) | ||
| || codec.Contains("aiff", StringComparison.Ordinal) | ||
| || container.Contains("ape", StringComparison.Ordinal) | ||
| || codec.Contains("ape", StringComparison.Ordinal) | ||
| || container.Contains("dsd", StringComparison.Ordinal) | ||
| || codec.Contains("dsd", StringComparison.Ordinal) | ||
| || container.Contains("wv", StringComparison.Ordinal) | ||
| || codec.Contains("wv", StringComparison.Ordinal) | ||
| || container.Contains("wav", StringComparison.Ordinal) | ||
| || codec.Contains("wav", StringComparison.Ordinal)) | ||
| private static bool IsCodecPrefixedLabel(string normalizedLabel) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we base this on the actual profile qualities instead of a fixed list of trusted labels? That would make custom qualities and future codecs work without needing to keep extending this method. |
||
| { | ||
| if (string.IsNullOrEmpty(normalizedLabel)) | ||
| { | ||
| return "lossless"; | ||
| return false; | ||
| } | ||
|
|
||
| return Normalize(file?.Format); | ||
| return normalizedLabel.StartsWith("mp3 ", StringComparison.Ordinal) | ||
| || normalizedLabel.StartsWith("aac ", StringComparison.Ordinal) | ||
| || normalizedLabel == "mp3 vbr" | ||
| || normalizedLabel == "flac"; | ||
| } | ||
|
|
||
| private static string Normalize(string? value) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So given this, shouldn't we also update the FE to remove the ability for users to create 96 kbps AAC/OPUS qualities? I think it would be confusing for it to get bucketed down to 64 here.