added release gate setting to seeker ui

This commit is contained in:
Flaminel committed 2026-10-26 15:58:09 +02:00
1 parent 469467249a
commit abff028ba1
9 files changed
+81 -3

No files matched your search

@@ -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' },
];
@@ -87,6 +87,7 @@ export interface SearchEvent {
export enum SeekerExclusionReason {
NotReleased = 'NotReleased',
InGracePeriod = 'InGracePeriod',
NotAvailable = 'NotAvailable',
NotMonitored = 'NotMonitored',
SkipTag = 'SkipTag',
MissingIncludeTag = 'MissingIncludeTag',
@@ -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',
},
};
@@ -106,6 +106,7 @@ export function funnelStages(funnel: InstanceFunnel, instanceType: string): { na
export const EXCLUSION_REASON_LABELS: Record<SeekerExclusionReason, string> = {
[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',
@@ -130,6 +130,13 @@
helpKey="seeker:searchAnime"
featureId="seeker-search-anime" />
}
@if (instance.instanceType === 'Radarr') {
<app-select label="Release Gate" [options]="radarrReleaseGateOptions" [value]="instance.radarrReleaseGate"
(valueChange)="updateInstanceRadarrReleaseGate($index, $event)"
hint="Release dates searches a movie once Radarr marks it released and its digital, physical or cinema date is past the grace period. Follow Radarr availability uses each movie's minimum availability and Radarr's availability delay; the grace period does not apply."
helpKey="seeker:radarrReleaseGate"
featureId="seeker-radarr-release-gate" />
}
<app-chip-input
label="Include Tags"
placeholder="Add tag..."
@@ -5,7 +5,7 @@ import { ConfirmService } from '@core/services/confirm.service';
import { ToastService } from '@core/services/toast.service';
import { ApiError } from '@core/interceptors/error.interceptor';
import { SeekerConfig, SeekerInstanceConfig, UpdateSeekerConfig } from '@shared/models/seeker-config.model';
import { SelectionStrategy, SonarrSearchMode } from '@shared/models/enums';
import { RadarrReleaseGate, SelectionStrategy, SonarrSearchMode } from '@shared/models/enums';
import { SeekerComponent } from './seeker.component';
const SONARR: SeekerInstanceConfig = {
@@ -26,6 +26,7 @@ const SONARR: SeekerInstanceConfig = {
useCutoff: false,
useCustomFormatScore: false,
sonarrSearchMode: SonarrSearchMode.Auto,
radarrReleaseGate: RadarrReleaseGate.ReleaseDate,
};
const RADARR: SeekerInstanceConfig = {
@@ -47,6 +48,7 @@ const RADARR: SeekerInstanceConfig = {
useCutoff: true,
useCustomFormatScore: true,
sonarrSearchMode: SonarrSearchMode.Auto,
radarrReleaseGate: RadarrReleaseGate.ReleaseDate,
};
const CONFIG: SeekerConfig = {
@@ -142,6 +144,13 @@ describe('SeekerComponent', () => {
) as HTMLElement | undefined;
}
function releaseGateSelect(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('Release Gate'),
) as HTMLElement | undefined;
}
function text(fixture: ComponentFixture<SeekerComponent>): 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,
},
],
});
@@ -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, string> = {
[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,
})),
};
@@ -128,6 +128,11 @@ export enum SonarrSearchMode {
Episode = 'Episode',
}
export enum RadarrReleaseGate {
ReleaseDate = 'ReleaseDate',
ArrAvailability = 'ArrAvailability',
}
export enum SearchCommandStatus {
Pending = 'Pending',
Started = 'Started',
@@ -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;
}