mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
added include tags to seeker settings
This commit is contained in:
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;
|
||||
|
||||
@@ -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:**
|
||||
```
|
||||
|
||||
Reference in new issue
Block a user