From abff028ba18598602d01cfea87a0f76cc3324e4c Mon Sep 17 00:00:00 2001 From: Flaminel Date: Mon, 26 Oct 2026 15:58:09 +0200 Subject: [PATCH] added release gate setting to seeker ui --- .../core/feature-badges/feature-registry.ts | 1 + .../app/core/models/search-stats.models.ts | 1 + .../core/services/documentation.service.ts | 1 + .../searches-tab/search-display.util.ts | 1 + .../settings/seeker/seeker.component.html | 7 +++ .../settings/seeker/seeker.component.spec.ts | 49 ++++++++++++++++++- .../settings/seeker/seeker.component.ts | 15 +++++- code/frontend/src/app/shared/models/enums.ts | 5 ++ .../app/shared/models/seeker-config.model.ts | 4 +- 9 files changed, 81 insertions(+), 3 deletions(-) diff --git a/code/frontend/src/app/core/feature-badges/feature-registry.ts b/code/frontend/src/app/core/feature-badges/feature-registry.ts index 346f7b8f..fa40331e 100644 --- a/code/frontend/src/app/core/feature-badges/feature-registry.ts +++ b/code/frontend/src/app/core/feature-badges/feature-registry.ts @@ -66,4 +66,5 @@ export const NEW_FEATURES: NewFeature[] = [ { id: 'seeker-cf-gap-strategy' }, { id: 'seeker-unit-rest' }, { id: 'seeker-quiet-hours' }, + { id: 'seeker-radarr-release-gate' }, ]; diff --git a/code/frontend/src/app/core/models/search-stats.models.ts b/code/frontend/src/app/core/models/search-stats.models.ts index 3a7d35f6..74db2aa9 100644 --- a/code/frontend/src/app/core/models/search-stats.models.ts +++ b/code/frontend/src/app/core/models/search-stats.models.ts @@ -87,6 +87,7 @@ export interface SearchEvent { export enum SeekerExclusionReason { NotReleased = 'NotReleased', InGracePeriod = 'InGracePeriod', + NotAvailable = 'NotAvailable', NotMonitored = 'NotMonitored', SkipTag = 'SkipTag', MissingIncludeTag = 'MissingIncludeTag', diff --git a/code/frontend/src/app/core/services/documentation.service.ts b/code/frontend/src/app/core/services/documentation.service.ts index 31a51e12..d1c43c1e 100644 --- a/code/frontend/src/app/core/services/documentation.service.ts +++ b/code/frontend/src/app/core/services/documentation.service.ts @@ -252,6 +252,7 @@ export class DocumentationService { 'searchSpecials': 'instance-search-for-specials', 'searchAnime': 'instance-search-for-anime', 'sonarrSearchMode': 'instance-search-mode', + 'radarrReleaseGate': 'instance-release-gate', }, }; diff --git a/code/frontend/src/app/features/seeker-stats/searches-tab/search-display.util.ts b/code/frontend/src/app/features/seeker-stats/searches-tab/search-display.util.ts index 981ec34b..9e057f9f 100644 --- a/code/frontend/src/app/features/seeker-stats/searches-tab/search-display.util.ts +++ b/code/frontend/src/app/features/seeker-stats/searches-tab/search-display.util.ts @@ -106,6 +106,7 @@ export function funnelStages(funnel: InstanceFunnel, instanceType: string): { na export const EXCLUSION_REASON_LABELS: Record = { [SeekerExclusionReason.NotReleased]: 'Not released', [SeekerExclusionReason.InGracePeriod]: 'In grace period', + [SeekerExclusionReason.NotAvailable]: 'Not available in Radarr', [SeekerExclusionReason.NotMonitored]: 'Not monitored', [SeekerExclusionReason.SkipTag]: 'Skip tag', [SeekerExclusionReason.MissingIncludeTag]: 'No include tag', diff --git a/code/frontend/src/app/features/settings/seeker/seeker.component.html b/code/frontend/src/app/features/settings/seeker/seeker.component.html index 186e8f52..733475bb 100644 --- a/code/frontend/src/app/features/settings/seeker/seeker.component.html +++ b/code/frontend/src/app/features/settings/seeker/seeker.component.html @@ -130,6 +130,13 @@ helpKey="seeker:searchAnime" featureId="seeker-search-anime" /> } + @if (instance.instanceType === 'Radarr') { + + } { ) as HTMLElement | undefined; } + function releaseGateSelect(fixture: ComponentFixture, row: number): HTMLElement | undefined { + const details = fixture.nativeElement.querySelectorAll('.instance-row__details')[row] as HTMLElement; + return Array.from(details.querySelectorAll('app-select')).find( + (el) => el.textContent!.includes('Release Gate'), + ) as HTMLElement | undefined; + } + function text(fixture: ComponentFixture): string { return fixture.nativeElement.textContent as string; } @@ -514,6 +523,42 @@ describe('SeekerComponent', () => { expect(sent.instances[1].sonarrSearchMode).toBe(SonarrSearchMode.Auto); }); + it('shows the release gate select for Radarr only', () => { + const { fixture } = setup(); + + expect(releaseGateSelect(fixture, 0)).toBeUndefined(); + + const radarrToggle = fixture.nativeElement.querySelectorAll('.instance-row__header [role="switch"]')[1] as HTMLButtonElement; + radarrToggle.click(); + fixture.detectChanges(); + + expect(releaseGateSelect(fixture, 1)).toBeDefined(); + }); + + it('sends the picked release gate for the Radarr row only', () => { + const { fixture, updateConfig } = setup(); + + const radarrToggle = fixture.nativeElement.querySelectorAll('.instance-row__header [role="switch"]')[1] as HTMLButtonElement; + radarrToggle.click(); + fixture.detectChanges(); + + const select = releaseGateSelect(fixture, 1)!; + (select.querySelector('.select-trigger') as HTMLButtonElement).click(); + fixture.detectChanges(); + const option = Array.from(select.querySelectorAll('.select-option')).find( + (el) => el.textContent!.trim() === 'Follow Radarr availability', + ) as HTMLButtonElement; + option.click(); + fixture.detectChanges(); + + saveButton(fixture).click(); + fixture.detectChanges(); + + const sent = updateConfig.mock.calls[0][0] as UpdateSeekerConfig; + expect(sent.instances[0].radarrReleaseGate).toBe(RadarrReleaseGate.ReleaseDate); + expect(sent.instances[1].radarrReleaseGate).toBe(RadarrReleaseGate.ArrAvailability); + }); + it('patches the instance limits typed into the numeric inputs', () => { const { fixture, component } = setup(); @@ -763,6 +808,7 @@ describe('SeekerComponent', () => { useCutoff: false, useCustomFormatScore: false, sonarrSearchMode: SonarrSearchMode.Auto, + radarrReleaseGate: RadarrReleaseGate.ReleaseDate, }, { arrInstanceId: RADARR.arrInstanceId, @@ -779,6 +825,7 @@ describe('SeekerComponent', () => { useCutoff: true, useCustomFormatScore: true, sonarrSearchMode: SonarrSearchMode.Auto, + radarrReleaseGate: RadarrReleaseGate.ReleaseDate, }, ], }); diff --git a/code/frontend/src/app/features/settings/seeker/seeker.component.ts b/code/frontend/src/app/features/settings/seeker/seeker.component.ts index 3c9fa02e..28476013 100644 --- a/code/frontend/src/app/features/settings/seeker/seeker.component.ts +++ b/code/frontend/src/app/features/settings/seeker/seeker.component.ts @@ -16,7 +16,7 @@ import { HasPendingChanges } from '@core/guards/pending-changes.guard'; import { ApiError } from '@core/interceptors/error.interceptor'; import { createSettingsResource } from '@shared/utils/settings-resource.util'; import { createDirtyTracker, SAVED_FLASH_MS } from '@shared/utils/dirty-tracker.util'; -import { SelectionStrategy, SonarrSearchMode } from '@shared/models/enums'; +import { RadarrReleaseGate, SelectionStrategy, SonarrSearchMode } from '@shared/models/enums'; import { instanceTypeSeverity } from '@shared/utils/instance-display.util'; const INTERVAL_OPTIONS: SelectOption[] = [ @@ -53,6 +53,11 @@ const SONARR_SEARCH_MODE_OPTIONS: SelectOption[] = [ { label: 'Episode', value: SonarrSearchMode.Episode }, ]; +const RADARR_RELEASE_GATE_OPTIONS: SelectOption[] = [ + { label: 'Release dates', value: RadarrReleaseGate.ReleaseDate }, + { label: 'Follow Radarr availability', value: RadarrReleaseGate.ArrAvailability }, +]; + const STRATEGY_DESCRIPTIONS: Record = { [SelectionStrategy.BalancedWeighted]: 'Prioritizes items that are both newly added and haven\'t been searched recently. Good default for most libraries.', [SelectionStrategy.OldestSearchFirst]: 'Picks the items Sonarr or Radarr searched longest ago, never-searched items first. Manual searches and other tools count too.', @@ -94,6 +99,7 @@ interface InstanceState { useCutoff: boolean; useCustomFormatScore: boolean; sonarrSearchMode: SonarrSearchMode; + radarrReleaseGate: RadarrReleaseGate; } @Component({ @@ -122,6 +128,7 @@ export class SeekerComponent implements HasPendingChanges { readonly intervalOptions = INTERVAL_OPTIONS; readonly strategyOptions = STRATEGY_OPTIONS; readonly sonarrSearchModeOptions = SONARR_SEARCH_MODE_OPTIONS; + readonly radarrReleaseGateOptions = RADARR_RELEASE_GATE_OPTIONS; readonly loader = this.settings.loader; readonly loadError = this.settings.loadError; readonly saving = signal(false); @@ -220,6 +227,7 @@ export class SeekerComponent implements HasPendingChanges { useCutoff: i.useCutoff, useCustomFormatScore: i.useCustomFormatScore, sonarrSearchMode: i.sonarrSearchMode, + radarrReleaseGate: i.radarrReleaseGate, }))); this.dirtyTracker.markSaved(); }); @@ -278,6 +286,10 @@ export class SeekerComponent implements HasPendingChanges { this.patchInstance(index, { sonarrSearchMode: mode as SonarrSearchMode }); } + updateInstanceRadarrReleaseGate(index: number, gate: unknown): void { + this.patchInstance(index, { radarrReleaseGate: gate as RadarrReleaseGate }); + } + getInstanceIcon(instanceType: string): string { return `icons/ext/${instanceType.toLowerCase()}-light.svg`; } @@ -311,6 +323,7 @@ export class SeekerComponent implements HasPendingChanges { useCutoff: i.useCutoff, useCustomFormatScore: i.useCustomFormatScore, sonarrSearchMode: i.sonarrSearchMode, + radarrReleaseGate: i.radarrReleaseGate, })), }; diff --git a/code/frontend/src/app/shared/models/enums.ts b/code/frontend/src/app/shared/models/enums.ts index f6b1eca3..fb53d6a4 100644 --- a/code/frontend/src/app/shared/models/enums.ts +++ b/code/frontend/src/app/shared/models/enums.ts @@ -128,6 +128,11 @@ export enum SonarrSearchMode { Episode = 'Episode', } +export enum RadarrReleaseGate { + ReleaseDate = 'ReleaseDate', + ArrAvailability = 'ArrAvailability', +} + export enum SearchCommandStatus { Pending = 'Pending', Started = 'Started', diff --git a/code/frontend/src/app/shared/models/seeker-config.model.ts b/code/frontend/src/app/shared/models/seeker-config.model.ts index 5eca156c..6c43f677 100644 --- a/code/frontend/src/app/shared/models/seeker-config.model.ts +++ b/code/frontend/src/app/shared/models/seeker-config.model.ts @@ -1,4 +1,4 @@ -import { SelectionStrategy, SonarrSearchMode } from './enums'; +import { RadarrReleaseGate, SelectionStrategy, SonarrSearchMode } from './enums'; export interface SeekerConfig { searchEnabled: boolean; @@ -32,6 +32,7 @@ export interface SeekerInstanceConfig { useCutoff: boolean; useCustomFormatScore: boolean; sonarrSearchMode: SonarrSearchMode; + radarrReleaseGate: RadarrReleaseGate; } export interface UpdateSeekerConfig { @@ -62,4 +63,5 @@ export interface UpdateSeekerInstanceConfig { useCutoff: boolean; useCustomFormatScore: boolean; sonarrSearchMode: SonarrSearchMode; + radarrReleaseGate: RadarrReleaseGate; }