From d2eb9e50e08ba9428c28b92447beb7e33b7e2e37 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Sat, 17 May 2025 20:11:28 +0300 Subject: [PATCH] fix #13 --- .../QueueCleaner/QueueCleanerConfig.cs | 2 +- .../Models/Deluge/Request/DelugeRequest.cs | 8 +- .../DependencyInjection/QuartzDI.cs | 134 +-------- code/Executable/Jobs/BackgroundJobManager.cs | 280 ++++++++++++++++++ code/Executable/config/content_blocker.json | 22 ++ code/Executable/config/download_cleaner.json | 11 + code/Executable/config/download_client.json | 13 + code/Executable/config/ignored_downloads.json | 3 + code/Executable/config/lidarr.json | 5 + code/Executable/config/queue_cleaner.json | 22 ++ code/Executable/config/radarr.json | 5 + code/Executable/config/sonarr.json | 13 + .../JsonConfigurationProvider.cs | 2 + .../Factory/DownloadClientFactory.cs | 9 +- .../Verticals/Jobs/GenericHandler.cs | 27 +- code/test/docker-compose.yml | 232 +++++++-------- 16 files changed, 518 insertions(+), 270 deletions(-) create mode 100644 code/Executable/Jobs/BackgroundJobManager.cs create mode 100644 code/Executable/config/content_blocker.json create mode 100644 code/Executable/config/download_cleaner.json create mode 100644 code/Executable/config/download_client.json create mode 100644 code/Executable/config/ignored_downloads.json create mode 100644 code/Executable/config/lidarr.json create mode 100644 code/Executable/config/queue_cleaner.json create mode 100644 code/Executable/config/radarr.json create mode 100644 code/Executable/config/sonarr.json diff --git a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs b/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs index 72b64e41..4b49177e 100644 --- a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs +++ b/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs @@ -1,6 +1,6 @@ +using System.Text.Json.Serialization; using Common.CustomDataTypes; using Common.Exceptions; -using Newtonsoft.Json; namespace Common.Configuration.QueueCleaner; diff --git a/code/Domain/Models/Deluge/Request/DelugeRequest.cs b/code/Domain/Models/Deluge/Request/DelugeRequest.cs index baa6e6ef..d1838cf2 100644 --- a/code/Domain/Models/Deluge/Request/DelugeRequest.cs +++ b/code/Domain/Models/Deluge/Request/DelugeRequest.cs @@ -8,19 +8,19 @@ public class DelugeRequest public int RequestId { get; set; } [JsonProperty(PropertyName = "method")] - public String Method { get; set; } + public string Method { get; set; } [JsonProperty(PropertyName = "params")] - public List Params { get; set; } + public List Params { get; set; } [JsonIgnore] public NullValueHandling NullValueHandling { get; set; } - public DelugeRequest(int requestId, String method, params object[] parameters) + public DelugeRequest(int requestId, string method, params object[]? parameters) { RequestId = requestId; Method = method; - Params = new List(); + Params = []; if (parameters != null) { diff --git a/code/Executable/DependencyInjection/QuartzDI.cs b/code/Executable/DependencyInjection/QuartzDI.cs index 895a3cea..b586f592 100644 --- a/code/Executable/DependencyInjection/QuartzDI.cs +++ b/code/Executable/DependencyInjection/QuartzDI.cs @@ -1,16 +1,5 @@ -using Common.Configuration; -using Common.Configuration.ContentBlocker; -using Common.Configuration.DownloadCleaner; -using Common.Configuration.QueueCleaner; -using Common.Helpers; using Executable.Jobs; -using Infrastructure.Configuration; -using Infrastructure.Verticals.ContentBlocker; -using Infrastructure.Verticals.DownloadCleaner; -using Infrastructure.Verticals.Jobs; -using Infrastructure.Verticals.QueueCleaner; using Quartz; -using Quartz.Spi; namespace Executable.DependencyInjection; @@ -18,125 +7,14 @@ public static class QuartzDI { public static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) => services - .AddQuartz(q => - { - // Configure quartz to use job-specific config files - var serviceProvider = services.BuildServiceProvider(); - q.AddJobs(serviceProvider); - }) + .AddQuartz() .AddQuartzHostedService(opt => { opt.WaitForJobsToComplete = true; - }); + }) + // Register BackgroundJobManager as a hosted service + .AddSingleton() + .AddHostedService(provider => provider.GetRequiredService()); - private static void AddJobs( - this IServiceCollectionQuartzConfigurator q, - IServiceProvider serviceProvider - ) - { - var configManager = serviceProvider.GetRequiredService(); - - // Get configurations from JSON files - ContentBlockerConfig? contentBlockerConfig = configManager.GetContentBlockerConfig(); - - if (contentBlockerConfig != null) - { - q.AddJob(contentBlockerConfig, contentBlockerConfig.CronExpression); - } - - QueueCleanerConfig? queueCleanerConfig = configManager.GetQueueCleanerConfig(); - - if (queueCleanerConfig != null) - { - if (contentBlockerConfig?.Enabled is true && queueCleanerConfig is { Enabled: true, RunSequentially: true }) - { - q.AddJob(queueCleanerConfig, string.Empty); - q.AddJobListener(new JobChainingListener(nameof(ContentBlocker), nameof(QueueCleaner))); - } - else - { - q.AddJob(queueCleanerConfig, queueCleanerConfig.CronExpression); - } - } - - DownloadCleanerConfig? downloadCleanerConfig = configManager.GetDownloadCleanerConfig(); - - if (downloadCleanerConfig != null) - { - q.AddJob(downloadCleanerConfig, downloadCleanerConfig.CronExpression); - } - } - - private static void AddJob( - this IServiceCollectionQuartzConfigurator q, - IJobConfig? config, - string trigger - ) where T: GenericHandler - { - string typeName = typeof(T).Name; - - if (config is null) - { - throw new NullReferenceException($"{typeName} configuration is null"); - } - - if (!config.Enabled) - { - return; - } - - bool hasTrigger = trigger.Length > 0; - - q.AddJob>(opts => - { - opts.WithIdentity(typeName); - - if (!hasTrigger) - { - // jobs with no triggers need to be stored durably - opts.StoreDurably(); - } - }); - - // skip empty triggers - if (!hasTrigger) - { - return; - } - - IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create() - .WithIdentity("ExampleTrigger") - .StartNow() - .WithCronSchedule(trigger) - .Build(); - - IReadOnlyList nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2); - TimeSpan triggerValue = nextFireTimes[1] - nextFireTimes[0]; - - if (triggerValue > Constants.TriggerMaxLimit) - { - throw new Exception($"{trigger} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours"); - } - - if (triggerValue > StaticConfiguration.TriggerValue) - { - StaticConfiguration.TriggerValue = triggerValue; - } - - q.AddTrigger(opts => - { - opts.ForJob(typeName) - .WithIdentity($"{typeName}-trigger") - .WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing()) - .StartNow(); - }); - - // Startup trigger - q.AddTrigger(opts => - { - opts.ForJob(typeName) - .WithIdentity($"{typeName}-startup-trigger") - .StartNow(); - }); - } + // Jobs are now managed by BackgroundJobManager } \ No newline at end of file diff --git a/code/Executable/Jobs/BackgroundJobManager.cs b/code/Executable/Jobs/BackgroundJobManager.cs new file mode 100644 index 00000000..f3b1de27 --- /dev/null +++ b/code/Executable/Jobs/BackgroundJobManager.cs @@ -0,0 +1,280 @@ +using Common.Configuration.ContentBlocker; +using Common.Configuration.DownloadCleaner; +using Common.Configuration.QueueCleaner; +using Common.Helpers; +using Infrastructure.Configuration; +using Infrastructure.Verticals.ContentBlocker; +using Infrastructure.Verticals.DownloadCleaner; +using Infrastructure.Verticals.Jobs; +using Infrastructure.Verticals.QueueCleaner; +using Microsoft.Extensions.Hosting; +using Quartz; +using Quartz.Impl.Matchers; +using Quartz.Spi; + +namespace Executable.Jobs; + +/// +/// Manages background jobs in the application. +/// This class is responsible for reading configurations and scheduling jobs. +/// +public class BackgroundJobManager : IHostedService +{ + private readonly ISchedulerFactory _schedulerFactory; + private readonly IConfigManager _configManager; + private readonly ILogger _logger; + private IScheduler? _scheduler; + + public BackgroundJobManager( + ISchedulerFactory schedulerFactory, + IConfigManager configManager, + IServiceProvider serviceProvider, + ILogger logger) + { + _schedulerFactory = schedulerFactory; + _configManager = configManager; + _logger = logger; + } + + /// + /// Starts the background job manager. + /// This method is called when the application starts. + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting BackgroundJobManager"); + _scheduler = await _schedulerFactory.GetScheduler(cancellationToken); + + await InitializeJobsFromConfiguration(cancellationToken); + + _logger.LogInformation("BackgroundJobManager started"); + } + + /// + /// Stops the background job manager. + /// This method is called when the application stops. + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping BackgroundJobManager"); + + if (_scheduler != null) + { + // Don't shutdown the scheduler as it's managed by QuartzHostedService + await _scheduler.Standby(cancellationToken); + } + + _logger.LogInformation("BackgroundJobManager stopped"); + } + + /// + /// Initializes jobs based on current configuration settings. + /// + private async Task InitializeJobsFromConfiguration(CancellationToken cancellationToken = default) + { + if (_scheduler == null) + { + throw new InvalidOperationException("Scheduler not initialized"); + } + + // Get configurations from JSON files + ContentBlockerConfig? contentBlockerConfig = await _configManager.GetContentBlockerConfigAsync(); + QueueCleanerConfig? queueCleanerConfig = await _configManager.GetQueueCleanerConfigAsync(); + DownloadCleanerConfig? downloadCleanerConfig = await _configManager.GetDownloadCleanerConfigAsync(); + + // Add ContentBlocker job if enabled + if (contentBlockerConfig?.Enabled == true) + { + await AddContentBlockerJob(contentBlockerConfig, cancellationToken); + } + + // Add QueueCleaner job if enabled + if (queueCleanerConfig?.Enabled == true) + { + // Check if we need to chain it after ContentBlocker + bool shouldChainAfterContentBlocker = + contentBlockerConfig?.Enabled == true && + queueCleanerConfig.RunSequentially; + + await AddQueueCleanerJob(queueCleanerConfig, shouldChainAfterContentBlocker, cancellationToken); + } + + // Add DownloadCleaner job if enabled + if (downloadCleanerConfig?.Enabled == true) + { + await AddDownloadCleanerJob(downloadCleanerConfig, cancellationToken); + } + } + + /// + /// Adds the ContentBlocker job to the scheduler. + /// + public async Task AddContentBlockerJob(ContentBlockerConfig config, CancellationToken cancellationToken = default) + { + if (!config.Enabled) + { + return; + } + + await AddJobWithTrigger( + config, + config.CronExpression, + cancellationToken); + } + + /// + /// Adds the QueueCleaner job to the scheduler. + /// + public async Task AddQueueCleanerJob( + QueueCleanerConfig config, + bool chainAfterContentBlocker = false, + CancellationToken cancellationToken = default) + { + if (!config.Enabled) + { + return; + } + + var jobKey = new JobKey(nameof(QueueCleaner)); + + // If the job should be chained after ContentBlocker, add it without a cron trigger + if (chainAfterContentBlocker) + { + await AddJobWithoutTrigger(cancellationToken); + + // Add job listener to chain QueueCleaner after ContentBlocker + if (_scheduler != null) + { + var chainListener = new JobChainingListener(nameof(ContentBlocker), nameof(QueueCleaner)); + _scheduler.ListenerManager.AddJobListener(chainListener, KeyMatcher.KeyEquals(new JobKey(nameof(ContentBlocker)))); + } + } + else + { + // Add job with normal cron trigger + await AddJobWithTrigger( + config, + config.CronExpression, + cancellationToken); + } + } + + /// + /// Adds the DownloadCleaner job to the scheduler. + /// + public async Task AddDownloadCleanerJob(DownloadCleanerConfig config, CancellationToken cancellationToken = default) + { + if (!config.Enabled) + { + return; + } + + await AddJobWithTrigger( + config, + config.CronExpression, + cancellationToken); + } + + /// + /// Helper method to add a job with a cron trigger. + /// + private async Task AddJobWithTrigger( + Common.Configuration.IJobConfig config, + string cronExpression, + CancellationToken cancellationToken = default) + where T : GenericHandler + { + if (_scheduler == null) + { + throw new InvalidOperationException("Scheduler not initialized"); + } + + if (!config.Enabled) + { + return; + } + + string typeName = typeof(T).Name; + var jobKey = new JobKey(typeName); + + // Create job detail + var jobDetail = JobBuilder.Create>() + .StoreDurably() + .WithIdentity(jobKey) + .Build(); + + // Add job to scheduler + await _scheduler.AddJob(jobDetail, true, cancellationToken); + + // Validate the cron expression + if (!string.IsNullOrEmpty(cronExpression)) + { + IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create() + .WithIdentity("ValidationTrigger") + .StartNow() + .WithCronSchedule(cronExpression) + .Build(); + + IReadOnlyList nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2); + TimeSpan triggerValue = nextFireTimes[1] - nextFireTimes[0]; + + if (triggerValue > Constants.TriggerMaxLimit) + { + throw new Exception($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours"); + } + + if (triggerValue > StaticConfiguration.TriggerValue) + { + StaticConfiguration.TriggerValue = triggerValue; + } + } + + // Create cron trigger + var trigger = TriggerBuilder.Create() + .WithIdentity($"{typeName}-trigger") + .ForJob(jobKey) + .WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing()) + .StartNow() + .Build(); + + // Create startup trigger to run immediately + var startupTrigger = TriggerBuilder.Create() + .WithIdentity($"{typeName}-startup-trigger") + .ForJob(jobKey) + .StartNow() + .Build(); + + // Schedule job with both triggers + await _scheduler.ScheduleJob(trigger, cancellationToken); + await _scheduler.ScheduleJob(startupTrigger, cancellationToken); + + _logger.LogInformation("Added job {JobName} with cron expression {CronExpression}", + typeName, cronExpression); + } + + /// + /// Helper method to add a job without a trigger (for chained jobs). + /// + private async Task AddJobWithoutTrigger(CancellationToken cancellationToken = default) + where T : GenericHandler + { + if (_scheduler == null) + { + throw new InvalidOperationException("Scheduler not initialized"); + } + + string typeName = typeof(T).Name; + var jobKey = new JobKey(typeName); + + // Create job detail that is durable (can exist without triggers) + var jobDetail = JobBuilder.Create>() + .WithIdentity(jobKey) + .StoreDurably() + .Build(); + + // Add job to scheduler + await _scheduler.AddJob(jobDetail, true, cancellationToken); + + _logger.LogInformation("Added job {JobName} without trigger (will be chained)", typeName); + } +} diff --git a/code/Executable/config/content_blocker.json b/code/Executable/config/content_blocker.json new file mode 100644 index 00000000..795d574b --- /dev/null +++ b/code/Executable/config/content_blocker.json @@ -0,0 +1,22 @@ +{ + "enabled": false, + "cron_expression": "0 0/5 * * * ?", + "ignore_private": false, + "delete_private": false, + "ignored_downloads_path": "", + "sonarr": { + "enabled": false, + "type": 0, + "path": null + }, + "radarr": { + "enabled": false, + "type": 0, + "path": null + }, + "lidarr": { + "enabled": false, + "type": 0, + "path": null + } +} \ No newline at end of file diff --git a/code/Executable/config/download_cleaner.json b/code/Executable/config/download_cleaner.json new file mode 100644 index 00000000..ceec878d --- /dev/null +++ b/code/Executable/config/download_cleaner.json @@ -0,0 +1,11 @@ +{ + "enabled": false, + "cron_expression": "0 0 * * * ?", + "categories": [], + "delete_private": false, + "ignored_downloads_path": "", + "unlinked_target_category": "cleanuperr-unlinked", + "unlinked_use_tag": false, + "unlinked_ignored_root_dir": "", + "unlinked_categories": [] +} \ No newline at end of file diff --git a/code/Executable/config/download_client.json b/code/Executable/config/download_client.json new file mode 100644 index 00000000..2e10d0b5 --- /dev/null +++ b/code/Executable/config/download_client.json @@ -0,0 +1,13 @@ +{ + "clients": [ + { + "enabled": true, + "id": "1ded77f9-199c-4325-9de6-7758b691e5e6", + "name": "qbittorrent", + "host": "http://localhost:8080", + "username": "test", + "password": "testing", + "type": "qbittorrent" + } + ] +} \ No newline at end of file diff --git a/code/Executable/config/ignored_downloads.json b/code/Executable/config/ignored_downloads.json new file mode 100644 index 00000000..b43e6e16 --- /dev/null +++ b/code/Executable/config/ignored_downloads.json @@ -0,0 +1,3 @@ +{ + "ignored_downloads": [] +} \ No newline at end of file diff --git a/code/Executable/config/lidarr.json b/code/Executable/config/lidarr.json new file mode 100644 index 00000000..61034031 --- /dev/null +++ b/code/Executable/config/lidarr.json @@ -0,0 +1,5 @@ +{ + "enabled": false, + "import_failed_max_strikes": -1, + "instances": [] +} \ No newline at end of file diff --git a/code/Executable/config/queue_cleaner.json b/code/Executable/config/queue_cleaner.json new file mode 100644 index 00000000..be66e5f7 --- /dev/null +++ b/code/Executable/config/queue_cleaner.json @@ -0,0 +1,22 @@ +{ + "enabled": true, + "cron_expression": "0/30 * * * * ?", + "run_sequentially": false, + "ignored_downloads_path": "", + "import_failed_max_strikes": 3, + "import_failed_ignore_private": false, + "import_failed_delete_private": false, + "import_failed_ignore_patterns": [], + "stalled_max_strikes": 3, + "stalled_reset_strikes_on_progress": false, + "stalled_ignore_private": false, + "stalled_delete_private": false, + "downloading_metadata_max_strikes": 3, + "slow_max_strikes": 3, + "slow_reset_strikes_on_progress": false, + "slow_ignore_private": false, + "slow_delete_private": false, + "slow_min_speed": "1KB", + "slow_max_time": 0, + "slow_ignore_above_size": "" +} \ No newline at end of file diff --git a/code/Executable/config/radarr.json b/code/Executable/config/radarr.json new file mode 100644 index 00000000..61034031 --- /dev/null +++ b/code/Executable/config/radarr.json @@ -0,0 +1,5 @@ +{ + "enabled": false, + "import_failed_max_strikes": -1, + "instances": [] +} \ No newline at end of file diff --git a/code/Executable/config/sonarr.json b/code/Executable/config/sonarr.json new file mode 100644 index 00000000..22189e49 --- /dev/null +++ b/code/Executable/config/sonarr.json @@ -0,0 +1,13 @@ +{ + "enabled": true, + "search_type": "episode", + "import_failed_max_strikes": -1, + "instances": [ + { + "id": "c260fdf2-5f79-4449-b40b-1dce1e8ef2ac", + "name": "sonarr1", + "url": "http://localhost:8989", + "api_key": "425d1e713f0c405cbbf359ac0502c1f4" + } + ] +} \ No newline at end of file diff --git a/code/Infrastructure/Configuration/JsonConfigurationProvider.cs b/code/Infrastructure/Configuration/JsonConfigurationProvider.cs index 61069850..27945dad 100644 --- a/code/Infrastructure/Configuration/JsonConfigurationProvider.cs +++ b/code/Infrastructure/Configuration/JsonConfigurationProvider.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; namespace Infrastructure.Configuration; @@ -40,6 +41,7 @@ public class JsonConfigurationProvider PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true }; + _serializerOptions.Converters.Add(new JsonStringEnumConverter()); } /// diff --git a/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs b/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs index a337570b..096c8669 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs @@ -51,8 +51,7 @@ public class DownloadClientFactory : IDownloadClientFactory /// public IEnumerable GetAllEnabledClients() { - var downloadClientConfig = _configManager.GetConfiguration("downloadclients.json") - ?? new DownloadClientConfig(); + var downloadClientConfig = _configManager.GetDownloadClientConfig(); foreach (var client in downloadClientConfig.GetEnabledClients()) { @@ -63,8 +62,7 @@ public class DownloadClientFactory : IDownloadClientFactory /// public IEnumerable GetClientsByType(DownloadClientType clientType) { - var downloadClientConfig = _configManager.GetConfiguration("downloadclients.json") - ?? new DownloadClientConfig(); + var downloadClientConfig = _configManager.GetDownloadClientConfig(); foreach (var client in downloadClientConfig.GetEnabledClients().Where(c => c.Type == clientType)) { @@ -102,8 +100,7 @@ public class DownloadClientFactory : IDownloadClientFactory private IDownloadService CreateClient(Guid clientId) { - var downloadClientConfig = _configManager.GetConfiguration("downloadclients.json") - ?? new DownloadClientConfig(); + var downloadClientConfig = _configManager.GetDownloadClientConfig(); var clientConfig = downloadClientConfig.GetClientConfig(clientId); diff --git a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs index 5f22b5a0..e5b9f1fa 100644 --- a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs +++ b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs @@ -62,28 +62,25 @@ public abstract class GenericHandler : IHandler, IDisposable _downloadServices.Clear(); // Add all enabled clients - if (_downloadClientConfig.Clients.Count > 0) + foreach (var client in _downloadClientConfig.GetEnabledClients()) { - foreach (var client in _downloadClientConfig.GetEnabledClients()) + try { - try + var service = _downloadServiceFactory.GetDownloadService(client); + if (service != null) { - var service = _downloadServiceFactory.GetDownloadService(client); - if (service != null) - { - _downloadServices.Add(service); - _logger.LogDebug("Initialized download client: {name}", client.Name); - } - else - { - _logger.LogWarning("Download client service not available for: {name}", client.Name); - } + _downloadServices.Add(service); + _logger.LogDebug("Initialized download client: {name}", client.Name); } - catch (Exception ex) + else { - _logger.LogError(ex, "Failed to initialize download client: {name}", client.Name); + _logger.LogWarning("Download client service not available for: {name}", client.Name); } } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to initialize download client: {name}", client.Name); + } } if (_downloadServices.Count == 0) diff --git a/code/test/docker-compose.yml b/code/test/docker-compose.yml index b081fc84..f592b06c 100644 --- a/code/test/docker-compose.yml +++ b/code/test/docker-compose.yml @@ -171,137 +171,137 @@ services: - 8787:8787 restart: unless-stopped - # cleanuperr: - # image: ghcr.io/flmorg/cleanuperr:latest - # container_name: cleanuperr - # environment: - # - TZ=Europe/Bucharest - # - DRY_RUN=false + cleanuperr: + image: ghcr.io/flmorg/cleanuperr:latest + container_name: cleanuperr + environment: + - TZ=Europe/Bucharest + - DRY_RUN=false - # - LOGGING__LOGLEVEL=Verbose - # - LOGGING__FILE__ENABLED=true - # - LOGGING__FILE__PATH=/var/logs - # - LOGGING__ENHANCED=true + - LOGGING__LOGLEVEL=Verbose + - LOGGING__FILE__ENABLED=true + - LOGGING__FILE__PATH=/var/logs + - LOGGING__ENHANCED=true - # - HTTP_MAX_RETRIES=0 - # - HTTP_TIMEOUT=20 + - HTTP_MAX_RETRIES=0 + - HTTP_TIMEOUT=20 - # - SEARCH_ENABLED=true - # - SEARCH_DELAY=5 + - SEARCH_ENABLED=true + - SEARCH_DELAY=5 - # - TRIGGERS__QUEUECLEANER=0/30 * * * * ? - # - TRIGGERS__CONTENTBLOCKER=0/30 * * * * ? - # - TRIGGERS__DOWNLOADCLEANER=0/30 * * * * ? + - TRIGGERS__QUEUECLEANER=0/30 * * * * ? + - TRIGGERS__CONTENTBLOCKER=0/30 * * * * ? + - TRIGGERS__DOWNLOADCLEANER=0/30 * * * * ? - # - QUEUECLEANER__ENABLED=true - # - QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored - # - QUEUECLEANER__RUNSEQUENTIALLY=true + - QUEUECLEANER__ENABLED=true + - QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored + - QUEUECLEANER__RUNSEQUENTIALLY=true - # - QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=3 - # - QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true - # - QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false - # - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample + - QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=3 + - QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true + - QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false + - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample - # - QUEUECLEANER__STALLED_MAX_STRIKES=3 - # - QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS=true - # - QUEUECLEANER__STALLED_IGNORE_PRIVATE=true - # - QUEUECLEANER__STALLED_DELETE_PRIVATE=false - # - QUEUECLEANER__DOWNLOADING_METADATA_MAX_STRIKES=3 + - QUEUECLEANER__STALLED_MAX_STRIKES=3 + - QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS=true + - QUEUECLEANER__STALLED_IGNORE_PRIVATE=true + - QUEUECLEANER__STALLED_DELETE_PRIVATE=false + - QUEUECLEANER__DOWNLOADING_METADATA_MAX_STRIKES=3 - # - QUEUECLEANER__SLOW_MAX_STRIKES=5 - # - QUEUECLEANER__SLOW_RESET_STRIKES_ON_PROGRESS=true - # - QUEUECLEANER__SLOW_IGNORE_PRIVATE=false - # - QUEUECLEANER__SLOW_DELETE_PRIVATE=false - # - QUEUECLEANER__SLOW_MIN_SPEED=1MB - # - QUEUECLEANER__SLOW_MAX_TIME=20 - # - QUEUECLEANER__SLOW_IGNORE_ABOVE_SIZE=1KB + - QUEUECLEANER__SLOW_MAX_STRIKES=5 + - QUEUECLEANER__SLOW_RESET_STRIKES_ON_PROGRESS=true + - QUEUECLEANER__SLOW_IGNORE_PRIVATE=false + - QUEUECLEANER__SLOW_DELETE_PRIVATE=false + - QUEUECLEANER__SLOW_MIN_SPEED=1MB + - QUEUECLEANER__SLOW_MAX_TIME=20 + - QUEUECLEANER__SLOW_IGNORE_ABOVE_SIZE=1KB - # - CONTENTBLOCKER__ENABLED=true - # - CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored - # - CONTENTBLOCKER__IGNORE_PRIVATE=true - # - CONTENTBLOCKER__DELETE_PRIVATE=false + - CONTENTBLOCKER__ENABLED=true + - CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored + - CONTENTBLOCKER__IGNORE_PRIVATE=true + - CONTENTBLOCKER__DELETE_PRIVATE=false - # - DOWNLOADCLEANER__ENABLED=true - # - DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored - # - DOWNLOADCLEANER__DELETE_PRIVATE=false + - DOWNLOADCLEANER__ENABLED=true + - DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored + - DOWNLOADCLEANER__DELETE_PRIVATE=false - # - DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr - # - DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1 - # - DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME=0 - # - DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME=99999 - # - DOWNLOADCLEANER__CATEGORIES__1__NAME=cleanuperr-unlinked - # - DOWNLOADCLEANER__CATEGORIES__1__MAX_RATIO=-1 - # - DOWNLOADCLEANER__CATEGORIES__1__MIN_SEED_TIME=0 - # - DOWNLOADCLEANER__CATEGORIES__1__MAX_SEED_TIME=99999 + - DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr + - DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1 + - DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME=0 + - DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME=99999 + - DOWNLOADCLEANER__CATEGORIES__1__NAME=cleanuperr-unlinked + - DOWNLOADCLEANER__CATEGORIES__1__MAX_RATIO=-1 + - DOWNLOADCLEANER__CATEGORIES__1__MIN_SEED_TIME=0 + - DOWNLOADCLEANER__CATEGORIES__1__MAX_SEED_TIME=99999 - # - DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY=cleanuperr-unlinked - # - DOWNLOADCLEANER__UNLINKED_USE_TAG=false - # - DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR=/downloads - # - DOWNLOADCLEANER__UNLINKED_CATEGORIES__0=tv-sonarr - # - DOWNLOADCLEANER__UNLINKED_CATEGORIES__1=radarr + - DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY=cleanuperr-unlinked + - DOWNLOADCLEANER__UNLINKED_USE_TAG=false + - DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR=/downloads + - DOWNLOADCLEANER__UNLINKED_CATEGORIES__0=tv-sonarr + - DOWNLOADCLEANER__UNLINKED_CATEGORIES__1=radarr - # - DOWNLOAD_CLIENT=qbittorrent - # - QBITTORRENT__URL=http://qbittorrent:8080 - # - QBITTORRENT__USERNAME=test - # - QBITTORRENT__PASSWORD=testing - # # OR - # # - DOWNLOAD_CLIENT=deluge - # # - DELUGE__URL=http://deluge:8112 - # # - DELUGE__PASSWORD=testing - # # OR - # # - DOWNLOAD_CLIENT=transmission - # # - TRANSMISSION__URL=http://transmission:9091 - # # - TRANSMISSION__USERNAME=test - # # - TRANSMISSION__PASSWORD=testing + - DOWNLOAD_CLIENT=qbittorrent + - QBITTORRENT__URL=http://qbittorrent:8080 + - QBITTORRENT__USERNAME=test + - QBITTORRENT__PASSWORD=testing + # OR + # - DOWNLOAD_CLIENT=deluge + # - DELUGE__URL=http://deluge:8112 + # - DELUGE__PASSWORD=testing + # OR + # - DOWNLOAD_CLIENT=transmission + # - TRANSMISSION__URL=http://transmission:9091 + # - TRANSMISSION__USERNAME=test + # - 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 - # - SONARR__INSTANCES__0__URL=http://sonarr:8989 - # - SONARR__INSTANCES__0__APIKEY=425d1e713f0c405cbbf359ac0502c1f4 + - 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 + - SONARR__INSTANCES__0__URL=http://sonarr:8989 + - 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 + - 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 - # - LIDARR__INSTANCES__0__APIKEY=7f677cfdc074414397af53dd633860c5 + - 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 + - LIDARR__INSTANCES__0__APIKEY=7f677cfdc074414397af53dd633860c5 - # # - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true - # # - NOTIFIARR__ON_STALLED_STRIKE=true - # # - NOTIFIARR__ON_SLOW_STRIKE=true - # # - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true - # # - NOTIFIARR__ON_DOWNLOAD_CLEANED=true - # # - NOTIFIARR__ON_CATEGORY_CHANGED=true - # # - NOTIFIARR__API_KEY=notifiarr_secret - # # - NOTIFIARR__CHANNEL_ID=discord_channel_id + # - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true + # - NOTIFIARR__ON_STALLED_STRIKE=true + # - NOTIFIARR__ON_SLOW_STRIKE=true + # - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true + # - NOTIFIARR__ON_DOWNLOAD_CLEANED=true + # - NOTIFIARR__ON_CATEGORY_CHANGED=true + # - NOTIFIARR__API_KEY=notifiarr_secret + # - NOTIFIARR__CHANNEL_ID=discord_channel_id - # # - APPRISE__ON_IMPORT_FAILED_STRIKE=true - # # - APPRISE__ON_STALLED_STRIKE=true - # # - APPRISE__ON_SLOW_STRIKE=true - # # - APPRISE__ON_QUEUE_ITEM_DELETED=true - # # - APPRISE__ON_DOWNLOAD_CLEANED=true - # # - APPRISE__URL=http://localhost:8000 - # # - APPRISE__KEY=mykey - # volumes: - # - ./data/cleanuperr/logs:/var/logs - # - ./data/cleanuperr/ignored_downloads:/ignored - # - ./data/qbittorrent/downloads:/downloads - # restart: unless-stopped - # depends_on: - # - qbittorrent - # - deluge - # - transmission - # - sonarr - # - radarr - # - lidarr - # - readarr \ No newline at end of file + # - APPRISE__ON_IMPORT_FAILED_STRIKE=true + # - APPRISE__ON_STALLED_STRIKE=true + # - APPRISE__ON_SLOW_STRIKE=true + # - APPRISE__ON_QUEUE_ITEM_DELETED=true + # - APPRISE__ON_DOWNLOAD_CLEANED=true + # - APPRISE__URL=http://localhost:8000 + # - APPRISE__KEY=mykey + volumes: + - ./data/cleanuperr/logs:/var/logs + - ./data/cleanuperr/ignored_downloads:/ignored + - ./data/qbittorrent/downloads:/downloads + restart: unless-stopped + depends_on: + - qbittorrent + - deluge + - transmission + - sonarr + - radarr + - lidarr + - readarr \ No newline at end of file