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;
}