mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 18:58:59 -04:00
added sonarr search mode select to seeker settings
This commit is contained in:
1 parent
db1774f4a9
commit
4dcfe8c299
7 files changed
+87
-4
No files matched your search
@@ -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' },
|
||||
];
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -103,6 +103,11 @@
|
||||
(checkedChange)="patchInstance($index, { useCustomFormatScore: $event })"
|
||||
helpKey="seeker:useCustomFormatScore" />
|
||||
@if (instance.instanceType === 'Sonarr') {
|
||||
<app-select label="Search Mode" [options]="sonarrSearchModeOptions" [value]="instance.sonarrSearchMode"
|
||||
(valueChange)="updateInstanceSonarrSearchMode($index, $event)"
|
||||
hint="Auto searches a whole season only when the season has finished airing and every aired monitored episode in it needs a search. Otherwise it searches one episode per run. Season always searches the whole season. Episode always searches one episode per run."
|
||||
helpKey="seeker:sonarrSearchMode"
|
||||
featureId="seeker-sonarr-search-mode" />
|
||||
<app-toggle label="Search for specials" [checked]="true" [disabled]="true"
|
||||
hint="Specials are always searched one episode at a time. Sonarr runs two indexer queries for every special, so searching a whole specials season would query all of them at once."
|
||||
helpKey="seeker:searchSpecials"
|
||||
|
||||
@@ -4,8 +4,8 @@ import { SeekerApi } from '@core/api/seeker.api';
|
||||
import { ConfirmService } from '@core/services/confirm.service';
|
||||
import { ToastService } from '@core/services/toast.service';
|
||||
import { ApiError } from '@core/interceptors/error.interceptor';
|
||||
import { SeekerConfig, SeekerInstanceConfig } from '@shared/models/seeker-config.model';
|
||||
import { SelectionStrategy } from '@shared/models/enums';
|
||||
import { SeekerConfig, SeekerInstanceConfig, UpdateSeekerConfig } from '@shared/models/seeker-config.model';
|
||||
import { SelectionStrategy, SonarrSearchMode } from '@shared/models/enums';
|
||||
import { SeekerComponent } from './seeker.component';
|
||||
|
||||
const SONARR: SeekerInstanceConfig = {
|
||||
@@ -23,6 +23,7 @@ const SONARR: SeekerInstanceConfig = {
|
||||
monitoredOnly: true,
|
||||
useCutoff: false,
|
||||
useCustomFormatScore: false,
|
||||
sonarrSearchMode: SonarrSearchMode.Auto,
|
||||
};
|
||||
|
||||
const RADARR: SeekerInstanceConfig = {
|
||||
@@ -41,6 +42,7 @@ const RADARR: SeekerInstanceConfig = {
|
||||
monitoredOnly: false,
|
||||
useCutoff: true,
|
||||
useCustomFormatScore: true,
|
||||
sonarrSearchMode: SonarrSearchMode.Auto,
|
||||
};
|
||||
|
||||
const CONFIG: SeekerConfig = {
|
||||
@@ -126,6 +128,13 @@ describe('SeekerComponent', () => {
|
||||
return fixture.nativeElement.querySelector(`[aria-label="${label}"]`) as HTMLButtonElement;
|
||||
}
|
||||
|
||||
function searchModeSelect(fixture: ComponentFixture<SeekerComponent>, 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<SeekerComponent>): 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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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, string> = {
|
||||
[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,
|
||||
})),
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user