diff --git a/code/Common/Configuration/Arr/ArrConfig.cs b/code/Common/Configuration/Arr/ArrConfig.cs index 802c5c96..6863084c 100644 --- a/code/Common/Configuration/Arr/ArrConfig.cs +++ b/code/Common/Configuration/Arr/ArrConfig.cs @@ -1,4 +1,5 @@ using Common.Configuration.ContentBlocker; +using Microsoft.Extensions.Configuration; namespace Common.Configuration.Arr; @@ -7,6 +8,9 @@ public abstract record ArrConfig public required bool Enabled { get; init; } public Block Block { get; init; } = new(); + + [ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")] + public short ImportFailedMaxStrikes { get; init; } = -1; public required List Instances { get; init; } } diff --git a/code/Executable/appsettings.Development.json b/code/Executable/appsettings.Development.json index de7c42cc..460153f7 100644 --- a/code/Executable/appsettings.Development.json +++ b/code/Executable/appsettings.Development.json @@ -85,6 +85,7 @@ }, "Sonarr": { "Enabled": true, + "IMPORT_FAILED_MAX_STRIKES": -1, "SearchType": "Episode", "Block": { "Type": "blacklist", @@ -99,6 +100,7 @@ }, "Radarr": { "Enabled": true, + "IMPORT_FAILED_MAX_STRIKES": -1, "Block": { "Type": "blacklist", "Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist" @@ -112,6 +114,7 @@ }, "Lidarr": { "Enabled": true, + "IMPORT_FAILED_MAX_STRIKES": -1, "Block": { "Type": "blacklist", "Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist" diff --git a/code/Executable/appsettings.json b/code/Executable/appsettings.json index 106489bc..e9690a47 100644 --- a/code/Executable/appsettings.json +++ b/code/Executable/appsettings.json @@ -72,6 +72,7 @@ }, "Sonarr": { "Enabled": false, + "IMPORT_FAILED_MAX_STRIKES": -1, "SearchType": "Episode", "Block": { "Type": "blacklist", @@ -86,6 +87,7 @@ }, "Radarr": { "Enabled": false, + "IMPORT_FAILED_MAX_STRIKES": -1, "Block": { "Type": "blacklist", "Path": "" @@ -99,6 +101,7 @@ }, "Lidarr": { "Enabled": false, + "IMPORT_FAILED_MAX_STRIKES": -1, "Block": { "Type": "blacklist", "Path": "" diff --git a/code/Infrastructure/Verticals/Arr/ArrClient.cs b/code/Infrastructure/Verticals/Arr/ArrClient.cs index 3574ed1f..38af9b1f 100644 --- a/code/Infrastructure/Verticals/Arr/ArrClient.cs +++ b/code/Infrastructure/Verticals/Arr/ArrClient.cs @@ -73,7 +73,7 @@ public abstract class ArrClient : IArrClient return queueResponse; } - public virtual async Task ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload) + public virtual async Task ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes) { if (_queueCleanerConfig.ImportFailedIgnorePrivate && isPrivateDownload) { @@ -102,11 +102,19 @@ public abstract class ArrClient : IArrClient _logger.LogDebug("skip failed import check | contains ignored pattern | {name}", record.Title); return false; } + + if (arrMaxStrikes is 0) + { + _logger.LogDebug("skip failed import check | arr max strikes is 0 | {name}", record.Title); + return false; + } + + ushort maxStrikes = arrMaxStrikes > 0 ? (ushort)arrMaxStrikes : _queueCleanerConfig.ImportFailedMaxStrikes; return await _striker.StrikeAndCheckLimit( record.DownloadId, record.Title, - _queueCleanerConfig.ImportFailedMaxStrikes, + maxStrikes, StrikeType.ImportFailed ); } diff --git a/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs b/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs index 30027a9d..e74f112b 100644 --- a/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs +++ b/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs @@ -9,7 +9,7 @@ public interface IArrClient { Task GetQueueItemsAsync(ArrInstance arrInstance, int page); - Task ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload); + Task ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes); Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, DeleteReason deleteReason); diff --git a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs index a998180a..aca7dee3 100644 --- a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs +++ b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs @@ -75,7 +75,7 @@ public sealed class ContentBlocker : GenericHandler await base.ExecuteAsync(); } - protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType) + protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config) { IReadOnlyList ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads(); diff --git a/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs b/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs index f6e08307..1c609cea 100644 --- a/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs +++ b/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs @@ -127,7 +127,7 @@ public sealed class DownloadCleaner : GenericHandler _logger.LogTrace("finished cleaning downloads"); } - protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType) + protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config) { using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString()); diff --git a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs index 5e7adcd8..f561c6b6 100644 --- a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs +++ b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs @@ -67,7 +67,7 @@ public abstract class GenericHandler : IHandler, IDisposable _downloadService.Dispose(); } - protected abstract Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType); + protected abstract Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config); protected async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType, bool throwOnFailure = false) { @@ -80,7 +80,7 @@ public abstract class GenericHandler : IHandler, IDisposable { try { - await ProcessInstanceAsync(arrInstance, instanceType); + await ProcessInstanceAsync(arrInstance, instanceType, config); } catch (Exception exception) { diff --git a/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs b/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs index d7752831..3cb7ac21 100644 --- a/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs +++ b/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs @@ -49,7 +49,7 @@ public sealed class QueueCleaner : GenericHandler _ignoredDownloadsProvider = ignoredDownloadsProvider; } - protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType) + protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config) { IReadOnlyList ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads(); @@ -108,7 +108,7 @@ public sealed class QueueCleaner : GenericHandler } // failed import check - bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate); + bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, config.ImportFailedMaxStrikes); DeleteReason deleteReason = downloadCheckResult.ShouldRemove ? downloadCheckResult.DeleteReason : DeleteReason.ImportFailed; if (!shouldRemoveFromArr && !downloadCheckResult.ShouldRemove) diff --git a/code/test/docker-compose.yml b/code/test/docker-compose.yml index 5e29e23f..3327019b 100644 --- a/code/test/docker-compose.yml +++ b/code/test/docker-compose.yml @@ -252,6 +252,7 @@ services: # - TRANSMISSION__PASSWORD=testing - SONARR__ENABLED=true + - SONARR__IMPORT_FAILED_MAX_STRIKES=-1 - SONARR__SEARCHTYPE=Episode - SONARR__BLOCK__TYPE=blacklist - SONARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist @@ -259,12 +260,14 @@ services: - SONARR__INSTANCES__0__APIKEY=425d1e713f0c405cbbf359ac0502c1f4 - RADARR__ENABLED=true + - RADARR__IMPORT_FAILED_MAX_STRIKES=-1 - RADARR__BLOCK__TYPE=blacklist - RADARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist - RADARR__INSTANCES__0__URL=http://radarr:7878 - RADARR__INSTANCES__0__APIKEY=8b7454f668e54c5b8f44f56f93969761 - LIDARR__ENABLED=true + - LIDARR__IMPORT_FAILED_MAX_STRIKES=-1 - LIDARR__BLOCK__TYPE=blacklist - LIDARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist # TODO - LIDARR__INSTANCES__0__URL=http://lidarr:8686 diff --git a/docs/docs/configuration/examples/1_docker.mdx b/docs/docs/configuration/examples/1_docker.mdx index 89c8b529..2af0a24e 100644 --- a/docs/docs/configuration/examples/1_docker.mdx +++ b/docs/docs/configuration/examples/1_docker.mdx @@ -118,6 +118,7 @@ services: # - TRANSMISSION__PASSWORD=testing - SONARR__ENABLED=true + - SONARR__IMPORT_FAILED_MAX_STRIKES=-1 - SONARR__SEARCHTYPE=Episode - SONARR__BLOCK__TYPE=blacklist - SONARR__BLOCK__PATH=https://example.com/path/to/file.txt @@ -127,6 +128,7 @@ services: - SONARR__INSTANCES__1__APIKEY=secret2 - RADARR__ENABLED=true + - RADARR__IMPORT_FAILED_MAX_STRIKES=-1 - RADARR__BLOCK__TYPE=blacklist - RADARR__BLOCK__PATH=https://example.com/path/to/file.txt - RADARR__INSTANCES__0__URL=http://localhost:7878 @@ -135,6 +137,7 @@ services: - RADARR__INSTANCES__1__APIKEY=secret4 - LIDARR__ENABLED=true + - LIDARR__IMPORT_FAILED_MAX_STRIKES=-1 - LIDARR__BLOCK__TYPE=blacklist - LIDARR__BLOCK__PATH=https://example.com/path/to/file.txt - LIDARR__INSTANCES__0__URL=http://radarr:8686 diff --git a/docs/docs/configuration/examples/2_config-file.mdx b/docs/docs/configuration/examples/2_config-file.mdx index e128f3a0..502c3dcc 100644 --- a/docs/docs/configuration/examples/2_config-file.mdx +++ b/docs/docs/configuration/examples/2_config-file.mdx @@ -106,6 +106,7 @@ import { Note } from '@site/src/components/Admonition'; }, "Sonarr": { "Enabled": true, + "IMPORT_FAILED_MAX_STRIKES=-1 "SearchType": "Episode", "Block": { "Type": "blacklist", @@ -124,6 +125,7 @@ import { Note } from '@site/src/components/Admonition'; }, "Radarr": { "Enabled": true, + "IMPORT_FAILED_MAX_STRIKES": -1, "Block": { "Type": "blacklist", "Path": "https://example.com/path/to/file.txt" @@ -141,6 +143,7 @@ import { Note } from '@site/src/components/Admonition'; }, "Lidarr": { "Enabled": true, + "IMPORT_FAILED_MAX_STRIKES": -1, "Block": { "Type": "blacklist", "Path": "https://example.com/path/to/file.txt" diff --git a/docs/src/components/configuration/arrs/LidarrSettings.tsx b/docs/src/components/configuration/arrs/LidarrSettings.tsx index 358bb6ed..5e9b3430 100644 --- a/docs/src/components/configuration/arrs/LidarrSettings.tsx +++ b/docs/src/components/configuration/arrs/LidarrSettings.tsx @@ -12,6 +12,24 @@ const settings: EnvVarProps[] = [ required: false, acceptedValues: ["true", "false"], }, + { + name: "LIDARR__IMPORT_FAILED_MAX_STRIKES", + description: [ + "Number of strikes before removing a failed import. Set to `0` to never remove failed imports.", + "A strike is given when an item fails to be imported." + ], + type: "integer number", + defaultValue: "-1", + required: false, + notes: [ + "If the value is a positive number, it overwrites the values of [QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES](/cleanuperr/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES).", + "`0` means to never remove failed imports.", + "If not set to `0` or a negative number, the minimum value is `3`.", + ], + warnings: [ + "The value is not restricted to be a certain positive number. Use a low value (e.g. `1`) at your own risk." + ] + }, { name: "LIDARR__BLOCK__TYPE", description: [ diff --git a/docs/src/components/configuration/arrs/RadarrSettings.tsx b/docs/src/components/configuration/arrs/RadarrSettings.tsx index 081acb53..1ed62fcc 100644 --- a/docs/src/components/configuration/arrs/RadarrSettings.tsx +++ b/docs/src/components/configuration/arrs/RadarrSettings.tsx @@ -12,6 +12,24 @@ const settings: EnvVarProps[] = [ required: false, acceptedValues: ["true", "false"], }, + { + name: "RADARR__IMPORT_FAILED_MAX_STRIKES", + description: [ + "Number of strikes before removing a failed import. Set to `0` to never remove failed imports.", + "A strike is given when an item fails to be imported." + ], + type: "integer number", + defaultValue: "-1", + required: false, + notes: [ + "If the value is a positive number, it overwrites the values of [QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES](/cleanuperr/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES).", + "`0` means to never remove failed imports.", + "If not set to `0` or a negative number, the minimum value is `3`.", + ], + warnings: [ + "The value is not restricted to be a certain positive number. Use a low value (e.g. `1`) at your own risk." + ] + }, { name: "RADARR__BLOCK__TYPE", description: [ diff --git a/docs/src/components/configuration/arrs/SonarrSettings.tsx b/docs/src/components/configuration/arrs/SonarrSettings.tsx index d8983bf5..807fd3cb 100644 --- a/docs/src/components/configuration/arrs/SonarrSettings.tsx +++ b/docs/src/components/configuration/arrs/SonarrSettings.tsx @@ -12,6 +12,24 @@ const settings: EnvVarProps[] = [ required: false, acceptedValues: ["true", "false"], }, + { + name: "SONARR__IMPORT_FAILED_MAX_STRIKES", + description: [ + "Number of strikes before removing a failed import. Set to `0` to never remove failed imports.", + "A strike is given when an item fails to be imported." + ], + type: "integer number", + defaultValue: "-1", + required: false, + notes: [ + "If the value is a positive number, it overwrites the values of [QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES](/cleanuperr/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES).", + "`0` means to never remove failed imports.", + "If not set to `0` or a negative number, the minimum value is `3`.", + ], + warnings: [ + "The value is not restricted to be a certain positive number. Use a low value (e.g. `1`) at your own risk." + ] + }, { name: "SONARR__BLOCK__TYPE", description: [