Skip to content

fix(downloads): make import quality comparison profile-aware (#582)#752

Open
jaredvincent414 wants to merge 3 commits into
Listenarrs:canaryfrom
jaredvincent414:fix-issue-582
Open

fix(downloads): make import quality comparison profile-aware (#582)#752
jaredvincent414 wants to merge 3 commits into
Listenarrs:canaryfrom
jaredvincent414:fix-issue-582

Conversation

@jaredvincent414

Copy link
Copy Markdown

Summary

When a completed download is imported for an audiobook that already has files, Listenarr is supposed to consult the audiobook's quality profile and skip the incoming file when it isn't an upgrade. That decision ran through ImportQualityEvaluator.IsAcceptable, which accepted a QualityProfile but never read it — it compared a regex-extracted numeric bitrate instead:

return !TryParseBitrate(candidate, out var c) || !TryParseBitrate(existing, out var e) || c >= e;

Any non-numeric quality (an existing FLAC) fails that parse and falls through to "acceptable", so a lossy MP3 gets imported on top of a lossless file and the audiobook ends up with both.

A profile-aware, lossless-aware comparator already exists in the domain — QualityMatcher — and the automatic-search path already uses it. Import was the only path still on the old regex evaluator. This PR routes import onto it.

Fixes #582. Related: #549 (quality determination algorithm).

Changes

Added

  • QualityMatcher.IsLossless(AudioQualityInput) — public wrapper over the existing private lossless check, so callers outside the profile path can still honour lossless-vs-lossy.
  • ImportQualityEvaluator.FromFile(...) / FromMetadata(...) — project an AudiobookFile and an incoming download's AudioMetadata into the shared AudioQualityInput vocabulary.
  • ImportQualityEvaluator.Describe(...) — readable quality label for the import skip log.
  • Four quality-gating tests: numeric upgrade, cross-codec (lossless-over-lossy) upgrade, equal-quality reimport, and unknown-quality/no-profile.

Changed

  • ImportQualityEvaluator.IsAcceptable now takes AudioQualityInput and ranks both sides through the profile's rung priority. When there is no profile, an empty profile, or a side the profile cannot rank, it falls back to codec/bitrate facts — lossy never lands on top of lossless, and genuinely unknown quality still imports rather than being silently dropped.
  • DownloadImportService picks the hardest-to-beat existing file via the same comparator, and gates the candidate through it.

Fixed

  • A lossy download no longer imports over an existing lossless file (Quality filters: Flaws in the current logic #582).
  • The "best existing quality" loop no longer hardcodes MP3 bitrate buckets ("MP3 320kbps" etc.), which mislabelled every non-MP3 file as MP3.

Removed

  • The regex TryParseBitrate decision helper.

Testing

  • dotnet test1020 passed, 0 failed.
  • cd fe && npm run test:unit — 389 passed; npm run type-check clean.
  • dotnet build — 0 warnings, 0 errors. Pre-commit (format, layering, async void) and pre-push (format, type-check, FE tests) hooks pass.

The reproduction is QualityGating_LosslessExisting_SkipsLossyDownload: an audiobook with an existing FLAC and a profile ranking FLAC above MP3 receives a lossy MP3 download. Before this change the MP3 was imported and the audiobook held two files; now it is skipped.

Worth being straight about the other four tests: they would also pass against the old code (its regex fell through to "acceptable" for any non-numeric label, so it imported in all four cases). They exist to guard against over-correction, not to prove the fix. The load-bearing one is QualityGating_EqualQuality_IsImported — it fails if the gate is ever tightened to the strictly-better rule search uses, which would skip every part of a multi-file audiobook whose quality merely matches what is already on disk.

Notes

Import compares "not worse"; search compares "strictly better". That asymmetry is deliberate and now documented in the code. Import must accept equal quality, or multi-file audiobooks would have every part skipped.

The delete/replace half of #582 is deliberately not here. The issue also proposes removing existing files when the incoming quality is better. That is a separate, riskier change and — per @Paelsmoessan's comment on the issue — is blocked on #542 (multi-file size aggregation), #736 (recycling bin, so a wrong quality call is not data loss), and #737 (imported files have no stored codec/bitrate). Deferring it keeps this PR to one bug fix.

Behaviour change for users with no quality profile. The fallback means lossy-over-lossless is now blocked even when no profile is configured. This is intended — previously nothing gated those users — but it is a real behaviour change worth flagging.

…rrs#582)

Import gating ran through ImportQualityEvaluator.IsAcceptable, which
accepted a QualityProfile but never read it. The decision was a regex
bitrate compare, so any non-numeric quality (an existing FLAC) failed
to parse and fell through to "acceptable" — letting a lossy MP3 import
on top of a lossless file and leaving the audiobook with both.

Route the decision through the profile-aware QualityMatcher that the
automatic-search path already uses. Both sides are projected into
AudioQualityInput and ranked by the profile's rung priority. When there
is no profile, an empty profile, or a side the profile cannot rank, fall
back to codec/bitrate facts so lossy never lands on top of lossless;
genuinely unknown quality still imports rather than being dropped.

Import compares as "not worse" (allow-equal) rather than the strictly
better rule used by search, so the parts of a multi-file audiobook that
match the quality already on disk still import.

Also drop the hardcoded MP3 bitrate buckets in DownloadImportService,
which mislabeled every non-MP3 file as MP3, and fix the repro test's
profile, whose priorities ranked MP3 above FLAC and so contradicted the
behavior the test asserts.
Add the cases that fence in the new profile-aware gate: a numeric
upgrade and a cross-codec (lossless-over-lossy) upgrade both import,
equal quality imports, and unknown quality with no profile imports.

The equal-quality case is the load-bearing one: it fails if the gate is
ever changed to the strictly-better rule that automatic search uses,
which would skip every part of a multi-file audiobook whose quality
merely matches what is already on disk.
@jaredvincent414
jaredvincent414 requested a review from a team July 12, 2026 02:32
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.

Quality filters: Flaws in the current logic

1 participant