diff --git a/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.html b/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.html index 5821a6aa..644628fb 100644 --- a/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.html +++ b/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.html @@ -116,7 +116,7 @@
- When enabled, private torrents will be skipped + When enabled, private torrents will not be processed
@@ -129,7 +129,7 @@
- When enabled, private torrents will be deleted + Enable this if you want to keep private torrents in the download client even if they are removed from the arrs
diff --git a/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.ts b/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.ts index cc8f1411..0c31a3cc 100644 --- a/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.ts +++ b/code/frontend/src/app/settings/content-blocker/content-blocker-settings.component.ts @@ -166,27 +166,36 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD effect(() => { const config = this.contentBlockerConfig(); if (config) { - // Reset form with the config values + // Handle the case where ignorePrivate is true but deletePrivate is also true + // This shouldn't happen, but if it does, correct it + const correctedConfig = { ...config }; + + // For Content Blocker + if (correctedConfig.ignorePrivate && correctedConfig.deletePrivate) { + correctedConfig.deletePrivate = false; + } + + // Reset form with the corrected config values this.contentBlockerForm.patchValue({ - enabled: config.enabled, - useAdvancedScheduling: config.useAdvancedScheduling || false, - cronExpression: config.cronExpression, - jobSchedule: config.jobSchedule || { + enabled: correctedConfig.enabled, + useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false, + cronExpression: correctedConfig.cronExpression, + jobSchedule: correctedConfig.jobSchedule || { every: 5, type: ScheduleUnit.Seconds }, - ignorePrivate: config.ignorePrivate, - deletePrivate: config.deletePrivate, - deleteKnownMalware: config.deleteKnownMalware, - sonarr: config.sonarr, - radarr: config.radarr, - lidarr: config.lidarr, - readarr: config.readarr, - whisparr: config.whisparr, + ignorePrivate: correctedConfig.ignorePrivate, + deletePrivate: correctedConfig.deletePrivate, + deleteKnownMalware: correctedConfig.deleteKnownMalware, + sonarr: correctedConfig.sonarr, + radarr: correctedConfig.radarr, + lidarr: correctedConfig.lidarr, + readarr: correctedConfig.readarr, + whisparr: correctedConfig.whisparr, }); // Update all form control states - this.updateFormControlDisabledStates(config); + this.updateFormControlDisabledStates(correctedConfig); // Store original values for dirty checking this.storeOriginalValues(); @@ -247,6 +256,27 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD this.updateMainControlsState(enabled); }); } + + // Add listener for ignorePrivate changes + const ignorePrivateControl = this.contentBlockerForm.get('ignorePrivate'); + if (ignorePrivateControl) { + ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe((ignorePrivate: boolean) => { + const deletePrivateControl = this.contentBlockerForm.get('deletePrivate'); + + if (ignorePrivate && deletePrivateControl) { + // If ignoring private, uncheck and disable delete private + deletePrivateControl.setValue(false); + deletePrivateControl.disable({ onlySelf: true }); + } else if (!ignorePrivate && deletePrivateControl) { + // If not ignoring private, enable delete private (if main feature is enabled) + const mainEnabled = this.contentBlockerForm.get('enabled')?.value || false; + if (mainEnabled) { + deletePrivateControl.enable({ onlySelf: true }); + } + } + }); + } // Listen for changes to the 'useAdvancedScheduling' control const advancedControl = this.contentBlockerForm.get('useAdvancedScheduling'); @@ -417,8 +447,17 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD // Enable content blocker specific controls this.contentBlockerForm.get("ignorePrivate")?.enable({ onlySelf: true }); - this.contentBlockerForm.get("deletePrivate")?.enable({ onlySelf: true }); this.contentBlockerForm.get("deleteKnownMalware")?.enable({ onlySelf: true }); + + // Only enable deletePrivate if ignorePrivate is false + const ignorePrivate = this.contentBlockerForm.get("ignorePrivate")?.value || false; + const deletePrivateControl = this.contentBlockerForm.get("deletePrivate"); + + if (!ignorePrivate && deletePrivateControl) { + deletePrivateControl.enable({ onlySelf: true }); + } else if (deletePrivateControl) { + deletePrivateControl.disable({ onlySelf: true }); + } // Enable blocklist settings for each Arr this.contentBlockerForm.get("sonarr.enabled")?.enable({ onlySelf: true }); diff --git a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html index b9d8b413..14c2b03b 100644 --- a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html +++ b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html @@ -274,6 +274,7 @@ Target category is required Category to move unlinked downloads to + You have to create a seeding rule for this category if you want to remove the downloads diff --git a/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html b/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html index 60497aa3..b085956d 100644 --- a/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html +++ b/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html @@ -155,7 +155,7 @@
- When enabled, private torrents will be skipped + When enabled, private torrents will not be checked for being failed imports
@@ -168,7 +168,7 @@
- When enabled, private torrents will be deleted + Enable this if you want to keep private torrents in the download client even if they are removed from the arrs
@@ -264,7 +264,7 @@
- When enabled, private torrents will be skipped + When enabled, private torrents will not be checked for being stalled
@@ -277,7 +277,7 @@
- When enabled, private torrents will be deleted + Enable this if you want to keep private torrents in the download client even if they are removed from the arrs
@@ -383,7 +383,7 @@
- When enabled, private torrents will be skipped + When enabled, private torrents will not be checked for being slow
@@ -396,7 +396,7 @@
- When enabled, private torrents will be deleted + Enable this if you want to keep private torrents in the download client even if they are removed from the arrs
@@ -413,6 +413,7 @@ [min]="0" placeholder="Enter minimum speed" helpText="Minimum speed threshold for slow downloads (e.g., 100KB/s)" + type="speed" > @@ -454,6 +455,7 @@ [min]="0" placeholder="Enter size threshold" helpText="Downloads will be ignored if size exceeds, e.g., 25 GB" + type="size" > diff --git a/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts b/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts index 24e1a08d..1a5e3e57 100644 --- a/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts +++ b/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts @@ -176,25 +176,40 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea effect(() => { const config = this.queueCleanerConfig(); if (config) { - // Save original cron expression - const cronExpression = config.cronExpression; + // Handle the case where ignorePrivate is true but deletePrivate is also true + // This shouldn't happen, but if it does, correct it + const correctedConfig = { ...config }; - // Reset form with the config values + // For Queue Cleaner (apply to all sections) + if (correctedConfig.failedImport?.ignorePrivate && correctedConfig.failedImport?.deletePrivate) { + correctedConfig.failedImport.deletePrivate = false; + } + if (correctedConfig.stalled?.ignorePrivate && correctedConfig.stalled?.deletePrivate) { + correctedConfig.stalled.deletePrivate = false; + } + if (correctedConfig.slow?.ignorePrivate && correctedConfig.slow?.deletePrivate) { + correctedConfig.slow.deletePrivate = false; + } + + // Save original cron expression + const cronExpression = correctedConfig.cronExpression; + + // Reset form with the corrected config values this.queueCleanerForm.patchValue({ - enabled: config.enabled, - useAdvancedScheduling: config.useAdvancedScheduling || false, - cronExpression: config.cronExpression, - jobSchedule: config.jobSchedule || { + enabled: correctedConfig.enabled, + useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false, + cronExpression: correctedConfig.cronExpression, + jobSchedule: correctedConfig.jobSchedule || { every: 5, type: ScheduleUnit.Minutes }, - failedImport: config.failedImport, - stalled: config.stalled, - slow: config.slow, + failedImport: correctedConfig.failedImport, + stalled: correctedConfig.stalled, + slow: correctedConfig.slow, }); // Then update all other dependent form control states - this.updateFormControlDisabledStates(config); + this.updateFormControlDisabledStates(correctedConfig); // Store original values for dirty checking this.storeOriginalValues(); @@ -255,6 +270,30 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea this.updateMainControlsState(enabled); }); } + + // Add listeners for ignorePrivate changes in each section + ['failedImport', 'stalled', 'slow'].forEach(section => { + const ignorePrivateControl = this.queueCleanerForm.get(`${section}.ignorePrivate`); + + if (ignorePrivateControl) { + ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe((ignorePrivate: boolean) => { + const deletePrivateControl = this.queueCleanerForm.get(`${section}.deletePrivate`); + + if (ignorePrivate && deletePrivateControl) { + // If ignoring private, uncheck and disable delete private + deletePrivateControl.setValue(false); + deletePrivateControl.disable({ onlySelf: true }); + } else if (!ignorePrivate && deletePrivateControl) { + // If not ignoring private, enable delete private (if parent section is enabled) + const sectionEnabled = this.isSectionEnabled(section); + if (sectionEnabled) { + deletePrivateControl.enable({ onlySelf: true }); + } + } + }); + } + }); // Listen for changes to the 'useAdvancedScheduling' control const advancedControl = this.queueCleanerForm.get('useAdvancedScheduling'); @@ -349,6 +388,17 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea this.originalFormValues = JSON.parse(JSON.stringify(this.queueCleanerForm.getRawValue())); this.hasActualChanges = false; } + + // Helper method to check if a section is enabled + private isSectionEnabled(section: string): boolean { + const mainEnabled = this.queueCleanerForm.get('enabled')?.value || false; + if (!mainEnabled) return false; + + const maxStrikesControl = this.queueCleanerForm.get(`${section}.maxStrikes`); + const maxStrikes = maxStrikesControl?.value || 0; + + return maxStrikes >= 3; + } // Check if the current form values are different from the original values private formValuesChanged(): boolean { @@ -463,8 +513,17 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea if (enable) { this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.enable(options); - this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.enable(options); this.queueCleanerForm.get("failedImport")?.get("ignoredPatterns")?.enable(options); + + // Only enable deletePrivate if ignorePrivate is false + const ignorePrivate = this.queueCleanerForm.get("failedImport.ignorePrivate")?.value || false; + const deletePrivateControl = this.queueCleanerForm.get("failedImport.deletePrivate"); + + if (!ignorePrivate && deletePrivateControl) { + deletePrivateControl.enable(options); + } else if (deletePrivateControl) { + deletePrivateControl.disable(options); + } } else { this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.disable(options); this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.disable(options); @@ -482,7 +541,16 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea if (enable) { this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.enable(options); this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.enable(options); - this.queueCleanerForm.get("stalled")?.get("deletePrivate")?.enable(options); + + // Only enable deletePrivate if ignorePrivate is false + const ignorePrivate = this.queueCleanerForm.get("stalled.ignorePrivate")?.value || false; + const deletePrivateControl = this.queueCleanerForm.get("stalled.deletePrivate"); + + if (!ignorePrivate && deletePrivateControl) { + deletePrivateControl.enable(options); + } else if (deletePrivateControl) { + deletePrivateControl.disable(options); + } } else { this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.disable(options); this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.disable(options); @@ -500,10 +568,19 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea if (enable) { this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.enable(options); this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.enable(options); - this.queueCleanerForm.get("slow")?.get("deletePrivate")?.enable(options); this.queueCleanerForm.get("slow")?.get("minSpeed")?.enable(options); this.queueCleanerForm.get("slow")?.get("maxTime")?.enable(options); this.queueCleanerForm.get("slow")?.get("ignoreAboveSize")?.enable(options); + + // Only enable deletePrivate if ignorePrivate is false + const ignorePrivate = this.queueCleanerForm.get("slow.ignorePrivate")?.value || false; + const deletePrivateControl = this.queueCleanerForm.get("slow.deletePrivate"); + + if (!ignorePrivate && deletePrivateControl) { + deletePrivateControl.enable(options); + } else if (deletePrivateControl) { + deletePrivateControl.disable(options); + } } else { this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.disable(options); this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.disable(options); diff --git a/code/frontend/src/app/shared/components/byte-size-input/byte-size-input.component.ts b/code/frontend/src/app/shared/components/byte-size-input/byte-size-input.component.ts index cd7dcc4b..a2cb80d0 100644 --- a/code/frontend/src/app/shared/components/byte-size-input/byte-size-input.component.ts +++ b/code/frontend/src/app/shared/components/byte-size-input/byte-size-input.component.ts @@ -4,7 +4,9 @@ import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModu import { InputNumberModule } from 'primeng/inputnumber'; import { SelectButtonModule } from 'primeng/selectbutton'; -type ByteSizeUnit = 'KB' | 'MB' | 'GB' | 'TB'; +export type ByteSizeInputType = 'speed' | 'size'; + +type ByteSizeUnit = 'KB' | 'MB' | 'GB'; @Component({ selector: 'app-byte-size-input', @@ -26,31 +28,57 @@ export class ByteSizeInputComponent implements ControlValueAccessor { @Input() disabled: boolean = false; @Input() placeholder: string = 'Enter size'; @Input() helpText: string = ''; + @Input() type: ByteSizeInputType = 'size'; // Value in the selected unit value = signal(null); - + // The selected unit unit = signal('MB'); - - // Available units - unitOptions = [ - { label: 'KB', value: 'KB' }, - { label: 'MB', value: 'MB' }, - { label: 'GB', value: 'GB' }, - { label: 'TB', value: 'TB' } - ]; + + // Available units, computed based on type + get unitOptions() { + switch (this.type) { + case 'speed': + return [ + { label: 'KB/s', value: 'KB' }, + { label: 'MB/s', value: 'MB' } + ]; + case 'size': + default: + return [ + { label: 'MB', value: 'MB' }, + { label: 'GB', value: 'GB' } + ]; + } + } + + // Get default unit based on type + private getDefaultUnit(): ByteSizeUnit { + switch (this.type) { + case 'speed': + return 'KB'; + case 'size': + default: + return 'MB'; + } + } // ControlValueAccessor interface methods private onChange: (value: string) => void = () => {}; private onTouched: () => void = () => {}; + ngOnInit(): void { + this.unit.set(this.getDefaultUnit()); + } + /** * Parse the string value in format '100MB', '1.5GB', etc. */ writeValue(value: string): void { if (!value) { this.value.set(null); + this.unit.set(this.getDefaultUnit()); return; } @@ -62,15 +90,24 @@ export class ByteSizeInputComponent implements ControlValueAccessor { if (match) { const numValue = parseFloat(match[1]); const unit = match[2].toUpperCase() as ByteSizeUnit; - - this.value.set(numValue); - this.unit.set(unit); + // Validate unit is allowed for this type + const allowedUnits = this.unitOptions.map(opt => opt.value); + if (allowedUnits.includes(unit)) { + this.value.set(numValue); + this.unit.set(unit); + } else { + // If unit not allowed, use default + this.value.set(numValue); + this.unit.set(this.getDefaultUnit()); + } } else { this.value.set(null); + this.unit.set(this.getDefaultUnit()); } } catch (e) { console.error('Error parsing byte size value:', value, e); this.value.set(null); + this.unit.set(this.getDefaultUnit()); } } @@ -91,12 +128,10 @@ export class ByteSizeInputComponent implements ControlValueAccessor { */ updateValue(): void { this.onTouched(); - if (this.value() === null) { this.onChange(''); return; } - // Format as "100MB", "1.5GB", etc. const formattedValue = `${this.value()}${this.unit()}`; this.onChange(formattedValue);