Files
Cleanuparr/code/frontend/src/app/features/settings/seeker/seeker.component.html
T

237 lines
13 KiB
HTML

<app-page-header
title="Seeker"
subtitle="Search for missing items and quality upgrades"
/>
@if (loadError()) {
<app-empty-state
icon="tablerPlugConnectedX"
heading="Could not connect to server"
description="Failed to load settings. Please check that the server is running and try again."
>
<app-button variant="primary" size="sm" (clicked)="retry()">
Retry
</app-button>
</app-empty-state>
} @else if (loader.showSpinner()) {
<app-loading-state message="Loading settings..." />
} @else if (!loader.loading()) {
<div class="settings-form">
<app-card header="Search Settings">
<div class="form-stack">
<app-toggle label="Search Enabled" [formField]="seekerForm.searchEnabled"
hint="Master toggle for all searching. When disabled, no replacement or proactive searches will be performed."
helpKey="seeker:searchEnabled" />
@if (seekerForm.searchEnabled().value()) {
<app-select label="Search Interval" [options]="intervalOptions" [formField]="seekerForm.searchInterval"
hint="How often the seeker runs to process searches"
helpKey="seeker:searchInterval" />
<p class="interval-warning">Using a very low interval will trigger searches more frequently, increasing indexer load significantly. This can result in rate limit warnings or a ban from your indexer, so be careful!</p>
<app-toggle label="Enable Replacement Search" [checked]="true" [disabled]="true"
hint="Automatically searches for a replacement whenever a download is removed from an arr queue"
helpKey="seeker:replacementSearchEnabled" />
}
</div>
</app-card>
@if (seekerForm.searchEnabled().value()) {
<app-card header="Proactive Search">
<div class="form-stack">
<app-toggle label="Proactive Search" [formField]="seekerForm.proactiveSearchEnabled"
hint="When enabled, the seeker will periodically search for missing items and quality upgrades across your library"
helpKey="seeker:proactiveSearchEnabled" />
@if (seekerForm.proactiveSearchEnabled().value()) {
<app-select label="Selection Strategy" [options]="strategyOptions" [formField]="seekerForm.selectionStrategy"
hint="How items are selected for searching"
helpKey="seeker:selectionStrategy"
featureId="seeker-cf-gap-strategy" />
@if (strategyDescription()) {
<p class="strategy-hint">{{ strategyDescription() }}</p>
}
<p class="priority-note">Replacement searches (triggered when an item is removed from an arr queue) always take priority over proactive searches, regardless of the selection strategy.</p>
<div class="form-divider"></div>
<app-toggle label="Enable Missing Search" [checked]="true" [disabled]="true"
hint="Always searches for items with no file. This cannot be disabled."
helpKey="seeker:missingSearchEnabled" />
<app-toggle label="Round Robin" [formField]="seekerForm.useRoundRobin"
[beforeChange]="confirmRoundRobin"
hint="Process one instance per run to spread indexer load"
helpKey="seeker:useRoundRobin" />
<app-number-input
label="Post-Release Grace Period (hours)"
placeholder="6"
hint="Wait this many hours after content is released before searching. Gives indexers time to process new releases. (0 = disabled)"
[formField]="seekerForm.postReleaseGraceHours"
helpKey="seeker:postReleaseGraceHours"
/>
<app-toggle label="Quiet Hours" [formField]="seekerForm.quietHoursEnabled"
hint="Pause proactive search between these times. Replacement searches keep running. Times follow the container's TZ setting."
helpKey="seeker:quietHoursEnabled" featureId="seeker-quiet-hours" />
@if (seekerForm.quietHoursEnabled().value()) {
<app-time-input label="Quiet From" [formField]="seekerForm.quietHoursStart"
[error]="seekerForm.quietHoursStart().errors()[0]?.message"
helpKey="seeker:quietHoursEnabled" />
<app-time-input label="Quiet Until" [formField]="seekerForm.quietHoursEnd"
hint="A window that ends before it starts runs past midnight, for example 23:00 to 07:00."
[error]="seekerForm.quietHoursEnd().errors()[0]?.message"
helpKey="seeker:quietHoursEnabled" />
}
}
</div>
</app-card>
@if (seekerForm.proactiveSearchEnabled().value() && instances().length > 0) {
<app-card header="Instances">
@if (instanceError()) {
<div class="instance-error">{{ instanceError() }}</div>
}
<div class="instance-list">
@for (instance of instances(); track instance.arrInstanceId) {
<div class="instance-row">
<div class="instance-row__header">
<div class="instance-row__info">
<img [src]="getInstanceIcon(instance.instanceType)" [alt]="instance.instanceType" class="instance-row__icon" />
<span class="instance-row__name">{{ instance.instanceName }}</span>
<app-badge [severity]="getInstanceTypeSeverity(instance.instanceType)" size="sm">{{ instance.instanceType }}</app-badge>
@if (instance.instanceType === 'Whisparr') {
<app-new-badge variant="inline" featureId="seeker-whisparr" />
}
@if (instance.instanceType === 'Lidarr') {
<app-new-badge variant="inline" featureId="seeker-lidarr" />
}
</div>
<app-toggle [checked]="instance.enabled" [disabled]="!instance.arrInstanceEnabled" (checkedChange)="toggleInstance($index)" helpKey="seeker:enabled" />
</div>
@if (!instance.arrInstanceEnabled) {
<div class="instance-row__warning">
Enable this instance in Arr Settings first
</div>
}
@if (instance.enabled) {
<div class="instance-row__details">
<app-toggle label="Monitored Only" [checked]="instance.monitoredOnly"
hint="Only search for items that are monitored in the arr app"
(checkedChange)="patchInstance($index, { monitoredOnly: $event })"
helpKey="seeker:monitoredOnly" />
<app-toggle label="Use Cutoff" [checked]="instance.useCutoff"
hint="Skip items that have already met their quality cutoff"
(checkedChange)="patchInstance($index, { useCutoff: $event })"
helpKey="seeker:useCutoff" />
<app-toggle label="Use Custom Format Score" [checked]="instance.useCustomFormatScore"
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" />
@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"
featureId="seeker-search-specials" />
<app-toggle label="Search for anime" [checked]="true" [disabled]="true"
hint="Anime seasons are searched as a whole only after every episode has aired or airs within 24 hours. Until then, episodes are searched one at a time. This is the rule Sonarr uses."
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..."
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..."
hint="Arr tags to exclude from search"
[value]="instance.skipTags"
(valueChange)="patchInstance($index, { skipTags: $event })"
helpKey="seeker:skipTags"
/>
<app-number-input
label="Active Download Limit"
placeholder="3"
hint="Skip proactive search when this many items have bytes remaining to download in the arr queue (0 = disabled)"
[value]="instance.activeDownloadLimit"
[min]="0"
[max]="100"
(valueChange)="updateInstanceActiveDownloadLimit($index, $event)"
helpKey="seeker:activeDownloadLimit"
/>
<app-toggle label="Ignore Struck Downloads" [checked]="instance.ignoreStruckDownloads"
hint="Downloads with a Queue Cleaner strike (stalled, slow, downloading metadata, errored) do not count toward the Active Download Limit"
(checkedChange)="patchInstance($index, { ignoreStruckDownloads: $event })"
helpKey="seeker:ignoreStruckDownloads"
featureId="seeker-ignore-struck-downloads" />
<app-number-input
label="Recent Grab Window (Hours)"
placeholder="24"
hint="Skip items the arr grabbed within this many hours, even after the download left the queue (0 = disabled)"
[value]="instance.recentGrabWindowHours"
[min]="0"
[max]="168"
(valueChange)="updateInstanceRecentGrabWindowHours($index, $event)"
helpKey="seeker:recentGrabWindowHours"
featureId="seeker-recent-grab-window"
/>
<app-toggle label="Rest Items That Find Nothing" [checked]="instance.restUnitsEnabled"
hint="After 3 searches in a row find nothing, skip the item for 1 cycle, doubling with each further miss up to 8 cycles or the max rest days, whichever ends first. A grab clears it."
(checkedChange)="updateInstanceRestUnitsEnabled($index, $event)"
helpKey="seeker:restUnitsEnabled"
featureId="seeker-unit-rest" />
@if (instance.restUnitsEnabled) {
<app-number-input
label="Max Rest (Days)"
placeholder="30"
hint="A rest always ends after this many days, even if cycles are slow"
[value]="instance.restMaxDays"
[min]="1"
[max]="365"
(valueChange)="updateInstanceRestMaxDays($index, $event)"
helpKey="seeker:restMaxDays"
/>
}
<app-number-input
label="Min Cycle Time (Days)"
placeholder="7"
hint="Minimum number of days a complete search cycle must span before starting a new one"
[value]="instance.minCycleTimeDays"
[min]="1"
[max]="365"
(valueChange)="updateInstanceMinCycleTimeDays($index, $event)"
helpKey="seeker:minCycleTimeDays"
/>
</div>
}
@if (instance.lastProcessedAt) {
<div class="instance-row__meta">
Last processed: {{ instance.lastProcessedAt | date:'medium' }}
</div>
}
</div>
}
</div>
</app-card>
}
}
<div class="form-actions">
<app-button variant="primary" [glowing]="dirty()" [loading]="saving()" [disabled]="!dirty() || saving() || saved() || hasErrors()" (clicked)="save()">
{{ saved() ? 'Saved!' : 'Save Settings' }}
</app-button>
</div>
</div>
}