mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-11-01 03:51:04 -05:00
237 lines
13 KiB
HTML
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>
|
|
}
|