Summary
Audiobooks always show quality-mismatch even when the file matches the profile's cutoff quality, because AudiobookStatusEvaluator.DeriveQualityLabel returns labels (e.g. "320kbps", "lossless") that never appear as keys in QualityProfile.Qualities (e.g. "MP3 320kbps", "FLAC").
Repro (canary 0.2.73, commit 6fe911a0)
- Create a QP via
POST /api/v1/qualityprofile with a quality { "quality": "MP3 320kbps", ... } and cutoffQuality: "MP3 320kbps".
- Import an audiobook whose only file is a 320 kbps MP3.
- Trigger a library scan. Expected:
status: "quality-match". Actual: status: "quality-mismatch".
Same symptom for FLAC files against a QP with quality "FLAC": derived label is "lossless", profile key is "flac" → no hit.
Root cause
listenarr.api/Services/AudiobookStatusEvaluator.cs lines 105–138 build qualityPriority keyed on Normalize(quality.Quality) — i.e. the lowercased profile-quality string such as "mp3 320kbps" or "flac".
DeriveQualityLabel (lines 141–192) produces:
"320kbps", "256kbps", "192kbps", "<rounded>kbps" for lossy files (no codec prefix), and
"lossless" for FLAC/ALAC/etc (no codec name).
qualityPriority.TryGetValue therefore always misses, returns int.MaxValue, the cutoff comparison fails, and every file falls through to return QualityMismatch; on line 138.
Impact
- Every audiobook in the library reports
quality-mismatch unless the legacy audiobookQuality column is already populated (rare for fresh imports).
- The "Below Cutoff" UI label is meaningless — it fires for files that perfectly meet the cutoff.
- Affects all default profiles seeded via the UI as well as profiles created via API.
Proposed fix
DeriveQualityLabel should produce labels that round-trip with QualityProfile.Qualities[i].Quality:
- For lossless containers/codecs, return the codec name itself:
"flac", "alac", "wav", etc., not "lossless".
- For lossy files, return
"<codec> <bucket>kbps" with the codec inferred from Codec/Container/Format ("mp3 320kbps", "aac 192kbps", "opus 128kbps").
Patch (running locally as a test):
private static string DeriveQualityLabel(AudiobookFileStatusInfo? file, string? audiobookQuality)
{
var normalizedAudiobookQuality = Normalize(audiobookQuality);
if (normalizedAudiobookQuality.Length > 0) return normalizedAudiobookQuality;
var container = Normalize(file?.Container);
var codec = Normalize(file?.Codec);
var format = Normalize(file?.Format);
if (container.Contains("flac", StringComparison.Ordinal) || codec.Contains("flac", StringComparison.Ordinal)) return "flac";
if (container.Contains("alac", StringComparison.Ordinal) || codec.Contains("alac", StringComparison.Ordinal)) return "alac";
if (container.Contains("wav", StringComparison.Ordinal) || codec.Contains("wav", StringComparison.Ordinal)) return "wav";
// ... (aiff/ape/dsd/wavpack analogous)
string codecName = "";
if (codec.Contains("mp3") || container.Contains("mp3") || format.Contains("mp3")) codecName = "mp3";
else if (codec.Contains("aac") || container.Contains("m4a") || container.Contains("m4b") || container.Contains("mp4")) codecName = "aac";
else if (codec.Contains("opus") || container.Contains("opus")) codecName = "opus";
else if (codec.Contains("vorbis") || container.Contains("ogg")) codecName = "ogg vorbis";
if (file?.Bitrate is int bitrate)
{
var kbps = bitrate >= 1000 ? bitrate / 1000d : bitrate;
int bucket = kbps >= 320 ? 320 : kbps >= 256 ? 256 : kbps >= 192 ? 192 : kbps >= 128 ? 128 : kbps >= 96 ? 96 : kbps >= 64 ? 64 : (int)Math.Round(kbps);
return codecName.Length > 0 ? $"{codecName} {bucket}kbps" : $"{bucket}kbps";
}
return codecName.Length > 0 ? codecName : format;
}
Bucket cutoffs match the existing schema seeded by qualityprofile/default-quality-formats.
Happy to send a PR if the above approach is acceptable. Currently testing this patched evaluator against a 48-book library on canary 0.2.73.
Summary
Audiobooks always show
quality-mismatcheven when the file matches the profile's cutoff quality, becauseAudiobookStatusEvaluator.DeriveQualityLabelreturns labels (e.g."320kbps","lossless") that never appear as keys inQualityProfile.Qualities(e.g."MP3 320kbps","FLAC").Repro (canary
0.2.73, commit6fe911a0)POST /api/v1/qualityprofilewith a quality{ "quality": "MP3 320kbps", ... }andcutoffQuality: "MP3 320kbps".status: "quality-match". Actual:status: "quality-mismatch".Same symptom for FLAC files against a QP with quality
"FLAC": derived label is"lossless", profile key is"flac"→ no hit.Root cause
listenarr.api/Services/AudiobookStatusEvaluator.cslines 105–138 buildqualityPrioritykeyed onNormalize(quality.Quality)— i.e. the lowercased profile-quality string such as"mp3 320kbps"or"flac".DeriveQualityLabel(lines 141–192) produces:"320kbps","256kbps","192kbps","<rounded>kbps"for lossy files (no codec prefix), and"lossless"for FLAC/ALAC/etc (no codec name).qualityPriority.TryGetValuetherefore always misses, returnsint.MaxValue, the cutoff comparison fails, and every file falls through toreturn QualityMismatch;on line 138.Impact
quality-mismatchunless the legacyaudiobookQualitycolumn is already populated (rare for fresh imports).Proposed fix
DeriveQualityLabelshould produce labels that round-trip withQualityProfile.Qualities[i].Quality:"flac","alac","wav", etc., not"lossless"."<codec> <bucket>kbps"with the codec inferred fromCodec/Container/Format("mp3 320kbps","aac 192kbps","opus 128kbps").Patch (running locally as a test):
Bucket cutoffs match the existing schema seeded by
qualityprofile/default-quality-formats.Happy to send a PR if the above approach is acceptable. Currently testing this patched evaluator against a 48-book library on canary
0.2.73.