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 @@
@@ -129,7 +129,7 @@
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 @@
@@ -168,7 +168,7 @@
@@ -264,7 +264,7 @@
@@ -277,7 +277,7 @@
@@ -383,7 +383,7 @@
@@ -396,7 +396,7 @@
@@ -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);