From 4dcfe8c299d2602fbcd8ffbec07afb9e80c72257 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Wed, 21 Oct 2026 22:17:04 +0300 Subject: [PATCH] added sonarr search mode select to seeker settings --- .../core/feature-badges/feature-registry.ts | 1 + .../core/services/documentation.service.ts | 1 + .../settings/seeker/seeker.component.html | 5 ++ .../settings/seeker/seeker.component.spec.ts | 58 ++++++++++++++++++- .../settings/seeker/seeker.component.ts | 16 ++++- code/frontend/src/app/shared/models/enums.ts | 6 ++ .../app/shared/models/seeker-config.model.ts | 4 +- 7 files changed, 87 insertions(+), 4 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 c6fbb309..b3b53d56 100644 --- a/code/frontend/src/app/core/feature-badges/feature-registry.ts +++ b/code/frontend/src/app/core/feature-badges/feature-registry.ts @@ -63,4 +63,5 @@ export const NEW_FEATURES: NewFeature[] = [ { id: 'seeker-cycle-notification' }, { id: 'seeker-eligibility-funnel' }, { id: 'seeker-preview' }, + { id: 'seeker-sonarr-search-mode' }, ]; diff --git a/code/frontend/src/app/core/services/documentation.service.ts b/code/frontend/src/app/core/services/documentation.service.ts index 73b79816..e458b988 100644 --- a/code/frontend/src/app/core/services/documentation.service.ts +++ b/code/frontend/src/app/core/services/documentation.service.ts @@ -248,6 +248,7 @@ export class DocumentationService { 'minCycleTimeDays': 'instance-min-cycle-time-days', 'searchSpecials': 'instance-search-for-specials', 'searchAnime': 'instance-search-for-anime', + 'sonarrSearchMode': 'instance-search-mode', }, }; 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 5ad4b027..46c355e9 100644 --- a/code/frontend/src/app/features/settings/seeker/seeker.component.html +++ b/code/frontend/src/app/features/settings/seeker/seeker.component.html @@ -103,6 +103,11 @@ (checkedChange)="patchInstance($index, { useCustomFormatScore: $event })" helpKey="seeker:useCustomFormatScore" /> @if (instance.instanceType === 'Sonarr') { + { return fixture.nativeElement.querySelector(`[aria-label="${label}"]`) as HTMLButtonElement; } + function searchModeSelect(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('Search Mode'), + ) as HTMLElement | undefined; + } + function text(fixture: ComponentFixture): string { return fixture.nativeElement.textContent as string; } @@ -397,6 +406,49 @@ describe('SeekerComponent', () => { expect(radarrDetails.textContent).not.toContain('Search for anime'); }); + it('shows the search mode select for Sonarr only', () => { + const { fixture } = setup(); + + expect(searchModeSelect(fixture, 0)).toBeDefined(); + + const radarrToggle = fixture.nativeElement.querySelectorAll('.instance-row__header [role="switch"]')[1] as HTMLButtonElement; + radarrToggle.click(); + fixture.detectChanges(); + + expect(searchModeSelect(fixture, 1)).toBeUndefined(); + }); + + it('shows the loaded search mode as selected', () => { + const { fixture } = setup({ + config: { ...CONFIG, instances: [{ ...SONARR, sonarrSearchMode: SonarrSearchMode.Season }, RADARR] }, + }); + + const trigger = searchModeSelect(fixture, 0)!.querySelector('.select-trigger') as HTMLButtonElement; + expect(trigger.textContent!.trim()).toBe('Season'); + }); + + it('sends the picked search mode on save', () => { + const { fixture, component, updateConfig } = setup(); + + const select = searchModeSelect(fixture, 0)!; + (select.querySelector('.select-trigger') as HTMLButtonElement).click(); + fixture.detectChanges(); + const option = Array.from(select.querySelectorAll('.select-option')).find( + (el) => el.textContent!.trim() === 'Episode', + ) as HTMLButtonElement; + option.click(); + fixture.detectChanges(); + + expect(component.dirty()).toBe(true); + + saveButton(fixture).click(); + fixture.detectChanges(); + + const sent = updateConfig.mock.calls[0][0] as UpdateSeekerConfig; + expect(sent.instances[0].sonarrSearchMode).toBe(SonarrSearchMode.Episode); + expect(sent.instances[1].sonarrSearchMode).toBe(SonarrSearchMode.Auto); + }); + it('patches the instance limits typed into the numeric inputs', () => { const { fixture, component } = setup(); @@ -543,6 +595,7 @@ describe('SeekerComponent', () => { monitoredOnly: true, useCutoff: false, useCustomFormatScore: false, + sonarrSearchMode: SonarrSearchMode.Auto, }, { arrInstanceId: RADARR.arrInstanceId, @@ -556,6 +609,7 @@ describe('SeekerComponent', () => { monitoredOnly: false, useCutoff: true, useCustomFormatScore: true, + sonarrSearchMode: SonarrSearchMode.Auto, }, ], }); 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 7c97b2b8..a8f0aa45 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 } from '@shared/models/enums'; +import { SelectionStrategy, SonarrSearchMode } from '@shared/models/enums'; import { instanceTypeSeverity } from '@shared/utils/instance-display.util'; const INTERVAL_OPTIONS: SelectOption[] = [ @@ -46,6 +46,12 @@ const STRATEGY_OPTIONS: SelectOption[] = [ { label: 'Random', value: SelectionStrategy.Random }, ]; +const SONARR_SEARCH_MODE_OPTIONS: SelectOption[] = [ + { label: 'Auto', value: SonarrSearchMode.Auto }, + { label: 'Season', value: SonarrSearchMode.Season }, + { label: 'Episode', value: SonarrSearchMode.Episode }, +]; + 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]: 'Works through your library in order, starting with items that haven\'t been searched the longest. Guarantees every item gets covered.', @@ -80,6 +86,7 @@ interface InstanceState { monitoredOnly: boolean; useCutoff: boolean; useCustomFormatScore: boolean; + sonarrSearchMode: SonarrSearchMode; } @Component({ @@ -107,6 +114,7 @@ export class SeekerComponent implements HasPendingChanges { readonly intervalOptions = INTERVAL_OPTIONS; readonly strategyOptions = STRATEGY_OPTIONS; + readonly sonarrSearchModeOptions = SONARR_SEARCH_MODE_OPTIONS; readonly loader = this.settings.loader; readonly loadError = this.settings.loadError; readonly saving = signal(false); @@ -179,6 +187,7 @@ export class SeekerComponent implements HasPendingChanges { monitoredOnly: i.monitoredOnly, useCutoff: i.useCutoff, useCustomFormatScore: i.useCustomFormatScore, + sonarrSearchMode: i.sonarrSearchMode, }))); this.dirtyTracker.markSaved(); }); @@ -225,6 +234,10 @@ export class SeekerComponent implements HasPendingChanges { this.patchInstance(index, { recentGrabWindowHours: hours ?? 24 }); } + updateInstanceSonarrSearchMode(index: number, mode: unknown): void { + this.patchInstance(index, { sonarrSearchMode: mode as SonarrSearchMode }); + } + getInstanceIcon(instanceType: string): string { return `icons/ext/${instanceType.toLowerCase()}-light.svg`; } @@ -252,6 +265,7 @@ export class SeekerComponent implements HasPendingChanges { monitoredOnly: i.monitoredOnly, useCutoff: i.useCutoff, useCustomFormatScore: i.useCustomFormatScore, + sonarrSearchMode: i.sonarrSearchMode, })), }; diff --git a/code/frontend/src/app/shared/models/enums.ts b/code/frontend/src/app/shared/models/enums.ts index 59ec4862..dfdd9817 100644 --- a/code/frontend/src/app/shared/models/enums.ts +++ b/code/frontend/src/app/shared/models/enums.ts @@ -121,6 +121,12 @@ export enum SelectionStrategy { Random = 'Random', } +export enum SonarrSearchMode { + Auto = 'Auto', + Season = 'Season', + Episode = 'Episode', +} + 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 7969258b..628c4e29 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 } from './enums'; +import { SelectionStrategy, SonarrSearchMode } from './enums'; export interface SeekerConfig { searchEnabled: boolean; @@ -26,6 +26,7 @@ export interface SeekerInstanceConfig { monitoredOnly: boolean; useCutoff: boolean; useCustomFormatScore: boolean; + sonarrSearchMode: SonarrSearchMode; } export interface UpdateSeekerConfig { @@ -50,4 +51,5 @@ export interface UpdateSeekerInstanceConfig { monitoredOnly: boolean; useCutoff: boolean; useCustomFormatScore: boolean; + sonarrSearchMode: SonarrSearchMode; }