Repository navigation
Conversation
c5e23e9 to
8026877
Compare
8026877 to
e9364ab
Compare
|
Rebased onto canary (a8a77aa), new head e9364ab. Two conflicts, both resolved by keeping both sides: The last commit re-scaffolds A database that ran an earlier head of this PR fails at startup with #918 also adds a migration ( Full backend suite: 4035 passed, 0 failed, 152 skipped; canary alone 3878/0/152. Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting. |
…d for A file encoded at a nominal tier reports a bitrate slightly under it (127241 bps for 128kbps), so rung eligibility dropped it to the tier below. Eligibility now goes through MeetsRung, which lets a file reach a rung it falls short of by up to five percent (floor of one kbps). One of the causes behind Listenarrs#549. Originally Listenarrs#934. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…qualities Maximum Age rendered only while Prefer newer releases was ticked, though the scorer applies it whenever it is above zero. And the profile watch runs immediately during setup and called helpers declared below it as const arrows, so every mount with a profile threw and showed an empty quality list. Both helpers are function declarations now. Fixes Listenarrs#895 Fixes Listenarrs#923 Originally Listenarrs#920. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tes one
The Torznab parsers labelled quality with bare substring checks
(Contains("320") down to Contains("64")), so "x264" became MP3 64kbps,
and the description scan overwrote the label the format attribute had
already set. SearchResultAttributeParser.DetectBitrateQuality now
reads a number as a bitrate only beside a rate word or unit, all
three call sites use it, and the description scan fills Quality
only when the attributes left it empty.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SearchResultScorer appended every PreferredFormats entry to the allowed list whatever its Allowed flag, matching by two-way substring, so with the default formats an unchecked MP3 or FLAC rung refused nothing. QualityGate now decides from the Allowed flags alone (rung name first, then codec group), PreferredFormats stays a score adjustment, and a release that declares only a format goes through the same gate. QualityMatcher maps MP4, AAX and AAXC to the AAC group. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lowed The quality profile API accepted any CutoffQuality, including blank, off-ladder and disallowed values. A cutoff must now name an allowed rung while upgrades are on, as in Readarr and Sonarr. Listenarr stored "upgrades off" as a blank cutoff, so the profile gains an UpgradeAllowed flag (default true); a migration adds the column and a startup repair turns it off for existing profiles whose cutoff is blank, so they keep their current behaviour. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integration fix for this bundle. Three of its fixes each add to QualityMatcher.cs and each stays under the 500-line cap enforced by BackendArchitectureTests.ActiveProductionSourceFiles_RemainFocused on its own, but together they reach 535 lines and fail that test. This moves the private codec, container and bitrate helpers, unchanged, into QualityMatcher.Codecs.cs as a partial of the same class, following the FileUtils.*.cs pattern already in listenarr.domain/Common. No behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ary did Integration fix to "fix(search): let a quality profile's Allowed flag refuse a release". That commit's gate returned NoOpinion for a label whose codec has no rung. A profile saved from the UI deletes a switched-off codec's rungs and stores PreferredFormats as [] or ["m4b"], so with that gate it stopped refusing FLAC and OPUS. Canary's allow-list (allowed rung names plus PreferredFormats tokens) refused them, measured on stock through the score endpoint. With no rung covering a label, the gate now passes it only where a PreferredFormats token names it, as canary did, and refuses it otherwise. The new tests were written first and seen failing without this change. The gate's comments are updated to match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ewest one AddQualityProfileUpgradeAllowed was scaffolded as 20261018212549, which now sorts before 20261021212418_AddAutomaticSearchTiming. It still applies to an existing database, but the newest migration's target model has no UpgradeAllowed column, so a later `dotnet ef migrations remove` restores a snapshot without it and the next scaffolded migration adds the column again. Regenerate it with `dotnet ef migrations add` against the current snapshot, so its Designer carries the full current model and its ID sorts after the newest one. Up and Down are unchanged and the snapshot is unchanged. The startup backfill gates on the new ID, the schema test lists it after AutomaticSearchTiming, and the migration test starts from AutomaticSearchTiming, which is now the migration immediately before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e9364ab to
63b7fcd
Compare
This bundle is about quality profiles and quality labels doing what their settings say: a file's real bitrate reaches the rung it was encoded for, the profile modal shows Maximum Age and loads a saved profile's qualities, a release's quality label only reads a bitrate the text actually states, an unchecked rung refuses a release, and the cutoff is validated on save with an
UpgradeAllowedflag beside it. It collects five fixes: two from my open PRs #934 and #920, and three that have not been opened as PRs before. Each fix is one commit, so any of them can be dropped or cherry-picked on its own. Two integration commits at the end are explained under merge order. The gate in commit 4 also changes which releases some existing profiles accept, and the biggest visible change is that a profile saved from the UI without Prefer M4B now accepts M4B releases. It comes first under default changes.2e5fa72e2d420f6e7381c0c6a463c251b5e4PreferredFormatsholds its codecf00e8c826UpgradeAllowedc52d15859QualityMatcher's private codec and bitrate helpers into a partial file, no behaviour changec5e23e9e1All file:line references below are at
a630572e9unless I say otherwise.1. A real encoder bitrate reaches its rung (
2e5fa72e2, from #934)QualityMatcher.NormalizeKbpsrounds to whole kbps (QualityMatcher.cs:390-398), and rung eligibility waswithBitrate.Where(r => r.BitrateKbps <= kbps)(:138). A file encoded at a nominal 128kbps reports something like 127241 bps, which rounds to 127, so the 128 rung is excluded and the file falls to the tier below. Where that tier is under the cutoff, the book reportsquality-mismatch. (Read.)Eligibility now goes through
MeetsRung, which lets a file reach a rung it falls short of by up to five percent, with a floor of one kbps. Ordinary adjacent rungs are at least a third apart, so I don't think the tolerance can carry a file across a real tier;Match_FileWellBelowATier_DoesNotGetPromotedToItpins that a 96kbps file still matches 64 against a 128/64 profile.Tests:
Match_FileJustUnderItsNominalTier_StillMatchesThatTier,Match_ReachesARungOnlyWithinTheTolerance,Match_KeepsAWholeKbpsOfSlackOnASmallRunginQualityMatcherTests, and twoComputeStatus_ReturnsQualityMatch_...tests inAudiobookStatusEvaluatorTestsso the user-visible status is covered. On the original branch,Match_FileJustUnderItsNominalTier_StillMatchesThatTierandComputeStatus_ReturnsQualityMatch_ForARealEncoderBitrateJustUnderTheCutoffwere seen failing without the change.This is one cause of #549, not all of it. The other one I found is that
AudiobookStatusEvaluatorfilters files byPreferredFormatsbefore the matcher runs, so an MP3-only book against an m4b-onlyPreferredFormatsreports a mismatch at any bitrate. I left that alone because I think redefining that status is your call. Full write-up in #934.2. Maximum Age, and loading a profile's qualities (
d420f6e73, from #920)Maximum Age (Days)rendered behindv-if="formData.preferNewerReleases"(QualityProfileFormModal.vue:479), butSearchResultScorerrejects onprofile.MaximumAgewhenever it is above zero (SearchResultScorer.cs:153and:171) and never readsPreferNewerReleases. Unticking the box hid the input while the filter stayed on. The input now always renders. (Read.)Separately, the watch that copies a profile into the form runs with
{ immediate: true }(:892) and callsinitializeQualitiesFromProfile(:858), which was aconstarrow declared below it (:898), withparseQualityString(:950) the same shape one level down. Every mount with a profile threwReferenceError, Vue swallowed it, and the quality list came up empty, so saving was refused until the ladder was picked again. Both are function declarations now. That is #923.Tests: a new
QualityProfileFormModal.spec.tswith four tests. On the original branch, re-hiding Maximum Age failed one, and putting either helper back to aconstarrow failed all four.Left alone:
PreferNewerReleasesstill has no backend reader. Full write-up in #920.3. Read a bitrate only where the text states one (
81c0c6a46, new)The description branch of the Torznab parser builds
title + descriptionand runs bareContainschecks,"320"down toContains("64")(TorznabResponseParser.cs:350-367, the64check at:362), whichx264satisfies. It also assignsQualityunconditionally, overwriting what theformatattr set at:142-149. The same chain is inTorznabNewznabSearchProvider.Parsing.cs:343-358andSearchResultAttributeParser.DetectQualityFromTags(:41-49). (Read.)Measured on a throwaway stock
ghcr.io/listenarrs/listenarr:canarycontainer (Listenarr.Api/1.3.4) with a stub Torznab indexer, reading labels back fromGET /api/v1/search/{indexerId}:<description>The Governor x264 WEBRip, format m4bMP3 64kbpsM4BThe Governor MP3 64kbps, format mp3MP3 64kbpsAgainst a profile with M4B allowed and MP3 64kbps unchecked, the described copy scored 30 and was rejected; the undescribed copy scored 100 and was accepted.
The change:
SearchResultAttributeParser.DetectBitrateQualitycounts a number as a bitrate only beside a rate word (mp3 320,MP3CBR320) or a rate unit (320kbps,[64k]), or as a bracketed tier when the text also says MP3. All three call sites use it, and the description scan now fillsQualityonly when the attributes left it empty.MP3 320 MBstill reads as 320kbps, and a test pins that.On an image built from the original branch, which ran the same code as commits 3 and 4 here, the same run gave
M4Bfor the described x264 release and both controls came out unchanged. Tests:BitrateTokenQualityTestsand additions toIndexersNewznabParsingTests.4. An unchecked rung refuses (
3c251b5e4, new)The scorer builds the allowed list from the Allowed rungs and then appends every
PreferredFormatsentry whatever its flag (SearchResultScorer.cs:287-297), and the match is a two-way substring test (:300). With the default formats (m4b,mp3,m4a,flac,opus,QualityProfile.cs:59), any label containing one of them passes. A release with no quality label takes the branch at:264and never reaches the veto. (Read.)Measured on the same stock container, straight to
POST /api/v1/qualityprofile/{id}/score, profile with FLAC and MP3 320kbps unchecked, M4B and AAC allowed:preferredFormatspreferredFormatsempty (control)This commit adds a
QualityGatethat decides from the Allowed flags. A rung name match decides first, then the codec group, soM4Bis judged by the AAC rungs.PreferredFormatsstays a score adjustment. A release that declares only a format goes through the same gate.QualityMatchernow mapsMP4,AAXandAAXCto the AAC group, so those labels follow the AAC rungs. That tightens a profile that refuses every AAC rung and loosens one where any AAC rung is allowed, the stock default included, because canary refused all three on any profile with a ladder. No shipped parser emits them today, as far as I can tell. The mapping also reaches cutoff matching for an.aaxfile (read; not measured on the import side). Both effects are listed under default changes. Blank-named rungs are ignored, since one allowed blank rung used to permit every label throughContains("").On an image built from the original branch, whose code is the same as this commit apart from the later
QualityMatcherfile split, the default-preferredFormatsprofile rejected FLAC and MP3 320kbps withnot allowed by profileand still accepted M4B; the empty-preferredFormatscontrol rejected them on both builds.A codec the ladder has no rung for is handled by
c5e23e9e1, below. Not changed: Usenet results still skip the quality checks, andImportQualityEvaluator.cs:14-17still runs the same bareContainschain on file names. Tests:QualityAllowedGateTests,QualityGateTests, additions toQualityMatcherTests.Integration fix to the gate (
c5e23e9e1)With commit 4 alone, the gate stayed silent on a label whose codec has no rung, so the release went through. That gives the right answer for the stock default profile but the wrong one for a profile saved from the UI. Switching a codec off in the UI deletes its rungs (
QualityProfileFormModal.vue:600-602), and the form savesPreferredFormatsas[]or['m4b'](:1185). Canary's allow-list was the allowed rung names plus thePreferredFormatstokens (SearchResultScorer.cs:287-300), so on such a profile it refused FLAC and OPUS. Measured on stock canary (ghcr.io/listenarrs/listenarr:canary, digestsha256:216f95f37d0179fc8c2638f24c599ed09d18c808a3d727d8a22b4bf8a2bff9c4,Listenarr.Api/1.3.4), throughPOST /api/v1/qualityprofile/{id}/score, with the eleven seeded AAC and MP3 rungs all ticked:preferredFormats[]['m4b']['m4b'], every AAC rung untickedMP3 320kbps/ MP3 (control)FLAC/ FLACOPUS/ OPUSM4B/ M4BWith commit 4 alone, the first profile's FLAC release was accepted. I measured that with an in-process test (
QualityProfileService.ScoreSearchResult, the method behind that endpoint), not on an image:FLAC must be refused by a ladder with no FLAC rung (score 100). So switching FLAC off in the UI would have stopped refusing FLAC.c5e23e9e1keeps canary's answer there. When no rung covers a label, the gate lets it through only if aPreferredFormatstoken names it (the same two-way substring test canary used), and refuses it otherwise. The stock default profile keeps the domain's five tokens, so FLAC and OPUS stay permitted there. A UI-saved profile goes back to refusing them. The tests,UiSavedProfile_StillRefusesACodecItsLadderDoesNotCarryandACodecTheLadderDoesNotCarry_IsRefusedUnlessAPreferredFormatNamesIt, were written first and seen failing without this change. I have not re-measured this commit on an image.5. Validate the cutoff on save, and add
UpgradeAllowed(f00e8c826, new)The API does not validate
CutoffQuality. Measured on a throwaway stock container, POSTing a profile with MP3 64 and 128 allowed and 320 present but not allowed: cutoffs of"MP3 128kbps","",null,"\t","FLAC"and"MP3 320kbps"all came back 201. The control, the same body with an emptyname, came back 400, so the request does reach model validation. The UI already refuses a blank or disabled cutoff while upgrades are on (QualityProfileFormModal.vue:1197-1209), so these only get in through the API. Readarr and Sonarr refuse them with the same one-line check (Readarrsrc/Readarr.Api.V1/Profiles/Quality/QualityCutoffValidator.cs:18-28at0b79d3000).The validation can't go in alone. Listenarr stores "upgrades off" as a blank cutoff: the save path clears it (
QualityProfileFormModal.vue:1210-1213) and the modal infers the checkbox back from!!cutoffQuality(:861). With only the rule, an upgrades-off profile could not survive GET then PUT. So the commit also addsQualityProfile.UpgradeAllowed, as Readarr and Sonarr have it (src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs:17in both), and the rule only applies while it is true.What changes, read at the bundle head:
ValidCutoffAttributeonCutoffQuality, which passes whenUpgradeAllowedis false;QualityMatcher.ResolveCutofftreats upgrades-off like a blank cutoff (QualityMatcher.cs:341); migration20260920025621_AddQualityProfileUpgradeAllowed, one non-nullableAddColumn<bool>with default true and no SQL; a startup repair,ListenarrDatabaseMigrationPreflight.RepairQualityProfileUpgradeFlags(:121), that sets the flag false on every profile whose cutoff is blank bystring.IsNullOrWhiteSpace(:136); and the frontend binds the checkbox to the flag and drops the "No Cutoff (Always Upgrade)" option, which could only produce a 400 now. That last point touches #519, which asks for the opposite, and I would rather you decided it than this diff.Measured on an image built from the original branch, which ran the same code as this commit (on canary, without the bundle's other commits), started on a copy of the stock container's database: the startup log said it turned upgrades off on 3 profiles. The blank, null and tab cutoffs came out with
UpgradeAllowed0, and the profile with a real cutoff came out 1, as the control. The same POSTs then gave 201 for the allowed cutoff and 400 withCutoff must be an allowed quality or groupfor the other five. Those POSTs did not sendupgradeAllowed, so it defaulted to true: a client that leaves the field out and sends a blank cutoff now gets a 400 where it used to get a 201. A blank cutoff with"upgradeAllowed": falsegave 201.Not fixed:
AudiobookQualityCutoffEvaluator.cs:59-66still treats an unset cutoff as "keep searching", so I'm not claiming this changes #965. A profile already stored with an off-ladder cutoff and upgrades on still loads, but can't be saved back unchanged until a valid cutoff is picked. Tests:QualityProfileController_CutoffValidationTests,QualityProfileCutoffValidationTests,QualityProfileUpgradeAllowedMigrationTests,AudiobookQualityCutoffEvaluator_UpgradeAllowedTests, andQualityProfileUpgradeAllowed.spec.tson the frontend.Merge order and dependencies
The commits are in the order above. Each one also merges cleanly onto
a630572e9on its own (git merge-treewith that commit's parent as base, exit 0 for all five), so I don't think any of them needs another to apply. The two integration commits are the exception and are described below. Commits 1, 4 and 5 all editQualityMatcher.cs, and 2 and 5 both editQualityProfileFormModal.vue; they merged without conflicts here. Commit 4 gates on the label commit 3 assigns, so I think 4 without 3 would apply but gate on the old, looser label (inferred). I have built and tested the five together, not each one alone.c52d15859exists only because of the bundle. Commits 1, 4 and 5 each keepQualityMatcher.csunder the 500-line cap thatBackendArchitectureTests.ActiveProductionSourceFiles_RemainFocusedenforces, but together they take it to 535 lines, and that test failed on the bundle without this commit (measured: 1 failed, the rest passed). It moves the private codec, container and bitrate helpers unchanged intoQualityMatcher.Codecs.csas a partial of the same class, the wayFileUtils.*.csis already split inlistenarr.domain/Common. It depends on the commits before it and does not apply on its own; if you drop any of 1, 4 or 5, it can be dropped too as long as the file stays under 500 lines.c5e23e9e1is a fix to commit 4 and only makes sense with it. It is a separate commit so the review can see the change on its own, and it could be squashed into commit 4.Default changes
Commit 4 with
c5e23e9e1, the Allowed gateI think the largest visible change is to
M4Breleases. A profile saved from the UI without Prefer M4B hasPreferredFormats[](QualityProfileFormModal.vue:1185, form default[]at:795). On stock canary that profile refused every release labelledM4B: 75, rejected, in the table above. No rung name or token containsm4b, and the required rungs are allAAC ...andMP3 ...(QualityProfileService.cs:75-85). With this bundle anM4Brelease is judged by the profile's AAC rungs, so it is accepted whenever one of them is ticked, which is the default. I measured that in-process (UiSavedProfile_StillRefusesACodecItsLadderDoesNotCarrypins it), not on an image.Beyond that, the gate makes a rung's Allowed flag take effect where it was ignored. A rung's domain default is
Allowed = true(QualityProfile.cs:132), the required rungs are re-added withAllowed = true(QualityProfileService.cs:97), and in the UI the rungs for a newly enabled codec start ticked (QualityProfileFormModal.vue:626,:638,:650). The domain default forPreferredFormatsism4b,mp3,m4a,flac,opus(QualityProfile.cs:59). The UI default is[], or['m4b']with Prefer M4B.Newly refused, and who is affected:
PreferredFormatstokens. With the domain's default list, an unticked MP3 or FLAC rung refused nothing on stock (measured, section 4) and now refuses. That mostly reaches the seeded default profile and profiles created through the API without the field. I don't think a UI-saved profile is affected, becausem4bnames no rung (read).M4Brelease against a['m4b']profile with every AAC rung unticked. On stock it was accepted (96, in the table above), because them4btoken let it through. Now it is judged by the AAC rungs and refused (read, from the gate's codec-group rule; not measured on an image).M4B,FLAC,OPUS,AACorMP3in that field (TorznabResponseParser.cs:135-139,SearchResultAttributeParser.DetectFormatFromTags, and the same chain inTorznabNewznabSearchProvider.Parsing.cs), so I don't think the "can't place it" refusal (a formatCodecGroupOfLabelreturns null for, such asEPUB) is reachable through a shipped indexer today. The Internet Archive provider's other format strings (128Kbps MP3,Ogg Vorbisand so on) always come with a quality (InternetArchiveRepresentationPlanner.cs:281-295at the bundle head), so the format-only gate never sees them. That is read, not measured.Newly accepted:
M4Bon a[]profile, above.AAX,AAXCandMP4labels on any profile with an allowed AAC rung, the stock default included. Canary refused all three on any profile with a ladder, because no rung name or default token contains them.TheMpeg4ContainerLabels_AreGatedOnTheAacRungspins the new answer.M4Agoes the same way on[]and['m4b']profiles, and a label likeM4B 128kbpson[]profiles, where canary had no token to let them through. As far as I can tell no shipped parser emits any of these labels today, so I think this is unreachable for now (read).FLAC and OPUS against a ladder without their rungs are refused or accepted exactly as on stock, depending on whether a token names them.
.aaxfiles:QualityMatchernow mapsAAXandAAXCto the AAC group (QualityMatcher.Codecs.cs,MapCodec,ParseQualityLabel,CanonicalCodecat the bundle head; canary'sMapCodecatQualityMatcher.cs:366-370has noaax). Cutoff matching goes throughMapCodec(AudiobookQualityCutoffEvaluator.cs:95,AudiobookStatusEvaluator.cs:125at the bundle head). So an existing.aaxfile identified only by its extension or container, with no codec recorded, can now meet an AAC cutoff, where before it was a codec mismatch and kept the book in the cutoff-unmet set to be searched again. An.aaxwhose probe recordedaacas the codec was already matched. Read, not measured.Readarr and Sonarr reject a release whose quality item is not Allowed, with nothing able to override it: Readarr
src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs:27-31at0b79d3000, Sonarr the same file:26-30at76c684e09. Their ladders list every quality, so there is no "no rung for this codec" case for them to decide (read).Commit 5, the cutoff rule
This makes
CutoffQualityvalidated where it was accepted as anything. Its domain default isnull(QualityProfile.cs:44), and the form's default is''(QualityProfileFormModal.vue:792) with the UI already refusing to save a blank or disabled cutoff while upgrades are on (:1197-1209). So the UI's behaviour doesn't change; what changes is that an API client sending a blank, off-ladder or disallowed cutoff with upgrades on now gets a 400 instead of a 201 (measured above). Readarr and Sonarr apply the same rule on every save (Readarrsrc/Readarr.Api.V1/Profiles/Quality/QualityProfileController.cs:25andQualityCutoffValidator.cs:18-28at0b79d3000; Sonarr the same files and lines undersrc/Sonarr.Api.V3at76c684e09), and theirs also applies with upgrades off. This bundle skips it when upgrades are off, because a blank cutoff is how every existing upgrades-off profile is stored.Commit 5,
UpgradeAllowedHere, new profiles default to true (
QualityProfile.cs:72at the bundle head, and the modal's form default). Existing profiles get the column default of true from the migration, and the startup repair then sets false wherever the cutoff is blank, so each existing profile keeps the upgrade behaviour it has today.The family, all read, not run:
public bool UpgradeAllowed { get; set; }with no initializer (src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs:17in both).GetDefaultProfiledoes not set it (Readarrsrc/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs:192-200at0b79d3000, Sonarr:259-268at76c684e09), and the new-profile schema comes fromGetDefaultProfile(QualityProfileSchemaController.cs:20in both). So a new profile there starts with upgrades off.src/NzbDrone.Core/Datastore/Migration/128_rename_quality_profiles_add_upgrade_allowed.cs:16-18at76c684e09).src/NzbDrone.Core/Books/Services/BookCutoffService.cs:34andsrc/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs:99and:171-175; Sonarrsrc/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs:60-64,:126and:201-205. In this bundle, upgrades-off counts as a met cutoff (QualityMatcher.cs:341), which I think comes to the same answer for an allowed file.I kept true for new profiles because today's upgrades checkbox starts on for a new profile (
QualityProfileFormModal.vue:572), and I didn't want to quietly change what it does. If you'd rather match the family and start new profiles with upgrades off, it's a one-line change and I'm happy to make it.Commits 1 to 3
These change no settings and make none newly take effect. Commit 2 only stops hiding Maximum Age; the scorer already enforced it whenever it was above zero (
SearchResultScorer.cs:153), and its form default stays 0 (QualityProfileFormModal.vue:804). Commit 1 changes which rung a file just under a tier matches, and commit 3 changes which label a release gets, both described above.Overlap with other open PRs
Checked with
git merge-treeagainst every open PR whose head still merges intoa630572e9. Your own #579, #611 and #841 don't merge intoa630572e9themselves at the moment, so I left them out of this check. #841 also editsListenarrDatabaseMigrationPreflight.csandInfrastructureStartupCompositionExtensions.cs, the two files commit 5's startup repair touches, so whichever lands second will need those resolved.MigrationMetadataTests.cs,SqliteMigrationSchemaTests.csandfe/src/types/index.ts. It conflicts only inSqliteMigrationSchemaTests.cs, where both add a line to the migration id list; whichever lands second needs a rebase, happy to do it.ListenArrDbContextModelSnapshot.csandfe/src/types/index.ts, and feat(search): configurable automatic-search sweep interval, per-book cooldown and book delay #714 the snapshot; all three merge cleanly.fe/src/types/index.ts; all merge cleanly.ListenArrDbContextModelSnapshot.cs. feat(import): embed cover art into imported files, behind a setting #914 merges cleanly with this bundle; feat(downloads): blocklist a release that failed, so the retry stops #918 conflicts only in the migration id list inSqliteMigrationSchemaTests.cs. A clean text merge of the snapshot isn't the same as a correct one, so I think whichever migration lands second should have its migration and snapshot regenerated against the other (inferred, I have not run that). Happy to do it for whichever lands second.Tests
At the bundle head, against stock canary at
a630572e9on the same machine:If you'd rather take any of these as separate PRs, I'll split them back out.
Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.