added include tags to seeker settings

This commit is contained in:
Flaminel committed 2026-10-20 15:55:51 +03:00
1 parent fcedd9522c
commit c9aa9988ee
7 files changed
+78 -14

No files matched your search

@@ -53,4 +53,5 @@ export const NEW_FEATURES: NewFeature[] = [
{ id: 'qbit-api-key' },
{ id: 'theme-follow-system' },
{ id: 'errored-downloads' },
{ id: 'seeker-include-tags' },
];
@@ -239,6 +239,7 @@ export class DocumentationService {
'postReleaseGraceHours': 'post-release-grace-period',
'missingSearchEnabled': 'enable-missing-search',
'enabled': 'instance-enabled',
'includeTags': 'instance-include-tags',
'skipTags': 'instance-skip-tags',
'activeDownloadLimit': 'instance-active-download-limit',
'ignoreStruckDownloads': 'instance-ignore-struck-downloads',
@@ -102,6 +102,15 @@
hint="Search for upgrades when a file's custom format score is below the quality profile's cutoff format score"
(checkedChange)="patchInstance($index, { useCustomFormatScore: $event })"
helpKey="seeker:useCustomFormatScore" />
<app-chip-input
label="Include Tags"
placeholder="Add tag..."
hint="Only search items with at least one of these arr tags. Leave empty to search all items. Skip tags take precedence"
[value]="instance.includeTags"
(valueChange)="patchInstance($index, { includeTags: $event })"
helpKey="seeker:includeTags"
featureId="seeker-include-tags"
/>
<app-chip-input
label="Skip Tags"
placeholder="Add tag..."
@@ -14,6 +14,7 @@ const SONARR: SeekerInstanceConfig = {
instanceType: 'Sonarr',
enabled: true,
skipTags: ['alpha', 'beta'],
includeTags: ['wanted'],
arrInstanceEnabled: true,
activeDownloadLimit: 3,
ignoreStruckDownloads: false,
@@ -29,6 +30,7 @@ const RADARR: SeekerInstanceConfig = {
instanceType: 'Radarr',
enabled: false,
skipTags: [],
includeTags: [],
lastProcessedAt: '2026-08-01T10:00:00Z',
arrInstanceEnabled: true,
activeDownloadLimit: 5,
@@ -133,6 +135,18 @@ describe('SeekerComponent', () => {
return host.querySelector('.number-field') as HTMLInputElement;
}
function chipField(fixture: ComponentFixture<SeekerComponent>, label: string): HTMLElement {
return Array.from(fixture.nativeElement.querySelectorAll('app-chip-input')).find((input) =>
(input as HTMLElement).querySelector('.chip-label')?.textContent?.includes(label),
) as HTMLElement;
}
function chipTexts(field: HTMLElement): string[] {
return Array.from(field.querySelectorAll('.chip')).map((chip) =>
(chip as HTMLElement).textContent!.replace('×', '').trim(),
);
}
function setNumberField(fixture: ComponentFixture<SeekerComponent>, label: string, value: string): void {
const field = numberField(fixture, label);
field.value = value;
@@ -151,8 +165,8 @@ describe('SeekerComponent', () => {
expect(component.seekerForm.postReleaseGraceHours().value()).toBe(6);
expect(component.instances()).toEqual([
{ ...SONARR, skipTags: ['alpha', 'beta'] },
{ ...RADARR, skipTags: [] },
{ ...SONARR, skipTags: ['alpha', 'beta'], includeTags: ['wanted'] },
{ ...RADARR, skipTags: [], includeTags: [] },
]);
expect(component.dirty()).toBe(false);
expect(component.hasPendingChanges()).toBe(false);
@@ -160,23 +174,22 @@ describe('SeekerComponent', () => {
expect(text(fixture)).toContain('Search for missing items and quality upgrades');
});
it('copies the loaded skip tags instead of sharing the response array', () => {
it('copies the loaded tags instead of sharing the response arrays', () => {
const { component } = setup();
component.patchInstance(0, { skipTags: ['gamma'] });
component.patchInstance(0, { skipTags: ['gamma'], includeTags: ['delta'] });
expect(SONARR.skipTags).toEqual(['alpha', 'beta']);
expect(SONARR.includeTags).toEqual(['wanted']);
expect(component.instances()[0].skipTags).toEqual(['gamma']);
expect(component.instances()[0].includeTags).toEqual(['delta']);
});
it('renders the skip tag chips and the per-instance detail controls', () => {
it('renders the include and skip tag chips and the per-instance detail controls', () => {
const { fixture } = setup();
const chips = Array.from(fixture.nativeElement.querySelectorAll('.chip')).map((chip) =>
(chip as HTMLElement).textContent!.replace('×', '').trim(),
);
expect(chips).toEqual(['alpha', 'beta']);
expect(chipTexts(chipField(fixture, 'Include Tags'))).toEqual(['wanted']);
expect(chipTexts(chipField(fixture, 'Skip Tags'))).toEqual(['alpha', 'beta']);
expect(toggleByLabel(fixture, 'Monitored Only')).not.toBeNull();
expect(toggleByLabel(fixture, 'Use Cutoff')).not.toBeNull();
expect(toggleByLabel(fixture, 'Use Custom Format Score')).not.toBeNull();
@@ -382,7 +395,8 @@ describe('SeekerComponent', () => {
it('patches the skip tags edited in the chip input', () => {
const { fixture, component } = setup();
const input = fixture.nativeElement.querySelector('.chip-input') as HTMLInputElement;
const field = chipField(fixture, 'Skip Tags');
const input = field.querySelector('.chip-input') as HTMLInputElement;
input.value = 'gamma';
input.dispatchEvent(new Event('input'));
@@ -392,12 +406,28 @@ describe('SeekerComponent', () => {
expect(component.instances()[0].skipTags).toEqual(['alpha', 'beta', 'gamma']);
(fixture.nativeElement.querySelector('.chip__remove') as HTMLButtonElement).click();
(field.querySelector('.chip__remove') as HTMLButtonElement).click();
fixture.detectChanges();
expect(component.instances()[0].skipTags).toEqual(['beta', 'gamma']);
});
it('patches the include tags edited in the chip input', () => {
const { fixture, component } = setup();
const field = chipField(fixture, 'Include Tags');
const input = field.querySelector('.chip-input') as HTMLInputElement;
input.value = 'anime';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
fixture.detectChanges();
expect(component.instances()[0].includeTags).toEqual(['wanted', 'anime']);
expect(component.instances()[0].skipTags).toEqual(['alpha', 'beta']);
expect(component.dirty()).toBe(true);
});
it('blocks saving while proactive search has no enabled instance', () => {
const { fixture, component } = setup();
@@ -457,7 +487,7 @@ describe('SeekerComponent', () => {
component.seekerForm.searchInterval().value.set(10);
component.seekerForm.selectionStrategy().value.set(SelectionStrategy.NewestFirst);
component.seekerForm.postReleaseGraceHours().value.set(24);
component.patchInstance(0, { skipTags: ['gamma'], ignoreStruckDownloads: true });
component.patchInstance(0, { skipTags: ['gamma'], includeTags: ['anime'], ignoreStruckDownloads: true });
fixture.detectChanges();
expect(component.dirty()).toBe(true);
@@ -477,6 +507,7 @@ describe('SeekerComponent', () => {
arrInstanceId: SONARR.arrInstanceId,
enabled: true,
skipTags: ['gamma'],
includeTags: ['anime'],
activeDownloadLimit: 3,
ignoreStruckDownloads: true,
minCycleTimeDays: 7,
@@ -488,6 +519,7 @@ describe('SeekerComponent', () => {
arrInstanceId: RADARR.arrInstanceId,
enabled: false,
skipTags: [],
includeTags: [],
activeDownloadLimit: 5,
ignoreStruckDownloads: true,
minCycleTimeDays: 14,
@@ -70,6 +70,7 @@ interface InstanceState {
instanceType: string;
enabled: boolean;
skipTags: string[];
includeTags: string[];
lastProcessedAt?: string;
arrInstanceEnabled: boolean;
activeDownloadLimit: number;
@@ -167,6 +168,7 @@ export class SeekerComponent implements HasPendingChanges {
instanceType: i.instanceType,
enabled: i.enabled,
skipTags: [...i.skipTags],
includeTags: [...i.includeTags],
lastProcessedAt: i.lastProcessedAt,
arrInstanceEnabled: i.arrInstanceEnabled,
activeDownloadLimit: i.activeDownloadLimit,
@@ -236,6 +238,7 @@ export class SeekerComponent implements HasPendingChanges {
arrInstanceId: i.arrInstanceId,
enabled: i.enabled,
skipTags: i.skipTags,
includeTags: i.includeTags,
activeDownloadLimit: i.activeDownloadLimit,
ignoreStruckDownloads: i.ignoreStruckDownloads,
minCycleTimeDays: i.minCycleTimeDays,
@@ -16,6 +16,7 @@ export interface SeekerInstanceConfig {
instanceType: string;
enabled: boolean;
skipTags: string[];
includeTags: string[];
lastProcessedAt?: string;
arrInstanceEnabled: boolean;
activeDownloadLimit: number;
@@ -40,6 +41,7 @@ export interface UpdateSeekerInstanceConfig {
arrInstanceId: string;
enabled: boolean;
skipTags: string[];
includeTags: string[];
activeDownloadLimit: number;
ignoreStruckDownloads: boolean;
minCycleTimeDays: number;
+17 -1
View File
@@ -182,11 +182,27 @@ Enables this instance for proactive search. When disabled, the instance will not
</ConfigSection>
<ConfigSection
title="Include Tags"
>
Arr tags that limit proactive search. When this list has tags, only a movie or series carrying at least one of them gets searched. Leave it empty to search every item.
Skip Tags take precedence: an item carrying both an include tag and a skip tag is skipped.
**Examples:**
```
anime
4k
```
</ConfigSection>
<ConfigSection
title="Skip Tags"
>
Arr tags to exclude from search. Any movie or series in the corresponding *arr application that has one of these tags assigned will be skipped entirely during proactive search.
Arr tags to exclude from search. Any movie or series in the corresponding *arr application that has one of these tags assigned will be skipped entirely during proactive search, even when it also carries an include tag.
**Examples:**
```