mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-31 01:48:49 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e0127e97e | ||
|
|
5bdbc98d68 | ||
|
|
e1aeb3da31 | ||
|
|
283b09e8f1 | ||
|
|
b03c96249b | ||
|
|
2971445090 | ||
|
|
55c23419cd | ||
|
|
c4b9d9503a | ||
|
|
823b73d9f0 | ||
|
|
31632d25a4 | ||
|
|
c59951a39c | ||
|
|
d9140d7b5b | ||
|
|
90865a73b5 | ||
|
|
cc45233223 | ||
|
|
5d12d601ae | ||
|
|
88f40438af | ||
|
|
0a9ec06841 |
@@ -363,14 +363,4 @@ jobs:
|
||||
path: '${{ env.pkgName }}'
|
||||
retention-days: 30
|
||||
|
||||
- name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ env.releaseVersion }}
|
||||
tag_name: ${{ env.releaseVersion }}
|
||||
repository: ${{ env.githubRepository }}
|
||||
token: ${{ env.REPO_READONLY_PAT }}
|
||||
make_latest: true
|
||||
files: |
|
||||
${{ env.pkgName }}
|
||||
# Removed individual release step - handled by main release workflow
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -106,7 +106,7 @@ jobs:
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: Cleanuparr ${{ needs.validate.outputs.release_version }}
|
||||
name: ${{ needs.validate.outputs.release_version }}
|
||||
tag_name: ${{ needs.validate.outputs.release_version }}
|
||||
token: ${{ env.REPO_READONLY_PAT }}
|
||||
make_latest: true
|
||||
|
||||
@@ -3,7 +3,6 @@ using Cleanuparr.Application.Features.Arr.Dtos;
|
||||
using Cleanuparr.Application.Features.DownloadClient.Dtos;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
using Cleanuparr.Infrastructure.Logging;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
@@ -327,6 +326,24 @@ public class ConfigurationController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("whisparr")]
|
||||
public async Task<IActionResult> GetWhisparrConfig()
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var config = await _dataContext.ArrConfigs
|
||||
.Include(x => x.Instances)
|
||||
.AsNoTracking()
|
||||
.FirstAsync(x => x.Type == InstanceType.Whisparr);
|
||||
return Ok(config.Adapt<ArrConfigDto>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("notifications")]
|
||||
public async Task<IActionResult> GetNotificationsConfig()
|
||||
{
|
||||
@@ -822,6 +839,37 @@ public class ConfigurationController : ControllerBase
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("whisparr")]
|
||||
public async Task<IActionResult> UpdateWhisparrConfig([FromBody] UpdateWhisparrConfigDto newConfigDto)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Get existing config
|
||||
var config = await _dataContext.ArrConfigs
|
||||
.FirstAsync(x => x.Type == InstanceType.Whisparr);
|
||||
|
||||
config.FailedImportMaxStrikes = newConfigDto.FailedImportMaxStrikes;
|
||||
|
||||
// Validate the configuration
|
||||
config.Validate();
|
||||
|
||||
// Persist the configuration
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
return Ok(new { Message = "Whisparr configuration updated successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to save Whisparr configuration");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a job schedule based on configuration changes
|
||||
@@ -1296,4 +1344,114 @@ public class ConfigurationController : ControllerBase
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("whisparr/instances")]
|
||||
public async Task<IActionResult> CreateWhisparrInstance([FromBody] CreateArrInstanceDto newInstance)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Get the Whisparr config to add the instance to
|
||||
var config = await _dataContext.ArrConfigs
|
||||
.FirstAsync(x => x.Type == InstanceType.Whisparr);
|
||||
|
||||
// Create the new instance
|
||||
var instance = new ArrInstance
|
||||
{
|
||||
Enabled = newInstance.Enabled,
|
||||
Name = newInstance.Name,
|
||||
Url = new Uri(newInstance.Url),
|
||||
ApiKey = newInstance.ApiKey,
|
||||
ArrConfigId = config.Id,
|
||||
};
|
||||
|
||||
// Add to the config's instances collection
|
||||
await _dataContext.ArrInstances.AddAsync(instance);
|
||||
// Save changes
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetWhisparrConfig), new { id = instance.Id }, instance.Adapt<ArrInstanceDto>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create Whisparr instance");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("whisparr/instances/{id}")]
|
||||
public async Task<IActionResult> UpdateWhisparrInstance(Guid id, [FromBody] CreateArrInstanceDto updatedInstance)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Get the Whisparr config and find the instance
|
||||
var config = await _dataContext.ArrConfigs
|
||||
.Include(c => c.Instances)
|
||||
.FirstAsync(x => x.Type == InstanceType.Whisparr);
|
||||
|
||||
var instance = config.Instances.FirstOrDefault(i => i.Id == id);
|
||||
if (instance == null)
|
||||
{
|
||||
return NotFound($"Whisparr instance with ID {id} not found");
|
||||
}
|
||||
|
||||
// Update the instance properties
|
||||
instance.Enabled = updatedInstance.Enabled;
|
||||
instance.Name = updatedInstance.Name;
|
||||
instance.Url = new Uri(updatedInstance.Url);
|
||||
instance.ApiKey = updatedInstance.ApiKey;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
return Ok(instance.Adapt<ArrInstanceDto>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update Whisparr instance with ID {Id}", id);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("whisparr/instances/{id}")]
|
||||
public async Task<IActionResult> DeleteWhisparrInstance(Guid id)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Get the Whisparr config and find the instance
|
||||
var config = await _dataContext.ArrConfigs
|
||||
.Include(c => c.Instances)
|
||||
.FirstAsync(x => x.Type == InstanceType.Whisparr);
|
||||
|
||||
var instance = config.Instances.FirstOrDefault(i => i.Id == id);
|
||||
if (instance == null)
|
||||
{
|
||||
return NotFound($"Whisparr instance with ID {id} not found");
|
||||
}
|
||||
|
||||
// Remove the instance
|
||||
config.Instances.Remove(instance);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete Whisparr instance with ID {Id}", id);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,38 @@ public static class ApiDI
|
||||
// Map SignalR hubs
|
||||
app.MapHub<HealthStatusHub>("/api/hubs/health");
|
||||
app.MapHub<AppHub>("/api/hubs/app");
|
||||
|
||||
app.MapGet("/manifest.webmanifest", (HttpContext context) =>
|
||||
{
|
||||
var basePath = context.Request.PathBase.HasValue
|
||||
? context.Request.PathBase.Value
|
||||
: "/";
|
||||
|
||||
var manifest = new
|
||||
{
|
||||
name = "Cleanuparr",
|
||||
short_name = "Cleanuparr",
|
||||
start_url = basePath,
|
||||
display = "standalone",
|
||||
background_color = "#ffffff",
|
||||
theme_color = "#ffffff",
|
||||
icons = new[]
|
||||
{
|
||||
new {
|
||||
src = "assets/icons/icon-192x192.png",
|
||||
sizes = "192x192",
|
||||
type = "image/png"
|
||||
},
|
||||
new {
|
||||
src = "assets/icons/icon-512x512.png",
|
||||
sizes = "512x512",
|
||||
type = "image/png"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Results.Json(manifest, contentType: "application/manifest+json");
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Domain.Entities.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Consumers;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Consumers;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Consumers;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Infrastructure.Health;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
using Data.Models.Arr;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
@@ -26,7 +28,9 @@ public static class MainDI
|
||||
.AddMassTransit(config =>
|
||||
{
|
||||
config.AddConsumer<DownloadRemoverConsumer<SearchItem>>();
|
||||
config.AddConsumer<DownloadRemoverConsumer<SonarrSearchItem>>();
|
||||
config.AddConsumer<DownloadRemoverConsumer<SeriesSearchItem>>();
|
||||
config.AddConsumer<DownloadHunterConsumer<SearchItem>>();
|
||||
config.AddConsumer<DownloadHunterConsumer<SeriesSearchItem>>();
|
||||
|
||||
config.AddConsumer<NotificationConsumer<FailedImportStrikeNotification>>();
|
||||
config.AddConsumer<NotificationConsumer<StalledStrikeNotification>>();
|
||||
@@ -48,7 +52,15 @@ public static class MainDI
|
||||
cfg.ReceiveEndpoint("download-remover-queue", e =>
|
||||
{
|
||||
e.ConfigureConsumer<DownloadRemoverConsumer<SearchItem>>(context);
|
||||
e.ConfigureConsumer<DownloadRemoverConsumer<SonarrSearchItem>>(context);
|
||||
e.ConfigureConsumer<DownloadRemoverConsumer<SeriesSearchItem>>(context);
|
||||
e.ConcurrentMessageLimit = 2;
|
||||
e.PrefetchCount = 2;
|
||||
});
|
||||
|
||||
cfg.ReceiveEndpoint("download-hunter-queue", e =>
|
||||
{
|
||||
e.ConfigureConsumer<DownloadHunterConsumer<SearchItem>>(context);
|
||||
e.ConfigureConsumer<DownloadHunterConsumer<SeriesSearchItem>>(context);
|
||||
e.ConcurrentMessageLimit = 1;
|
||||
e.PrefetchCount = 1;
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.ContentBlocker;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.Files;
|
||||
@@ -23,32 +25,32 @@ public static class ServicesDI
|
||||
{
|
||||
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||
services
|
||||
.AddSingleton<IEncryptionService, AesEncryptionService>()
|
||||
.AddTransient<SensitiveDataJsonConverter>()
|
||||
.AddTransient<EventsContext>()
|
||||
.AddTransient<DataContext>()
|
||||
.AddTransient<EventPublisher>()
|
||||
.AddScoped<IEncryptionService, AesEncryptionService>()
|
||||
.AddScoped<SensitiveDataJsonConverter>()
|
||||
.AddScoped<EventsContext>()
|
||||
.AddScoped<DataContext>()
|
||||
.AddScoped<EventPublisher>()
|
||||
.AddHostedService<EventCleanupService>()
|
||||
// API services
|
||||
.AddScoped<IDryRunInterceptor, DryRunInterceptor>()
|
||||
.AddScoped<CertificateValidationService>()
|
||||
.AddScoped<SonarrClient>()
|
||||
.AddScoped<RadarrClient>()
|
||||
.AddScoped<LidarrClient>()
|
||||
.AddScoped<ReadarrClient>()
|
||||
.AddScoped<WhisparrClient>()
|
||||
.AddScoped<ArrClientFactory>()
|
||||
.AddScoped<QueueCleaner>()
|
||||
.AddScoped<ContentBlocker>()
|
||||
.AddScoped<DownloadCleaner>()
|
||||
.AddScoped<IQueueItemRemover, QueueItemRemover>()
|
||||
.AddScoped<IDownloadHunter, DownloadHunter>()
|
||||
.AddScoped<IFilenameEvaluator, FilenameEvaluator>()
|
||||
.AddScoped<IHardLinkFileService, HardLinkFileService>()
|
||||
.AddScoped<UnixHardLinkFileService>()
|
||||
.AddScoped<WindowsHardLinkFileService>()
|
||||
.AddScoped<ArrQueueIterator>()
|
||||
.AddScoped<DownloadServiceFactory>()
|
||||
.AddScoped<IStriker, Striker>()
|
||||
.AddSingleton<IJobManagementService, JobManagementService>()
|
||||
// Core services
|
||||
.AddTransient<IDryRunInterceptor, DryRunInterceptor>()
|
||||
.AddTransient<CertificateValidationService>()
|
||||
.AddTransient<SonarrClient>()
|
||||
.AddTransient<RadarrClient>()
|
||||
.AddTransient<LidarrClient>()
|
||||
.AddTransient<ReadarrClient>()
|
||||
.AddTransient<ArrClientFactory>()
|
||||
.AddTransient<QueueCleaner>()
|
||||
.AddTransient<ContentBlocker>()
|
||||
.AddTransient<DownloadCleaner>()
|
||||
.AddTransient<IQueueItemRemover, QueueItemRemover>()
|
||||
.AddTransient<IFilenameEvaluator, FilenameEvaluator>()
|
||||
.AddTransient<IHardLinkFileService, HardLinkFileService>()
|
||||
.AddTransient<UnixHardLinkFileService>()
|
||||
.AddTransient<WindowsHardLinkFileService>()
|
||||
.AddTransient<ArrQueueIterator>()
|
||||
.AddTransient<DownloadServiceFactory>()
|
||||
.AddTransient<IStriker, Striker>()
|
||||
.AddSingleton<BlocklistProvider>();
|
||||
}
|
||||
@@ -21,13 +21,16 @@ public static class HostExtensions
|
||||
logger.LogInformation("timezone: {tz}", TimeZoneInfo.Local.DisplayName);
|
||||
|
||||
// Apply db migrations
|
||||
var eventsContext = app.Services.GetRequiredService<EventsContext>();
|
||||
var scopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>();
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
|
||||
await using var eventsContext = scope.ServiceProvider.GetRequiredService<EventsContext>();
|
||||
if ((await eventsContext.Database.GetPendingMigrationsAsync()).Any())
|
||||
{
|
||||
await eventsContext.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
var configContext = app.Services.GetRequiredService<DataContext>();
|
||||
await using var configContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
if ((await configContext.Database.GetPendingMigrationsAsync()).Any())
|
||||
{
|
||||
await configContext.Database.MigrateAsync();
|
||||
|
||||
@@ -22,18 +22,18 @@ namespace Cleanuparr.Api.Jobs;
|
||||
public class BackgroundJobManager : IHostedService
|
||||
{
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<BackgroundJobManager> _logger;
|
||||
private IScheduler? _scheduler;
|
||||
|
||||
public BackgroundJobManager(
|
||||
ISchedulerFactory schedulerFactory,
|
||||
DataContext dataContext,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<BackgroundJobManager> logger
|
||||
)
|
||||
{
|
||||
_schedulerFactory = schedulerFactory;
|
||||
_dataContext = dataContext;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -86,14 +86,18 @@ public class BackgroundJobManager : IHostedService
|
||||
throw new InvalidOperationException("Scheduler not initialized");
|
||||
}
|
||||
|
||||
// Use scoped DataContext to prevent memory leaks
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
await using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
// Get configurations from db
|
||||
QueueCleanerConfig queueCleanerConfig = await _dataContext.QueueCleanerConfigs
|
||||
QueueCleanerConfig queueCleanerConfig = await dataContext.QueueCleanerConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync(cancellationToken);
|
||||
ContentBlockerConfig contentBlockerConfig = await _dataContext.ContentBlockerConfigs
|
||||
ContentBlockerConfig contentBlockerConfig = await dataContext.ContentBlockerConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync(cancellationToken);
|
||||
DownloadCleanerConfig downloadCleanerConfig = await _dataContext.DownloadCleanerConfigs
|
||||
DownloadCleanerConfig downloadCleanerConfig = await dataContext.DownloadCleanerConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ public sealed class GenericJob<T> : IJob
|
||||
where T : IHandler
|
||||
{
|
||||
private readonly ILogger<GenericJob<T>> _logger;
|
||||
private readonly T _handler;
|
||||
|
||||
public GenericJob(ILogger<GenericJob<T>> logger, T handler)
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public GenericJob(ILogger<GenericJob<T>> logger, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_handler = handler;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
@@ -23,7 +23,9 @@ public sealed class GenericJob<T> : IJob
|
||||
|
||||
try
|
||||
{
|
||||
await _handler.ExecuteAsync();
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var handler = scope.ServiceProvider.GetRequiredService<T>();
|
||||
await handler.ExecuteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ builder.Services.AddCors(options =>
|
||||
|
||||
// Register services needed for logging first
|
||||
builder.Services
|
||||
.AddTransient<LoggingConfigManager>()
|
||||
.AddScoped<LoggingConfigManager>()
|
||||
.AddSingleton<SignalRLogSink>();
|
||||
|
||||
// Add logging with proper service provider
|
||||
@@ -133,21 +133,25 @@ logger.LogInformation("Server configuration: PORT={port}, BASE_PATH={basePath}",
|
||||
await app.Init();
|
||||
|
||||
// Get LoggingConfigManager (will be created if not already registered)
|
||||
var configManager = app.Services.GetRequiredService<LoggingConfigManager>();
|
||||
|
||||
// Get the dynamic level switch for controlling log levels
|
||||
var levelSwitch = configManager.GetLevelSwitch();
|
||||
var scopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>();
|
||||
using (var scope = scopeFactory.CreateScope())
|
||||
{
|
||||
var configManager = scope.ServiceProvider.GetRequiredService<LoggingConfigManager>();
|
||||
|
||||
// Get the dynamic level switch for controlling log levels
|
||||
var levelSwitch = configManager.GetLevelSwitch();
|
||||
|
||||
// Get the SignalRLogSink instance
|
||||
var signalRSink = app.Services.GetRequiredService<SignalRLogSink>();
|
||||
// Get the SignalRLogSink instance
|
||||
var signalRSink = app.Services.GetRequiredService<SignalRLogSink>();
|
||||
|
||||
var logConfig = LoggingDI.GetDefaultLoggerConfiguration();
|
||||
logConfig.MinimumLevel.ControlledBy(levelSwitch);
|
||||
var logConfig = LoggingDI.GetDefaultLoggerConfiguration();
|
||||
logConfig.MinimumLevel.ControlledBy(levelSwitch);
|
||||
|
||||
// Add to Serilog pipeline
|
||||
logConfig.WriteTo.Sink(signalRSink);
|
||||
// Add to Serilog pipeline
|
||||
logConfig.WriteTo.Sink(signalRSink);
|
||||
|
||||
Log.Logger = logConfig.CreateLogger();
|
||||
Log.Logger = logConfig.CreateLogger();
|
||||
}
|
||||
|
||||
// Configure health check endpoints before the API configuration
|
||||
app.MapHealthChecks("/health", new HealthCheckOptions
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Cleanuparr.Application.Features.Arr.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for updating Whisparr configuration basic settings (instances managed separately)
|
||||
/// </summary>
|
||||
public record UpdateWhisparrConfigDto
|
||||
{
|
||||
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||
}
|
||||
@@ -13,7 +13,6 @@ using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Data.Models.Arr.Queue;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -53,7 +52,7 @@ public sealed class ContentBlocker : GenericHandler
|
||||
|
||||
var config = ContextProvider.Get<ContentBlockerConfig>();
|
||||
|
||||
if (!config.Sonarr.Enabled && !config.Radarr.Enabled && !config.Lidarr.Enabled)
|
||||
if (!config.Sonarr.Enabled && !config.Radarr.Enabled && !config.Lidarr.Enabled && !config.Readarr.Enabled && !config.Whisparr.Enabled)
|
||||
{
|
||||
_logger.LogWarning("No blocklists are enabled");
|
||||
return;
|
||||
@@ -65,6 +64,7 @@ public sealed class ContentBlocker : GenericHandler
|
||||
var radarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr));
|
||||
var lidarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr));
|
||||
var readarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr));
|
||||
var whisparrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr));
|
||||
|
||||
if (config.Sonarr.Enabled)
|
||||
{
|
||||
@@ -85,6 +85,11 @@ public sealed class ContentBlocker : GenericHandler
|
||||
{
|
||||
await ProcessArrConfigAsync(readarrConfig, InstanceType.Readarr);
|
||||
}
|
||||
|
||||
if (config.Whisparr.Enabled)
|
||||
{
|
||||
await ProcessArrConfigAsync(whisparrConfig, InstanceType.Whisparr);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
@@ -178,6 +183,10 @@ public sealed class ContentBlocker : GenericHandler
|
||||
_logger.LogWarning("Download not found in any torrent client | {title}", record.Title);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No torrent clients enabled");
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.ShouldRemove)
|
||||
@@ -201,7 +210,7 @@ public sealed class ContentBlocker : GenericHandler
|
||||
record,
|
||||
group.Count() > 1,
|
||||
removeFromClient,
|
||||
DeleteReason.AllFilesBlocked
|
||||
result.DeleteReason
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Data.Models.Arr.Queue;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -132,7 +131,8 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr)), InstanceType.Radarr, true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr)), InstanceType.Lidarr, true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr)), InstanceType.Readarr, true);
|
||||
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr)), InstanceType.Whisparr, true);
|
||||
|
||||
if (isUnlinkedEnabled && downloadServiceWithDownloads.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {count} potential downloads to change category", downloadServiceWithDownloads.Sum(x => x.Item2.Count));
|
||||
|
||||
@@ -12,7 +12,6 @@ using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Data.Models.Arr.Queue;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -44,11 +43,13 @@ public sealed class QueueCleaner : GenericHandler
|
||||
var radarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr));
|
||||
var lidarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr));
|
||||
var readarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr));
|
||||
var whisparrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr));
|
||||
|
||||
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
||||
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
||||
await ProcessArrConfigAsync(readarrConfig, InstanceType.Readarr);
|
||||
await ProcessArrConfigAsync(whisparrConfig, InstanceType.Whisparr);
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
@@ -106,7 +107,7 @@ public sealed class QueueCleaner : GenericHandler
|
||||
|
||||
DownloadCheckResult downloadCheckResult = new();
|
||||
|
||||
if (record.Protocol is "torrent")
|
||||
if (record.Protocol.Contains("torrent", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var torrentClients = downloadServices
|
||||
.Where(x => x.ClientConfig.Type is DownloadClientType.Torrent)
|
||||
@@ -140,12 +141,16 @@ public sealed class QueueCleaner : GenericHandler
|
||||
_logger.LogWarning("Download not found in any torrent client | {title}", record.Title);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No torrent clients enabled");
|
||||
}
|
||||
}
|
||||
|
||||
var config = ContextProvider.Get<QueueCleanerConfig>();
|
||||
|
||||
// failed import check
|
||||
bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, config.FailedImport.MaxStrikes);
|
||||
bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, instance.ArrConfig.FailedImportMaxStrikes);
|
||||
DeleteReason deleteReason = downloadCheckResult.ShouldRemove ? downloadCheckResult.DeleteReason : DeleteReason.FailedImport;
|
||||
|
||||
if (!shouldRemoveFromArr && !downloadCheckResult.ShouldRemove)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Data.Models.Arr.Queue;
|
||||
|
||||
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
|
||||
public record QueueListResponse
|
||||
{
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using Data.Models.Arr.Queue;
|
||||
|
||||
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
|
||||
public sealed record QueueRecord
|
||||
{
|
||||
// Sonarr
|
||||
// Sonarr and Whisparr
|
||||
public long SeriesId { get; init; }
|
||||
public long EpisodeId { get; init; }
|
||||
public long SeasonNumber { get; init; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Data.Models.Arr.Queue;
|
||||
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
|
||||
public sealed record TrackedDownloadStatusMessage
|
||||
{
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Data.Models.Arr;
|
||||
|
||||
namespace Data.Models.Arr;
|
||||
namespace Cleanuparr.Domain.Entities.Arr;
|
||||
|
||||
public sealed class SonarrSearchItem : SearchItem
|
||||
public sealed class SeriesSearchItem : SearchItem
|
||||
{
|
||||
public long SeriesId { get; set; }
|
||||
|
||||
public SonarrSearchType SearchType { get; set; }
|
||||
public SeriesSearchType SearchType { get; set; }
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not SonarrSearchItem other)
|
||||
if (obj is not SeriesSearchItem other)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -12,5 +12,5 @@ public sealed record SonarrCommand
|
||||
|
||||
public List<long>? EpisodeIds { get; set; }
|
||||
|
||||
public SonarrSearchType SearchType { get; set; }
|
||||
public SeriesSearchType SearchType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Domain.Entities.Whisparr;
|
||||
|
||||
public sealed record WhisparrCommand
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public long? SeriesId { get; set; }
|
||||
|
||||
public long? SeasonNumber { get; set; }
|
||||
|
||||
public List<long>? EpisodeIds { get; set; }
|
||||
|
||||
public SeriesSearchType SearchType { get; set; }
|
||||
}
|
||||
@@ -11,4 +11,5 @@ public enum DeleteReason
|
||||
AllFilesSkipped,
|
||||
AllFilesSkippedByQBit,
|
||||
AllFilesBlocked,
|
||||
MalwareFileFound,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Cleanuparr.Domain.Enums;
|
||||
|
||||
public enum SonarrSearchType
|
||||
public enum SeriesSearchType
|
||||
{
|
||||
Episode,
|
||||
Season,
|
||||
@@ -6,10 +6,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Features\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FLM.QBittorrent" Version="1.0.1" />
|
||||
<PackageReference Include="FLM.Transmission" Version="1.0.3" />
|
||||
|
||||
@@ -11,15 +11,15 @@ namespace Cleanuparr.Infrastructure.Events;
|
||||
/// </summary>
|
||||
public class EventCleanupService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<EventCleanupService> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(4); // Run every 4 hours
|
||||
private readonly int _retentionDays = 30; // Keep events for 30 days
|
||||
|
||||
public EventCleanupService(IServiceProvider serviceProvider, ILogger<EventCleanupService> logger)
|
||||
public EventCleanupService(ILogger<EventCleanupService> logger, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@@ -58,7 +58,7 @@ public class EventCleanupService : BackgroundService
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<EventsContext>();
|
||||
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-_retentionDays);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications;
|
||||
@@ -47,7 +50,10 @@ public class EventPublisher
|
||||
EventType = eventType,
|
||||
Message = message,
|
||||
Severity = severity,
|
||||
Data = data != null ? JsonSerializer.Serialize(data) : null,
|
||||
Data = data != null ? JsonSerializer.Serialize(data, new JsonSerializerOptions
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
}) : null,
|
||||
TrackingId = trackingId
|
||||
};
|
||||
|
||||
@@ -75,12 +81,37 @@ public class EventPublisher
|
||||
StrikeType.SlowTime => EventType.SlowTimeStrike,
|
||||
};
|
||||
|
||||
dynamic data;
|
||||
|
||||
if (strikeType is StrikeType.FailedImport)
|
||||
{
|
||||
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||
data = new
|
||||
{
|
||||
hash,
|
||||
itemName,
|
||||
strikeCount,
|
||||
strikeType,
|
||||
failedImportReasons = record.StatusMessages ?? [],
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
data = new
|
||||
{
|
||||
hash,
|
||||
itemName,
|
||||
strikeCount,
|
||||
strikeType,
|
||||
};
|
||||
}
|
||||
|
||||
// Publish the event
|
||||
await PublishAsync(
|
||||
eventType,
|
||||
$"Item '{itemName}' has been struck {strikeCount} times for reason '{strikeType}'",
|
||||
EventSeverity.Important,
|
||||
data: new { hash, itemName, strikeCount, strikeType });
|
||||
data: data);
|
||||
|
||||
// Send notification (uses ContextProvider internally)
|
||||
await _notificationPublisher.NotifyStrike(strikeType, strikeCount);
|
||||
|
||||
@@ -7,7 +7,6 @@ using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@@ -66,7 +65,7 @@ public abstract class ArrClient : IArrClient
|
||||
return queueResponse;
|
||||
}
|
||||
|
||||
public virtual async Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, ushort arrMaxStrikes)
|
||||
public virtual async Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes)
|
||||
{
|
||||
var queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>();
|
||||
|
||||
@@ -106,6 +105,12 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
ushort maxStrikes = arrMaxStrikes > 0 ? (ushort)arrMaxStrikes : queueCleanerConfig.FailedImport.MaxStrikes;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Item {title} has failed import status with the following reason(s):\n{messages}",
|
||||
record.Title,
|
||||
string.Join("\n", record.StatusMessages?.Select(JsonConvert.SerializeObject) ?? [])
|
||||
);
|
||||
|
||||
return await _striker.StrikeAndCheckLimit(
|
||||
record.DownloadId,
|
||||
record.Title,
|
||||
@@ -207,7 +212,7 @@ public abstract class ArrClient : IArrClient
|
||||
return response;
|
||||
}
|
||||
|
||||
private bool HasIgnoredPatterns(QueueRecord record)
|
||||
private static bool HasIgnoredPatterns(QueueRecord record)
|
||||
{
|
||||
var queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>();
|
||||
|
||||
|
||||
@@ -9,18 +9,21 @@ public sealed class ArrClientFactory
|
||||
private readonly IRadarrClient _radarrClient;
|
||||
private readonly ILidarrClient _lidarrClient;
|
||||
private readonly IReadarrClient _readarrClient;
|
||||
private readonly IWhisparrClient _whisparrClient;
|
||||
|
||||
public ArrClientFactory(
|
||||
SonarrClient sonarrClient,
|
||||
RadarrClient radarrClient,
|
||||
LidarrClient lidarrClient,
|
||||
ReadarrClient readarrClient
|
||||
ReadarrClient readarrClient,
|
||||
WhisparrClient whisparrClient
|
||||
)
|
||||
{
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
_lidarrClient = lidarrClient;
|
||||
_readarrClient = readarrClient;
|
||||
_whisparrClient = whisparrClient;
|
||||
}
|
||||
|
||||
public IArrClient GetClient(InstanceType type) =>
|
||||
@@ -30,6 +33,7 @@ public sealed class ArrClientFactory
|
||||
InstanceType.Radarr => _radarrClient,
|
||||
InstanceType.Lidarr => _lidarrClient,
|
||||
InstanceType.Readarr => _readarrClient,
|
||||
InstanceType.Whisparr => _whisparrClient,
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
|
||||
@@ -10,7 +9,7 @@ public interface IArrClient
|
||||
{
|
||||
Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page);
|
||||
|
||||
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, ushort arrMaxStrikes);
|
||||
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes);
|
||||
|
||||
Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, DeleteReason deleteReason);
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
|
||||
public interface IWhisparrClient : IArrClient
|
||||
{
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -5,7 +5,6 @@ using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -5,7 +5,6 @@ using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using Cleanuparr.Domain.Entities.Arr;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Entities.Sonarr;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
@@ -6,7 +7,6 @@ using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@@ -58,7 +58,7 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/command";
|
||||
|
||||
foreach (SonarrCommand command in GetSearchCommands(items.Cast<SonarrSearchItem>().ToHashSet()))
|
||||
foreach (SonarrCommand command in GetSearchCommands(items.Cast<SeriesSearchItem>().ToHashSet()))
|
||||
{
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Content = new StringContent(
|
||||
@@ -97,7 +97,7 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
}
|
||||
|
||||
private static string GetSearchLog(
|
||||
SonarrSearchType searchType,
|
||||
SeriesSearchType searchType,
|
||||
Uri instanceUrl,
|
||||
SonarrCommand command,
|
||||
bool success,
|
||||
@@ -108,22 +108,22 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
|
||||
return searchType switch
|
||||
{
|
||||
SonarrSearchType.Episode =>
|
||||
SeriesSearchType.Episode =>
|
||||
$"episodes search {status} | {instanceUrl} | {logContext ?? $"episode ids: {string.Join(',', command.EpisodeIds)}"}",
|
||||
SonarrSearchType.Season =>
|
||||
SeriesSearchType.Season =>
|
||||
$"season search {status} | {instanceUrl} | {logContext ?? $"season: {command.SeasonNumber} series id: {command.SeriesId}"}",
|
||||
SonarrSearchType.Series => $"series search {status} | {instanceUrl} | {logContext ?? $"series id: {command.SeriesId}"}",
|
||||
SeriesSearchType.Series => $"series search {status} | {instanceUrl} | {logContext ?? $"series id: {command.SeriesId}"}",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(searchType), searchType, null)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, SonarrCommand command, SonarrSearchType searchType)
|
||||
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, SonarrCommand command, SeriesSearchType searchType)
|
||||
{
|
||||
try
|
||||
{
|
||||
StringBuilder log = new();
|
||||
|
||||
if (searchType is SonarrSearchType.Episode)
|
||||
if (searchType is SeriesSearchType.Episode)
|
||||
{
|
||||
var episodes = await GetEpisodesAsync(arrInstance, command.EpisodeIds);
|
||||
|
||||
@@ -165,7 +165,7 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
}
|
||||
}
|
||||
|
||||
if (searchType is SonarrSearchType.Season)
|
||||
if (searchType is SeriesSearchType.Season)
|
||||
{
|
||||
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
|
||||
|
||||
@@ -177,7 +177,7 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
log.Append($"[{show.Title} season {command.SeasonNumber}]");
|
||||
}
|
||||
|
||||
if (searchType is SonarrSearchType.Series)
|
||||
if (searchType is SeriesSearchType.Series)
|
||||
{
|
||||
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
|
||||
|
||||
@@ -230,7 +230,7 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
return JsonConvert.DeserializeObject<Series>(responseBody);
|
||||
}
|
||||
|
||||
private List<SonarrCommand> GetSearchCommands(HashSet<SonarrSearchItem> items)
|
||||
private List<SonarrCommand> GetSearchCommands(HashSet<SeriesSearchItem> items)
|
||||
{
|
||||
const string episodeSearch = "EpisodeSearch";
|
||||
const string seasonSearch = "SeasonSearch";
|
||||
@@ -238,29 +238,29 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
|
||||
List<SonarrCommand> commands = new();
|
||||
|
||||
foreach (SonarrSearchItem item in items)
|
||||
foreach (SeriesSearchItem item in items)
|
||||
{
|
||||
SonarrCommand command = item.SearchType is SonarrSearchType.Episode
|
||||
SonarrCommand command = item.SearchType is SeriesSearchType.Episode
|
||||
? commands.FirstOrDefault() ?? new() { Name = episodeSearch, EpisodeIds = new() }
|
||||
: new();
|
||||
|
||||
switch (item.SearchType)
|
||||
{
|
||||
case SonarrSearchType.Episode when command.EpisodeIds is null:
|
||||
case SeriesSearchType.Episode when command.EpisodeIds is null:
|
||||
command.EpisodeIds = [item.Id];
|
||||
break;
|
||||
|
||||
case SonarrSearchType.Episode when command.EpisodeIds is not null:
|
||||
case SeriesSearchType.Episode when command.EpisodeIds is not null:
|
||||
command.EpisodeIds.Add(item.Id);
|
||||
break;
|
||||
|
||||
case SonarrSearchType.Season:
|
||||
case SeriesSearchType.Season:
|
||||
command.Name = seasonSearch;
|
||||
command.SeasonNumber = item.Id;
|
||||
command.SeriesId = ((SonarrSearchItem)item).SeriesId;
|
||||
command.SeriesId = ((SeriesSearchItem)item).SeriesId;
|
||||
break;
|
||||
|
||||
case SonarrSearchType.Series:
|
||||
case SeriesSearchType.Series:
|
||||
command.Name = seriesSearch;
|
||||
command.SeriesId = item.Id;
|
||||
break;
|
||||
@@ -269,7 +269,7 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
throw new ArgumentOutOfRangeException(nameof(item.SearchType), item.SearchType, null);
|
||||
}
|
||||
|
||||
if (item.SearchType is SonarrSearchType.Episode && commands.Count > 0)
|
||||
if (item.SearchType is SeriesSearchType.Episode && commands.Count > 0)
|
||||
{
|
||||
// only one command will be generated for episodes search
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
using System.Text;
|
||||
using Cleanuparr.Domain.Entities.Arr;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Entities.Sonarr;
|
||||
using Cleanuparr.Domain.Entities.Whisparr;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr;
|
||||
|
||||
public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
{
|
||||
public WhisparrClient(
|
||||
ILogger<WhisparrClient> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor
|
||||
) : base(logger, httpClientFactory, striker, dryRunInterceptor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v3/queue";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlQuery(int page)
|
||||
{
|
||||
return $"page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true&includeEpisode=true";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlPath(long recordId)
|
||||
{
|
||||
return $"/api/v3/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/command";
|
||||
|
||||
foreach (WhisparrCommand command in GetSearchCommands(items.Cast<SeriesSearchItem>().ToHashSet()))
|
||||
{
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Content = new StringContent(
|
||||
JsonConvert.SerializeObject(command, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
string? logContext = await ComputeCommandLogContextAsync(arrInstance, command, command.SearchType);
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||
response?.Dispose();
|
||||
|
||||
_logger.LogInformation("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, true, logContext));
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, false, logContext));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (record.EpisodeId is 0 || record.SeriesId is 0)
|
||||
{
|
||||
_logger.LogDebug("skip | episode id and/or series id missing | {title}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsRecordValid(record);
|
||||
}
|
||||
|
||||
private static string GetSearchLog(
|
||||
SeriesSearchType searchType,
|
||||
Uri instanceUrl,
|
||||
WhisparrCommand command,
|
||||
bool success,
|
||||
string? logContext
|
||||
)
|
||||
{
|
||||
string status = success ? "triggered" : "failed";
|
||||
|
||||
return searchType switch
|
||||
{
|
||||
SeriesSearchType.Episode =>
|
||||
$"episodes search {status} | {instanceUrl} | {logContext ?? $"episode ids: {string.Join(',', command.EpisodeIds)}"}",
|
||||
SeriesSearchType.Season =>
|
||||
$"season search {status} | {instanceUrl} | {logContext ?? $"season: {command.SeasonNumber} series id: {command.SeriesId}"}",
|
||||
SeriesSearchType.Series => $"series search {status} | {instanceUrl} | {logContext ?? $"series id: {command.SeriesId}"}",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(searchType), searchType, null)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, WhisparrCommand command, SeriesSearchType searchType)
|
||||
{
|
||||
try
|
||||
{
|
||||
StringBuilder log = new();
|
||||
|
||||
if (searchType is SeriesSearchType.Episode)
|
||||
{
|
||||
var episodes = await GetEpisodesAsync(arrInstance, command.EpisodeIds);
|
||||
|
||||
if (episodes?.Count is null or 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var seriesIds = episodes
|
||||
.Select(x => x.SeriesId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
List<Series> series = [];
|
||||
|
||||
foreach (long id in seriesIds)
|
||||
{
|
||||
Series? show = await GetSeriesAsync(arrInstance, id);
|
||||
|
||||
if (show is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
series.Add(show);
|
||||
}
|
||||
|
||||
foreach (var group in command.EpisodeIds.GroupBy(id => episodes.First(x => x.Id == id).SeriesId))
|
||||
{
|
||||
var show = series.First(x => x.Id == group.Key);
|
||||
var episode = episodes
|
||||
.Where(ep => group.Any(x => x == ep.Id))
|
||||
.OrderBy(x => x.SeasonNumber)
|
||||
.ThenBy(x => x.EpisodeNumber)
|
||||
.Select(x => $"S{x.SeasonNumber.ToString().PadLeft(2, '0')}E{x.EpisodeNumber.ToString().PadLeft(2, '0')}")
|
||||
.ToList();
|
||||
|
||||
log.Append($"[{show.Title} {string.Join(',', episode)}]");
|
||||
}
|
||||
}
|
||||
|
||||
if (searchType is SeriesSearchType.Season)
|
||||
{
|
||||
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
|
||||
|
||||
if (show is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
log.Append($"[{show.Title} season {command.SeasonNumber}]");
|
||||
}
|
||||
|
||||
if (searchType is SeriesSearchType.Series)
|
||||
{
|
||||
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
|
||||
|
||||
if (show is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
log.Append($"[{show.Title}]");
|
||||
}
|
||||
|
||||
return log.ToString();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to compute log context");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<List<Episode>?> GetEpisodesAsync(ArrInstance arrInstance, List<long> episodeIds)
|
||||
{
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/episode";
|
||||
uriBuilder.Query = $"episodeIds={string.Join(',', episodeIds)}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
HttpResponseMessage response = await SendRequestAsync(request);
|
||||
string responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonConvert.DeserializeObject<List<Episode>>(responseContent);
|
||||
}
|
||||
|
||||
private async Task<Series?> GetSeriesAsync(ArrInstance arrInstance, long seriesId)
|
||||
{
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/series/{seriesId}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
HttpResponseMessage response = await SendRequestAsync(request);
|
||||
string responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonConvert.DeserializeObject<Series>(responseContent);
|
||||
}
|
||||
|
||||
private List<WhisparrCommand> GetSearchCommands(HashSet<SeriesSearchItem> items)
|
||||
{
|
||||
const string episodeSearch = "EpisodeSearch";
|
||||
const string seasonSearch = "SeasonSearch";
|
||||
const string seriesSearch = "SeriesSearch";
|
||||
|
||||
List<WhisparrCommand> commands = new();
|
||||
|
||||
foreach (SeriesSearchItem item in items)
|
||||
{
|
||||
WhisparrCommand command = item.SearchType is SeriesSearchType.Episode
|
||||
? commands.FirstOrDefault() ?? new() { Name = episodeSearch, EpisodeIds = new() }
|
||||
: new();
|
||||
|
||||
switch (item.SearchType)
|
||||
{
|
||||
case SeriesSearchType.Episode when command.EpisodeIds is null:
|
||||
command.EpisodeIds = [item.Id];
|
||||
break;
|
||||
|
||||
case SeriesSearchType.Episode when command.EpisodeIds is not null:
|
||||
command.EpisodeIds.Add(item.Id);
|
||||
break;
|
||||
|
||||
case SeriesSearchType.Season:
|
||||
command.Name = seasonSearch;
|
||||
command.SeasonNumber = item.Id;
|
||||
command.SeriesId = ((SeriesSearchItem)item).SeriesId;
|
||||
break;
|
||||
|
||||
case SeriesSearchType.Series:
|
||||
command.Name = seriesSearch;
|
||||
command.SeriesId = item.Id;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(item.SearchType), item.SearchType, null);
|
||||
}
|
||||
|
||||
if (item.SearchType is SeriesSearchType.Episode && commands.Count > 0)
|
||||
{
|
||||
// only one command will be generated for episodes search
|
||||
continue;
|
||||
}
|
||||
|
||||
command.SearchType = item.SearchType;
|
||||
commands.Add(command);
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Cleanuparr.Infrastructure.Features.ContentBlocker;
|
||||
public sealed class BlocklistProvider
|
||||
{
|
||||
private readonly ILogger<BlocklistProvider> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly Dictionary<InstanceType, string> _configHashes = new();
|
||||
@@ -28,13 +28,13 @@ public sealed class BlocklistProvider
|
||||
|
||||
public BlocklistProvider(
|
||||
ILogger<BlocklistProvider> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IMemoryCache cache,
|
||||
IHttpClientFactory httpClientFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_scopeFactory = scopeFactory;
|
||||
_cache = cache;
|
||||
_httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
|
||||
}
|
||||
@@ -43,7 +43,8 @@ public sealed class BlocklistProvider
|
||||
{
|
||||
try
|
||||
{
|
||||
var dataContext = _serviceProvider.GetRequiredService<DataContext>();
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
await using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
int changedCount = 0;
|
||||
var contentBlockerConfig = await dataContext.ContentBlockerConfigs
|
||||
.AsNoTracking()
|
||||
@@ -106,6 +107,17 @@ public sealed class BlocklistProvider
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
// Check and update Whisparr blocklist if needed
|
||||
string whisparrHash = GenerateSettingsHash(contentBlockerConfig.Whisparr);
|
||||
if (shouldReload || !_configHashes.TryGetValue(InstanceType.Whisparr, out string? oldWhisparrHash) || whisparrHash != oldWhisparrHash)
|
||||
{
|
||||
_logger.LogDebug("Loading Whisparr blocklist");
|
||||
|
||||
await LoadPatternsAndRegexesAsync(contentBlockerConfig.Whisparr, InstanceType.Whisparr);
|
||||
_configHashes[InstanceType.Whisparr] = whisparrHash;
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
if (changedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("Successfully loaded {count} blocklists", changedCount);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
|
||||
public sealed record BlockFilesResult
|
||||
{
|
||||
@@ -13,4 +15,6 @@ public sealed record BlockFilesResult
|
||||
public bool IsPrivate { get; set; }
|
||||
|
||||
public bool Found { get; set; }
|
||||
|
||||
public DeleteReason DeleteReason { get; set; } = DeleteReason.None;
|
||||
}
|
||||
@@ -75,8 +75,21 @@ public partial class DelugeService
|
||||
totalFiles++;
|
||||
int priority = file.Priority;
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDefinitelyMalware(name))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", file.Path, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
}
|
||||
|
||||
if (file.Priority is 0)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", file.Path);
|
||||
totalUnwantedFiles++;
|
||||
}
|
||||
|
||||
@@ -88,9 +101,15 @@ public partial class DelugeService
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||
}
|
||||
|
||||
_logger.LogTrace("File is valid | {file}", file.Path);
|
||||
priorities.Add(file.Index, priority);
|
||||
});
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!hasPriorityUpdates)
|
||||
{
|
||||
return result;
|
||||
@@ -105,8 +124,12 @@ public partial class DelugeService
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Marking {count} unwanted files as skipped for {name}", totalUnwantedFiles, download.Name);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(ChangeFilesPriority, hash, sortedPriorities);
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
@@ -61,6 +59,7 @@ public partial class DelugeService
|
||||
if (shouldRemove)
|
||||
{
|
||||
// remove if all files are unwanted
|
||||
_logger.LogTrace("all files are unwanted | removing download | {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesSkipped;
|
||||
return result;
|
||||
@@ -95,11 +94,13 @@ public partial class DelugeService
|
||||
|
||||
if (download.State is null || !download.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.LogTrace("skip slow check | item is in {state} state | {name}", download.State, download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
if (download.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
@@ -137,6 +138,7 @@ public partial class DelugeService
|
||||
|
||||
if (queueCleanerConfig.Stalled.MaxStrikes is 0)
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | max strikes is 0 | {name}", status.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
@@ -149,11 +151,13 @@ public partial class DelugeService
|
||||
|
||||
if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is in {state} state | {name}", status.State, status.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
if (status.Eta > 0)
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is not stalled | {name}", status.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,16 @@ public abstract class DownloadService : IDownloadService
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
protected bool IsDefinitelyMalware(string filename)
|
||||
{
|
||||
if (filename.Contains("thepirateheaven.org", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void ResetStalledStrikesOnProgress(string hash, long downloaded)
|
||||
{
|
||||
|
||||
@@ -20,12 +20,13 @@ namespace Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
/// </summary>
|
||||
public sealed class DownloadServiceFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<DownloadServiceFactory> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DownloadServiceFactory(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<DownloadServiceFactory> logger)
|
||||
ILogger<DownloadServiceFactory> logger,
|
||||
IServiceProvider serviceProvider
|
||||
)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
@@ -62,6 +62,7 @@ public partial class QBitService
|
||||
|
||||
if (files is null)
|
||||
{
|
||||
_logger.LogDebug("torrent {hash} has no files", hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -78,19 +79,30 @@ public partial class QBitService
|
||||
{
|
||||
if (!file.Index.HasValue)
|
||||
{
|
||||
_logger.LogTrace("Skipping file with no index | {file}", file.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (IsDefinitelyMalware(file.Name))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", file.Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (file.Priority is TorrentContentPriority.Skip)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", file.Name);
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_filenameEvaluator.IsValid(file.Name, blocklistType, patterns, regexes))
|
||||
{
|
||||
_logger.LogTrace("File is valid | {file}", file.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -101,13 +113,18 @@ public partial class QBitService
|
||||
|
||||
if (unwantedFiles.Count is 0)
|
||||
{
|
||||
_logger.LogDebug("No unwanted files found for {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Marking {count} unwanted files as skipped for {name}", totalUnwantedFiles, download.Name);
|
||||
|
||||
foreach (int fileIndex in unwantedFiles)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ public partial class QBitService
|
||||
/// <inheritdoc/>
|
||||
public override async Task<List<object>?> GetSeedingDownloads()
|
||||
{
|
||||
var torrentList = await _client.GetTorrentListAsync(new TorrentListQuery { Filter = TorrentListFilter.Seeding });
|
||||
var torrentList = await _client.GetTorrentListAsync(new TorrentListQuery { Filter = TorrentListFilter.Completed });
|
||||
return torrentList?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
|
||||
@@ -55,11 +55,13 @@ public partial class QBitService
|
||||
// if all files were blocked by qBittorrent
|
||||
if (download is { CompletionOn: not null, Downloaded: null or 0 })
|
||||
{
|
||||
_logger.LogDebug("all files are unwanted by qBit | removing download | {name}", download.Name);
|
||||
result.DeleteReason = DeleteReason.AllFilesSkippedByQBit;
|
||||
return result;
|
||||
}
|
||||
|
||||
// remove if all files are unwanted
|
||||
_logger.LogDebug("all files are unwanted | removing download | {name}", download.Name);
|
||||
result.DeleteReason = DeleteReason.AllFilesSkipped;
|
||||
return result;
|
||||
}
|
||||
@@ -87,16 +89,19 @@ public partial class QBitService
|
||||
|
||||
if (queueCleanerConfig.Slow.MaxStrikes is 0)
|
||||
{
|
||||
_logger.LogDebug("skip slow check | max strikes is 0 | {name}", download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
if (download.State is not (TorrentState.Downloading or TorrentState.ForcedDownload))
|
||||
{
|
||||
_logger.LogDebug("skip slow check | download is in {state} state | {name}", download.State, download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
if (download.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogDebug("skip slow check | download speed is 0 | {name}", download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
@@ -134,6 +139,7 @@ public partial class QBitService
|
||||
|
||||
if (queueCleanerConfig.Stalled.MaxStrikes is 0 && queueCleanerConfig.Stalled.DownloadingMetadataMaxStrikes is 0)
|
||||
{
|
||||
_logger.LogDebug("skip stalled check | max strikes is 0 | {name}", torrent.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
@@ -141,6 +147,7 @@ public partial class QBitService
|
||||
and not TorrentState.ForcedFetchingMetadata)
|
||||
{
|
||||
// ignore other states
|
||||
_logger.LogDebug("skip stalled check | download is in {state} state | {name}", torrent.State, torrent.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
@@ -168,6 +175,7 @@ public partial class QBitService
|
||||
StrikeType.DownloadingMetadata), DeleteReason.DownloadingMetadata);
|
||||
}
|
||||
|
||||
_logger.LogDebug("skip stalled check | download is not stalled | {name}", torrent.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
}
|
||||
@@ -62,19 +62,30 @@ public partial class TransmissionService
|
||||
{
|
||||
if (download.FileStats?[i].Wanted == null)
|
||||
{
|
||||
_logger.LogTrace("Skipping file with no stats | {file}", download.Files[i].Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (IsDefinitelyMalware(download.Files[i].Name))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", download.Files[i].Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!download.FileStats[i].Wanted.Value)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", download.Files[i].Name);
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_filenameEvaluator.IsValid(download.Files[i].Name, blocklistType, patterns, regexes))
|
||||
{
|
||||
_logger.LogTrace("File is valid | {file}", download.Files[i].Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -85,15 +96,18 @@ public partial class TransmissionService
|
||||
|
||||
if (unwantedFiles.Count is 0)
|
||||
{
|
||||
_logger.LogDebug("No unwanted files found for {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
_logger.LogDebug("marking {count} unwanted files as skipped for {name}", totalUnwantedFiles, download.Name);
|
||||
_logger.LogDebug("Marking {count} unwanted files as skipped for {name}", totalUnwantedFiles, download.Name);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(SetUnwantedFiles, download.Id, unwantedFiles.ToArray());
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
@@ -56,6 +54,7 @@ public partial class TransmissionService
|
||||
if (shouldRemove)
|
||||
{
|
||||
// remove if all files are unwanted
|
||||
_logger.LogDebug("all files are unwanted | removing download | {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesSkipped;
|
||||
return result;
|
||||
@@ -100,11 +99,13 @@ public partial class TransmissionService
|
||||
if (download.Status is not 4)
|
||||
{
|
||||
// not in downloading state
|
||||
_logger.LogTrace("skip slow check | download is in {state} state | {name}", download.Status, download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
if (download.RateDownload <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
@@ -142,17 +143,20 @@ public partial class TransmissionService
|
||||
|
||||
if (queueCleanerConfig.Stalled.MaxStrikes is 0)
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | max strikes is 0 | {name}", download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
if (download.Status is not 4)
|
||||
{
|
||||
// not in downloading state
|
||||
_logger.LogTrace("skip stalled check | download is in {state} state | {name}", download.Status, download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
if (download.RateDownload > 0 || download.Eta > 0)
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is not stalled | {name}", download.Name);
|
||||
return (false, DeleteReason.None);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Models;
|
||||
using Data.Models.Arr;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadHunter.Consumers;
|
||||
|
||||
public class DownloadHunterConsumer<T> : IConsumer<DownloadHuntRequest<T>>
|
||||
where T : SearchItem
|
||||
{
|
||||
private readonly ILogger<DownloadHunterConsumer<T>> _logger;
|
||||
private readonly IDownloadHunter _downloadHunter;
|
||||
|
||||
public DownloadHunterConsumer(ILogger<DownloadHunterConsumer<T>> logger, IDownloadHunter downloadHunter)
|
||||
{
|
||||
_logger = logger;
|
||||
_downloadHunter = downloadHunter;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<DownloadHuntRequest<T>> context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _downloadHunter.HuntDownloadsAsync(context.Message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception,
|
||||
"failed to search for replacement | {title} | {url}",
|
||||
context.Message.Record.Title,
|
||||
context.Message.Instance.Url
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Models;
|
||||
using Cleanuparr.Persistence;
|
||||
using Data.Models.Arr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadHunter;
|
||||
|
||||
public sealed class DownloadHunter : IDownloadHunter
|
||||
{
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly ArrClientFactory _arrClientFactory;
|
||||
|
||||
public DownloadHunter(
|
||||
DataContext dataContext,
|
||||
ArrClientFactory arrClientFactory
|
||||
)
|
||||
{
|
||||
_dataContext = dataContext;
|
||||
_arrClientFactory = arrClientFactory;
|
||||
}
|
||||
|
||||
public async Task HuntDownloadsAsync<T>(DownloadHuntRequest<T> request)
|
||||
where T : SearchItem
|
||||
{
|
||||
var generalConfig = await _dataContext.GeneralConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync();
|
||||
|
||||
if (!generalConfig.SearchEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var arrClient = _arrClientFactory.GetClient(request.InstanceType);
|
||||
await arrClient.SearchItemsAsync(request.Instance, [request.SearchItem]);
|
||||
|
||||
// prevent tracker spamming
|
||||
await Task.Delay(TimeSpan.FromSeconds(generalConfig.SearchDelay));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Models;
|
||||
using Data.Models.Arr;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadHunter.Interfaces;
|
||||
|
||||
public interface IDownloadHunter
|
||||
{
|
||||
Task HuntDownloadsAsync<T>(DownloadHuntRequest<T> request) where T : SearchItem;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadHunter.Models;
|
||||
|
||||
public sealed record DownloadHuntRequest<T>
|
||||
where T : SearchItem
|
||||
{
|
||||
public required InstanceType InstanceType { get; init; }
|
||||
|
||||
public required ArrInstance Instance { get; init; }
|
||||
|
||||
public required T SearchItem { get; init; }
|
||||
|
||||
public required QueueRecord Record { get; init; }
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class DownloadRemoverConsumer<T> : IConsumer<QueueItemRemoveRequest<T>>
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception,
|
||||
"failed to remove queue item| {title} | {url}",
|
||||
"failed to remove queue item | {title} | {url}",
|
||||
context.Message.Record.Title,
|
||||
context.Message.Instance.Url
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
|
||||
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using System.Net;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Models;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadRemover;
|
||||
|
||||
public sealed class QueueItemRemover : IQueueItemRemover
|
||||
{
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly IBus _messageBus;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ArrClientFactory _arrClientFactory;
|
||||
private readonly EventPublisher _eventPublisher;
|
||||
|
||||
public QueueItemRemover(
|
||||
DataContext dataContext,
|
||||
IBus messageBus,
|
||||
IMemoryCache cache,
|
||||
ArrClientFactory arrClientFactory,
|
||||
EventPublisher eventPublisher
|
||||
)
|
||||
{
|
||||
_dataContext = dataContext;
|
||||
_messageBus = messageBus;
|
||||
_cache = cache;
|
||||
_arrClientFactory = arrClientFactory;
|
||||
_eventPublisher = eventPublisher;
|
||||
@@ -40,31 +40,35 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
{
|
||||
try
|
||||
{
|
||||
var generalConfig = await _dataContext.GeneralConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync();
|
||||
var arrClient = _arrClientFactory.GetClient(request.InstanceType);
|
||||
await arrClient.DeleteQueueItemAsync(request.Instance, request.Record, request.RemoveFromClient, request.DeleteReason);
|
||||
|
||||
|
||||
// Set context for EventPublisher
|
||||
ContextProvider.Set("downloadName", request.Record.Title);
|
||||
ContextProvider.Set("hash", request.Record.DownloadId);
|
||||
ContextProvider.Set(nameof(QueueRecord), request.Record);
|
||||
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), request.Instance.Url);
|
||||
ContextProvider.Set(nameof(InstanceType), request.InstanceType);
|
||||
|
||||
|
||||
// Use the new centralized EventPublisher method
|
||||
await _eventPublisher.PublishQueueItemDeleted(request.RemoveFromClient, request.DeleteReason);
|
||||
|
||||
if (!generalConfig.SearchEnabled)
|
||||
await _messageBus.Publish(new DownloadHuntRequest<T>
|
||||
{
|
||||
return;
|
||||
InstanceType = request.InstanceType,
|
||||
Instance = request.Instance,
|
||||
SearchItem = request.SearchItem,
|
||||
Record = request.Record
|
||||
});
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
if (exception.StatusCode is not HttpStatusCode.NotFound)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
await arrClient.SearchItemsAsync(request.Instance, [request.SearchItem]);
|
||||
|
||||
// prevent tracker spamming
|
||||
await Task.Delay(TimeSpan.FromSeconds(generalConfig.SearchDelay));
|
||||
throw new Exception($"Item might have already been deleted by your {request.InstanceType} instance", exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ public sealed class Striker : IStriker
|
||||
{
|
||||
if (maxStrikes is 0)
|
||||
{
|
||||
_logger.LogTrace("skip striking for {reason} | max strikes is 0 | {name}", strikeType, itemName);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cleanuparr.Domain.Entities.Arr;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
@@ -13,7 +14,6 @@ using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Data.Models.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using MassTransit;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@@ -72,6 +72,9 @@ public abstract class GenericHandler : IHandler
|
||||
ContextProvider.Set(nameof(InstanceType.Readarr), await _dataContext.ArrConfigs.AsNoTracking()
|
||||
.Include(x => x.Instances)
|
||||
.FirstAsync(x => x.Type == InstanceType.Readarr));
|
||||
ContextProvider.Set(nameof(InstanceType.Whisparr), await _dataContext.ArrConfigs.AsNoTracking()
|
||||
.Include(x => x.Instances)
|
||||
.FirstAsync(x => x.Type == InstanceType.Whisparr));
|
||||
ContextProvider.Set(nameof(QueueCleanerConfig), await _dataContext.QueueCleanerConfigs.AsNoTracking().FirstAsync());
|
||||
ContextProvider.Set(nameof(ContentBlockerConfig), await _dataContext.ContentBlockerConfigs.AsNoTracking().FirstAsync());
|
||||
ContextProvider.Set(nameof(DownloadCleanerConfig), await _dataContext.DownloadCleanerConfigs.Include(x => x.Categories).AsNoTracking().FirstAsync());
|
||||
@@ -137,14 +140,14 @@ public abstract class GenericHandler : IHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if (instanceType is InstanceType.Sonarr)
|
||||
if (instanceType is InstanceType.Sonarr or InstanceType.Whisparr)
|
||||
{
|
||||
QueueItemRemoveRequest<SonarrSearchItem> removeRequest = new()
|
||||
QueueItemRemoveRequest<SeriesSearchItem> removeRequest = new()
|
||||
{
|
||||
InstanceType = instanceType,
|
||||
Instance = instance,
|
||||
Record = record,
|
||||
SearchItem = (SonarrSearchItem)GetRecordSearchItem(instanceType, record, isPack),
|
||||
SearchItem = (SeriesSearchItem)GetRecordSearchItem(instanceType, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason
|
||||
};
|
||||
@@ -174,17 +177,17 @@ public abstract class GenericHandler : IHandler
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
InstanceType.Sonarr when !isPack => new SonarrSearchItem
|
||||
InstanceType.Sonarr when !isPack => new SeriesSearchItem
|
||||
{
|
||||
Id = record.EpisodeId,
|
||||
SeriesId = record.SeriesId,
|
||||
SearchType = SonarrSearchType.Episode
|
||||
SearchType = SeriesSearchType.Episode
|
||||
},
|
||||
InstanceType.Sonarr when isPack => new SonarrSearchItem
|
||||
InstanceType.Sonarr when isPack => new SeriesSearchItem
|
||||
{
|
||||
Id = record.SeasonNumber,
|
||||
SeriesId = record.SeriesId,
|
||||
SearchType = SonarrSearchType.Season
|
||||
SearchType = SeriesSearchType.Season
|
||||
},
|
||||
InstanceType.Radarr => new SearchItem
|
||||
{
|
||||
@@ -198,6 +201,18 @@ public abstract class GenericHandler : IHandler
|
||||
{
|
||||
Id = record.BookId
|
||||
},
|
||||
InstanceType.Whisparr when !isPack => new SeriesSearchItem
|
||||
{
|
||||
Id = record.EpisodeId,
|
||||
SeriesId = record.SeriesId,
|
||||
SearchType = SeriesSearchType.Episode
|
||||
},
|
||||
InstanceType.Whisparr when isPack => new SeriesSearchItem
|
||||
{
|
||||
Id = record.SeasonNumber,
|
||||
SeriesId = record.SeriesId,
|
||||
SearchType = SeriesSearchType.Season
|
||||
},
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
|
||||
public class AppriseException : Exception
|
||||
{
|
||||
public AppriseException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AppriseException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ public sealed record ApprisePayload
|
||||
public string Type { get; init; } = NotificationType.Info.ToString().ToLowerInvariant();
|
||||
|
||||
public string Format { get; init; } = FormatType.Text.ToString().ToLowerInvariant();
|
||||
|
||||
public string? Tags { get; init; }
|
||||
}
|
||||
|
||||
public enum NotificationType
|
||||
|
||||
@@ -3,13 +3,11 @@ using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Infrastructure.Verticals.Notifications;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
|
||||
public sealed class AppriseProvider : NotificationProvider<AppriseConfig>
|
||||
{
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly IAppriseProxy _proxy;
|
||||
|
||||
public override string Name => "Apprise";
|
||||
@@ -17,7 +15,6 @@ public sealed class AppriseProvider : NotificationProvider<AppriseConfig>
|
||||
public AppriseProvider(DataContext dataContext, IAppriseProxy proxy)
|
||||
: base(dataContext.AppriseConfigs)
|
||||
{
|
||||
_dataContext = dataContext;
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
@@ -51,7 +48,7 @@ public sealed class AppriseProvider : NotificationProvider<AppriseConfig>
|
||||
await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), Config);
|
||||
}
|
||||
|
||||
private static ApprisePayload BuildPayload(ArrNotification notification, NotificationType notificationType)
|
||||
private ApprisePayload BuildPayload(ArrNotification notification, NotificationType notificationType)
|
||||
{
|
||||
StringBuilder body = new();
|
||||
body.AppendLine(notification.Description);
|
||||
@@ -70,12 +67,13 @@ public sealed class AppriseProvider : NotificationProvider<AppriseConfig>
|
||||
Title = notification.Title,
|
||||
Body = body.ToString(),
|
||||
Type = notificationType.ToString().ToLowerInvariant(),
|
||||
Tags = Config.Tags,
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static ApprisePayload BuildPayload(Notification notification, NotificationType notificationType)
|
||||
private ApprisePayload BuildPayload(Notification notification, NotificationType notificationType)
|
||||
{
|
||||
StringBuilder body = new();
|
||||
body.AppendLine(notification.Description);
|
||||
@@ -91,6 +89,7 @@ public sealed class AppriseProvider : NotificationProvider<AppriseConfig>
|
||||
Title = notification.Title,
|
||||
Body = body.ToString(),
|
||||
Type = notificationType.ToString().ToLowerInvariant(),
|
||||
Tags = Config.Tags,
|
||||
};
|
||||
|
||||
return payload;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
@@ -17,19 +18,50 @@ public sealed class AppriseProxy : IAppriseProxy
|
||||
|
||||
public async Task SendNotification(ApprisePayload payload, AppriseConfig config)
|
||||
{
|
||||
string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
|
||||
try
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
|
||||
UriBuilder uriBuilder = new(config.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/notify/{config.Key}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Method = HttpMethod.Post;
|
||||
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
|
||||
UriBuilder uriBuilder = new(config.Url.ToString());
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/notify/{config.Key}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Method = HttpMethod.Post;
|
||||
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
|
||||
|
||||
if (!string.IsNullOrEmpty(config.Url.UserInfo))
|
||||
{
|
||||
var byteArray = Encoding.ASCII.GetBytes(config.Url.UserInfo);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
|
||||
}
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
if (exception.StatusCode is null)
|
||||
{
|
||||
throw new AppriseException("Unable to send notification", exception);
|
||||
}
|
||||
|
||||
switch ((int)exception.StatusCode)
|
||||
{
|
||||
case 401:
|
||||
throw new AppriseException("Unable to send notification | API key is invalid");
|
||||
case 424:
|
||||
throw new AppriseException("Your tags are not configured correctly", exception);
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
throw new AppriseException("Unable to send notification | service unavailable", exception);
|
||||
default:
|
||||
throw new AppriseException("Unable to send notification", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Infrastructure.Verticals.Notifications;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Infrastructure.Verticals.Notifications;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public sealed record CategoryChangedNotification : Notification
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public sealed record DownloadCleanedNotification : Notification
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public sealed record FailedImportStrikeNotification : ArrNotification
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public abstract record Notification
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public sealed record NotificationField
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public enum NotificationLevel
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public sealed record QueueItemDeletedNotification : ArrNotification
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public sealed record SlowStrikeNotification : ArrNotification
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
|
||||
public sealed record StalledStrikeNotification : ArrNotification
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Infrastructure.Verticals.Notifications;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Mapster;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Notifiarr;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications;
|
||||
@@ -10,7 +10,7 @@ public abstract class NotificationProvider<T> : INotificationProvider<T>
|
||||
protected readonly DbSet<T> _notificationConfig;
|
||||
protected T? _config;
|
||||
|
||||
public T Config => _config ??= _notificationConfig.First();
|
||||
public T Config => _config ??= _notificationConfig.AsNoTracking().First();
|
||||
|
||||
NotificationConfig INotificationProvider.Config => Config;
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr.Queue;
|
||||
using Infrastructure.Interceptors;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Mapster;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -168,6 +166,7 @@ public class NotificationPublisher : INotificationPublisher
|
||||
InstanceType.Radarr => record.Movie?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Lidarr => record.Album?.Images?.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||
InstanceType.Readarr => record.Book?.Images?.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||
InstanceType.Whisparr => record.Series?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(instanceType))
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -13,9 +12,8 @@ namespace Cleanuparr.Infrastructure.Health;
|
||||
public class HealthCheckService : IHealthCheckService
|
||||
{
|
||||
private readonly ILogger<HealthCheckService> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly DownloadServiceFactory _downloadServiceFactory;
|
||||
private readonly Dictionary<Guid, HealthStatus> _healthStatuses = new();
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly object _lockObject = new();
|
||||
|
||||
/// <summary>
|
||||
@@ -25,12 +23,11 @@ public class HealthCheckService : IHealthCheckService
|
||||
|
||||
public HealthCheckService(
|
||||
ILogger<HealthCheckService> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
DownloadServiceFactory downloadServiceFactory)
|
||||
IServiceScopeFactory scopeFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_downloadServiceFactory = downloadServiceFactory;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -40,7 +37,8 @@ public class HealthCheckService : IHealthCheckService
|
||||
|
||||
try
|
||||
{
|
||||
var dataContext = _serviceProvider.GetRequiredService<DataContext>();
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
await using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
// Get the client configuration
|
||||
var downloadClientConfig = await dataContext.DownloadClients
|
||||
@@ -63,7 +61,8 @@ public class HealthCheckService : IHealthCheckService
|
||||
}
|
||||
|
||||
// Get the client instance
|
||||
var client = _downloadServiceFactory.GetDownloadService(downloadClientConfig);
|
||||
var downloadServiceFactory = scope.ServiceProvider.GetRequiredService<DownloadServiceFactory>();
|
||||
var client = downloadServiceFactory.GetDownloadService(downloadClientConfig);
|
||||
|
||||
// Execute the health check
|
||||
var healthResult = await client.HealthCheckAsync();
|
||||
@@ -107,7 +106,8 @@ public class HealthCheckService : IHealthCheckService
|
||||
|
||||
try
|
||||
{
|
||||
var dataContext = _serviceProvider.GetRequiredService<DataContext>();
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
await using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
// Get all enabled client configurations
|
||||
var enabledClients = await dataContext.DownloadClients
|
||||
|
||||
@@ -13,16 +13,16 @@ namespace Cleanuparr.Infrastructure.Http;
|
||||
public class DynamicHttpClientProvider : IDynamicHttpClientProvider
|
||||
{
|
||||
private readonly ILogger<DynamicHttpClientProvider> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IDynamicHttpClientFactory _dynamicHttpClientFactory;
|
||||
|
||||
public DynamicHttpClientProvider(
|
||||
ILogger<DynamicHttpClientProvider> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IDynamicHttpClientFactory dynamicHttpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_scopeFactory = scopeFactory;
|
||||
_dynamicHttpClientFactory = dynamicHttpClientFactory;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ public class DynamicHttpClientProvider : IDynamicHttpClientProvider
|
||||
/// <returns>A configured HttpClient instance</returns>
|
||||
private HttpClient CreateGenericClient(DownloadClientConfig downloadClientConfig)
|
||||
{
|
||||
var dataContext = _serviceProvider.GetRequiredService<DataContext>();
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
var httpConfig = dataContext.GeneralConfigs.First();
|
||||
var clientName = GetClientName(downloadClientConfig);
|
||||
|
||||
|
||||
@@ -13,16 +13,17 @@ namespace Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
/// </summary>
|
||||
public class DynamicHttpClientConfiguration : IConfigureNamedOptions<HttpClientFactoryOptions>
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public DynamicHttpClientConfiguration(IServiceProvider serviceProvider)
|
||||
public DynamicHttpClientConfiguration(IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public void Configure(string name, HttpClientFactoryOptions options)
|
||||
{
|
||||
var configStore = _serviceProvider.GetRequiredService<IHttpClientConfigStore>();
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var configStore = scope.ServiceProvider.GetRequiredService<IHttpClientConfigStore>();
|
||||
|
||||
if (!configStore.TryGetConfiguration(name, out HttpClientConfig? config))
|
||||
return;
|
||||
@@ -48,7 +49,8 @@ public class DynamicHttpClientConfiguration : IConfigureNamedOptions<HttpClientF
|
||||
|
||||
private void ConfigureHandler(HttpMessageHandlerBuilder builder, HttpClientConfig config)
|
||||
{
|
||||
var certValidationService = _serviceProvider.GetRequiredService<CertificateValidationService>();
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var certValidationService = scope.ServiceProvider.GetRequiredService<CertificateValidationService>();
|
||||
|
||||
switch (config.Type)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using DelugeService = Cleanuparr.Infrastructure.Features.DownloadClient.Deluge.DelugeService;
|
||||
@@ -13,24 +14,27 @@ namespace Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
public class HttpClientConfigurationService : IHostedService
|
||||
{
|
||||
private readonly IDynamicHttpClientFactory _clientFactory;
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly ILogger<HttpClientConfigurationService> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public HttpClientConfigurationService(
|
||||
IDynamicHttpClientFactory clientFactory,
|
||||
DataContext dataContext,
|
||||
ILogger<HttpClientConfigurationService> logger)
|
||||
ILogger<HttpClientConfigurationService> logger,
|
||||
IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_clientFactory = clientFactory;
|
||||
_dataContext = dataContext;
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _dataContext.GeneralConfigs
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
await using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
var config = await dataContext.GeneralConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync(cancellationToken);
|
||||
|
||||
|
||||
637
code/backend/Cleanuparr.Persistence/Migrations/Data/20250702091059_AddWhisparr.Designer.cs
generated
Normal file
637
code/backend/Cleanuparr.Persistence/Migrations/Data/20250702091059_AddWhisparr.Designer.cs
generated
Normal file
@@ -0,0 +1,637 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20250702091059_AddWhisparr")]
|
||||
partial class AddWhisparr
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<short>("FailedImportMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_configs");
|
||||
|
||||
b.ToTable("arr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<Guid>("ArrConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_config_id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_instances");
|
||||
|
||||
b.HasIndex("ArrConfigId")
|
||||
.HasDatabaseName("ix_arr_instances_arr_config_id");
|
||||
|
||||
b.ToTable("arr_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ignore_private");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Lidarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("lidarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Radarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Radarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("radarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Readarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Readarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("readarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sonarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Whisparr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Whisparr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("whisparr_blocklist_path");
|
||||
|
||||
b1.Property<int>("BlocklistType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_enabled");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_content_blocker_configs");
|
||||
|
||||
b.ToTable("content_blocker_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("DownloadCleanerConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_cleaner_config_id");
|
||||
|
||||
b.Property<double>("MaxRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_ratio");
|
||||
|
||||
b.Property<double>("MaxSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_seed_time");
|
||||
|
||||
b.Property<double>("MinSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("min_seed_time");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_clean_categories");
|
||||
|
||||
b.HasIndex("DownloadCleanerConfigId")
|
||||
.HasDatabaseName("ix_clean_categories_download_cleaner_config_id");
|
||||
|
||||
b.ToTable("clean_categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.PrimitiveCollection<string>("UnlinkedCategories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_categories");
|
||||
|
||||
b.Property<bool>("UnlinkedEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_enabled");
|
||||
|
||||
b.Property<string>("UnlinkedIgnoredRootDir")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_ignored_root_dir");
|
||||
|
||||
b.Property<string>("UnlinkedTargetCategory")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_target_category");
|
||||
|
||||
b.Property<bool>("UnlinkedUseTag")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_use_tag");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_cleaner_configs");
|
||||
|
||||
b.ToTable("download_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("host");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<string>("TypeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type_name");
|
||||
|
||||
b.Property<string>("UrlBase")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url_base");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_clients");
|
||||
|
||||
b.ToTable("download_clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("DisplaySupportBanner")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("display_support_banner");
|
||||
|
||||
b.Property<bool>("DryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("dry_run");
|
||||
|
||||
b.Property<string>("EncryptionKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("encryption_key");
|
||||
|
||||
b.Property<string>("HttpCertificateValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("http_certificate_validation");
|
||||
|
||||
b.Property<ushort>("HttpMaxRetries")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_max_retries");
|
||||
|
||||
b.Property<ushort>("HttpTimeout")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_timeout");
|
||||
|
||||
b.PrimitiveCollection<string>("IgnoredDownloads")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ignored_downloads");
|
||||
|
||||
b.Property<string>("LogLevel")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_level");
|
||||
|
||||
b.Property<ushort>("SearchDelay")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_delay");
|
||||
|
||||
b.Property<bool>("SearchEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_enabled");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_general_configs");
|
||||
|
||||
b.ToTable("general_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_apprise_configs");
|
||||
|
||||
b.ToTable("apprise_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<string>("ChannelId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("channel_id");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifiarr_configs");
|
||||
|
||||
b.ToTable("notifiarr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_delete_private");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_ignore_private");
|
||||
|
||||
b1.PrimitiveCollection<string>("IgnoredPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("failed_import_ignored_patterns");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Slow", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Slow#SlowConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_delete_private");
|
||||
|
||||
b1.Property<string>("IgnoreAboveSize")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_ignore_above_size");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_max_strikes");
|
||||
|
||||
b1.Property<double>("MaxTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("slow_max_time");
|
||||
|
||||
b1.Property<string>("MinSpeed")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_min_speed");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Stalled", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Stalled#StalledConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_delete_private");
|
||||
|
||||
b1.Property<ushort>("DownloadingMetadataMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_downloading_metadata_max_strikes");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_max_strikes");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_queue_cleaner_configs");
|
||||
|
||||
b.ToTable("queue_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", "ArrConfig")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("ArrConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_arr_instances_arr_configs_arr_config_id");
|
||||
|
||||
b.Navigation("ArrConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig")
|
||||
.WithMany("Categories")
|
||||
.HasForeignKey("DownloadCleanerConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_clean_categories_download_cleaner_configs_download_cleaner_config_id");
|
||||
|
||||
b.Navigation("DownloadCleanerConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWhisparr : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "whisparr_blocklist_path",
|
||||
table: "content_blocker_configs",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "whisparr_blocklist_type",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "whisparr_enabled",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "arr_configs",
|
||||
columns: new[] { "id", "failed_import_max_strikes", "type" },
|
||||
values: new object[] { new Guid("a7363ca9-224a-46c1-9a94-edda11fde7b2"), (short)-1, "whisparr" });
|
||||
|
||||
migrationBuilder.Sql("UPDATE content_blocker_configs SET whisparr_blocklist_type = 'blacklist' WHERE whisparr_blocklist_type = ''");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "whisparr_blocklist_path",
|
||||
table: "content_blocker_configs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "whisparr_blocklist_type",
|
||||
table: "content_blocker_configs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "whisparr_enabled",
|
||||
table: "content_blocker_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
620
code/backend/Cleanuparr.Persistence/Migrations/Data/20250702192200_ChangeAppriseConfig.Designer.cs
generated
Normal file
620
code/backend/Cleanuparr.Persistence/Migrations/Data/20250702192200_ChangeAppriseConfig.Designer.cs
generated
Normal file
@@ -0,0 +1,620 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20250702192200_ChangeAppriseConfig")]
|
||||
partial class ChangeAppriseConfig
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<short>("FailedImportMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_configs");
|
||||
|
||||
b.ToTable("arr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<Guid>("ArrConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_config_id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_instances");
|
||||
|
||||
b.HasIndex("ArrConfigId")
|
||||
.HasDatabaseName("ix_arr_instances_arr_config_id");
|
||||
|
||||
b.ToTable("arr_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ignore_private");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Lidarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("lidarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Radarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Radarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("radarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Readarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Readarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("readarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sonarr_enabled");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_content_blocker_configs");
|
||||
|
||||
b.ToTable("content_blocker_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("DownloadCleanerConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_cleaner_config_id");
|
||||
|
||||
b.Property<double>("MaxRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_ratio");
|
||||
|
||||
b.Property<double>("MaxSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_seed_time");
|
||||
|
||||
b.Property<double>("MinSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("min_seed_time");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_clean_categories");
|
||||
|
||||
b.HasIndex("DownloadCleanerConfigId")
|
||||
.HasDatabaseName("ix_clean_categories_download_cleaner_config_id");
|
||||
|
||||
b.ToTable("clean_categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.PrimitiveCollection<string>("UnlinkedCategories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_categories");
|
||||
|
||||
b.Property<bool>("UnlinkedEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_enabled");
|
||||
|
||||
b.Property<string>("UnlinkedIgnoredRootDir")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_ignored_root_dir");
|
||||
|
||||
b.Property<string>("UnlinkedTargetCategory")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_target_category");
|
||||
|
||||
b.Property<bool>("UnlinkedUseTag")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_use_tag");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_cleaner_configs");
|
||||
|
||||
b.ToTable("download_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("host");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<string>("TypeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type_name");
|
||||
|
||||
b.Property<string>("UrlBase")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url_base");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_clients");
|
||||
|
||||
b.ToTable("download_clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("DisplaySupportBanner")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("display_support_banner");
|
||||
|
||||
b.Property<bool>("DryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("dry_run");
|
||||
|
||||
b.Property<string>("EncryptionKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("encryption_key");
|
||||
|
||||
b.Property<string>("HttpCertificateValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("http_certificate_validation");
|
||||
|
||||
b.Property<ushort>("HttpMaxRetries")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_max_retries");
|
||||
|
||||
b.Property<ushort>("HttpTimeout")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_timeout");
|
||||
|
||||
b.PrimitiveCollection<string>("IgnoredDownloads")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ignored_downloads");
|
||||
|
||||
b.Property<string>("LogLevel")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_level");
|
||||
|
||||
b.Property<ushort>("SearchDelay")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_delay");
|
||||
|
||||
b.Property<bool>("SearchEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_enabled");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_general_configs");
|
||||
|
||||
b.ToTable("general_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("FullUrl")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("full_url");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_apprise_configs");
|
||||
|
||||
b.ToTable("apprise_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<string>("ChannelId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("channel_id");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifiarr_configs");
|
||||
|
||||
b.ToTable("notifiarr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_delete_private");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_ignore_private");
|
||||
|
||||
b1.PrimitiveCollection<string>("IgnoredPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("failed_import_ignored_patterns");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Slow", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Slow#SlowConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_delete_private");
|
||||
|
||||
b1.Property<string>("IgnoreAboveSize")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_ignore_above_size");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_max_strikes");
|
||||
|
||||
b1.Property<double>("MaxTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("slow_max_time");
|
||||
|
||||
b1.Property<string>("MinSpeed")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_min_speed");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Stalled", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Stalled#StalledConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_delete_private");
|
||||
|
||||
b1.Property<ushort>("DownloadingMetadataMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_downloading_metadata_max_strikes");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_max_strikes");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_queue_cleaner_configs");
|
||||
|
||||
b.ToTable("queue_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", "ArrConfig")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("ArrConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_arr_instances_arr_configs_arr_config_id");
|
||||
|
||||
b.Navigation("ArrConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig")
|
||||
.WithMany("Categories")
|
||||
.HasForeignKey("DownloadCleanerConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_clean_categories_download_cleaner_configs_download_cleaner_config_id");
|
||||
|
||||
b.Navigation("DownloadCleanerConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChangeAppriseConfig : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "url",
|
||||
table: "apprise_configs",
|
||||
newName: "full_url");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "full_url",
|
||||
table: "apprise_configs",
|
||||
newName: "url");
|
||||
}
|
||||
}
|
||||
}
|
||||
624
code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.Designer.cs
generated
Normal file
624
code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.Designer.cs
generated
Normal file
@@ -0,0 +1,624 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20250704152940_AddAppriseTags")]
|
||||
partial class AddAppriseTags
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<short>("FailedImportMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_configs");
|
||||
|
||||
b.ToTable("arr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<Guid>("ArrConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_config_id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_instances");
|
||||
|
||||
b.HasIndex("ArrConfigId")
|
||||
.HasDatabaseName("ix_arr_instances_arr_config_id");
|
||||
|
||||
b.ToTable("arr_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ignore_private");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Lidarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("lidarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Radarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Radarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("radarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Readarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Readarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("readarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sonarr_enabled");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_content_blocker_configs");
|
||||
|
||||
b.ToTable("content_blocker_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("DownloadCleanerConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_cleaner_config_id");
|
||||
|
||||
b.Property<double>("MaxRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_ratio");
|
||||
|
||||
b.Property<double>("MaxSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_seed_time");
|
||||
|
||||
b.Property<double>("MinSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("min_seed_time");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_clean_categories");
|
||||
|
||||
b.HasIndex("DownloadCleanerConfigId")
|
||||
.HasDatabaseName("ix_clean_categories_download_cleaner_config_id");
|
||||
|
||||
b.ToTable("clean_categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.PrimitiveCollection<string>("UnlinkedCategories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_categories");
|
||||
|
||||
b.Property<bool>("UnlinkedEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_enabled");
|
||||
|
||||
b.Property<string>("UnlinkedIgnoredRootDir")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_ignored_root_dir");
|
||||
|
||||
b.Property<string>("UnlinkedTargetCategory")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_target_category");
|
||||
|
||||
b.Property<bool>("UnlinkedUseTag")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_use_tag");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_cleaner_configs");
|
||||
|
||||
b.ToTable("download_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("host");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<string>("TypeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type_name");
|
||||
|
||||
b.Property<string>("UrlBase")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url_base");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_clients");
|
||||
|
||||
b.ToTable("download_clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("DisplaySupportBanner")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("display_support_banner");
|
||||
|
||||
b.Property<bool>("DryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("dry_run");
|
||||
|
||||
b.Property<string>("EncryptionKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("encryption_key");
|
||||
|
||||
b.Property<string>("HttpCertificateValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("http_certificate_validation");
|
||||
|
||||
b.Property<ushort>("HttpMaxRetries")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_max_retries");
|
||||
|
||||
b.Property<ushort>("HttpTimeout")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_timeout");
|
||||
|
||||
b.PrimitiveCollection<string>("IgnoredDownloads")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ignored_downloads");
|
||||
|
||||
b.Property<string>("LogLevel")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_level");
|
||||
|
||||
b.Property<ushort>("SearchDelay")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_delay");
|
||||
|
||||
b.Property<bool>("SearchEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_enabled");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_general_configs");
|
||||
|
||||
b.ToTable("general_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("FullUrl")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("full_url");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_apprise_configs");
|
||||
|
||||
b.ToTable("apprise_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<string>("ChannelId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("channel_id");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifiarr_configs");
|
||||
|
||||
b.ToTable("notifiarr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_delete_private");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_ignore_private");
|
||||
|
||||
b1.PrimitiveCollection<string>("IgnoredPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("failed_import_ignored_patterns");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Slow", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Slow#SlowConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_delete_private");
|
||||
|
||||
b1.Property<string>("IgnoreAboveSize")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_ignore_above_size");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_max_strikes");
|
||||
|
||||
b1.Property<double>("MaxTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("slow_max_time");
|
||||
|
||||
b1.Property<string>("MinSpeed")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_min_speed");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Stalled", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Stalled#StalledConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_delete_private");
|
||||
|
||||
b1.Property<ushort>("DownloadingMetadataMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_downloading_metadata_max_strikes");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_max_strikes");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_queue_cleaner_configs");
|
||||
|
||||
b.ToTable("queue_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", "ArrConfig")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("ArrConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_arr_instances_arr_configs_arr_config_id");
|
||||
|
||||
b.Navigation("ArrConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig")
|
||||
.WithMany("Categories")
|
||||
.HasForeignKey("DownloadCleanerConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_clean_categories_download_cleaner_configs_download_cleaner_config_id");
|
||||
|
||||
b.Navigation("DownloadCleanerConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAppriseTags : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tags",
|
||||
table: "apprise_configs",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tags",
|
||||
table: "apprise_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,23 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnName("sonarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Whisparr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Whisparr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("whisparr_blocklist_path");
|
||||
|
||||
b1.Property<int>("BlocklistType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_enabled");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_content_blocker_configs");
|
||||
|
||||
@@ -387,6 +404,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("FullUrl")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("full_url");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("key");
|
||||
@@ -415,9 +436,9 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.Property<string>("Url")
|
||||
b.Property<string>("Tags")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
.HasColumnName("tags");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_apprise_configs");
|
||||
|
||||
@@ -28,12 +28,15 @@ public sealed record ContentBlockerConfig : IJobConfig
|
||||
|
||||
public BlocklistSettings Readarr { get; set; } = new();
|
||||
|
||||
public BlocklistSettings Whisparr { get; set; } = new();
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
ValidateBlocklistSettings(Sonarr, "Sonarr");
|
||||
ValidateBlocklistSettings(Radarr, "Radarr");
|
||||
ValidateBlocklistSettings(Lidarr, "Lidarr");
|
||||
ValidateBlocklistSettings(Readarr, "Readarr");
|
||||
ValidateBlocklistSettings(Whisparr, "Whisparr");
|
||||
}
|
||||
|
||||
private static void ValidateBlocklistSettings(BlocklistSettings settings, string context)
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
|
||||
public sealed record AppriseConfig : NotificationConfig
|
||||
{
|
||||
public Uri? Url { get; init; }
|
||||
public string? FullUrl { get; set; }
|
||||
|
||||
public string? Key { get; init; }
|
||||
[NotMapped]
|
||||
public Uri? Url => string.IsNullOrEmpty(FullUrl) ? null : new Uri(FullUrl, UriKind.Absolute);
|
||||
|
||||
public string? Key { get; set; }
|
||||
|
||||
public string? Tags { get; set; }
|
||||
|
||||
public override bool IsValid()
|
||||
{
|
||||
|
||||
BIN
code/frontend/public/icons/apple-touch-icon.png
Normal file
BIN
code/frontend/public/icons/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
BIN
code/frontend/public/icons/icon-192x192.png
Normal file
BIN
code/frontend/public/icons/icon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
code/frontend/public/icons/icon-512x512.png
Normal file
BIN
code/frontend/public/icons/icon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"name": "Cleanuparr",
|
||||
"short_name": "Cleanuparr",
|
||||
"theme_color": "#1976d2",
|
||||
"background_color": "#fafafa",
|
||||
"display": "standalone",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export const routes: Routes = [
|
||||
{ path: 'radarr', loadComponent: () => import('./settings/radarr/radarr-settings.component').then(m => m.RadarrSettingsComponent) },
|
||||
{ path: 'lidarr', loadComponent: () => import('./settings/lidarr/lidarr-settings.component').then(m => m.LidarrSettingsComponent) },
|
||||
{ path: 'readarr', loadComponent: () => import('./settings/readarr/readarr-settings.component').then(m => m.ReadarrSettingsComponent) },
|
||||
{ path: 'whisparr', loadComponent: () => import('./settings/whisparr/whisparr-settings.component').then(m => m.WhisparrSettingsComponent) },
|
||||
{ path: 'download-clients', loadComponent: () => import('./settings/download-client/download-client-settings.component').then(m => m.DownloadClientSettingsComponent) },
|
||||
{ path: 'notifications', loadComponent: () => import('./settings/notification-settings/notification-settings.component').then(m => m.NotificationSettingsComponent) },
|
||||
];
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SonarrConfig } from "../../shared/models/sonarr-config.model";
|
||||
import { RadarrConfig } from "../../shared/models/radarr-config.model";
|
||||
import { LidarrConfig } from "../../shared/models/lidarr-config.model";
|
||||
import { ReadarrConfig } from "../../shared/models/readarr-config.model";
|
||||
import { WhisparrConfig } from "../../shared/models/whisparr-config.model";
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto } from "../../shared/models/download-client-config.model";
|
||||
import { ArrInstance, CreateArrInstanceDto } from "../../shared/models/arr-config.model";
|
||||
import { GeneralConfig } from "../../shared/models/general-config.model";
|
||||
@@ -354,6 +355,29 @@ export class ConfigurationService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Whisparr configuration
|
||||
*/
|
||||
getWhisparrConfig(): Observable<WhisparrConfig> {
|
||||
return this.http.get<WhisparrConfig>(this.ApplicationPathService.buildApiUrl('/configuration/whisparr')).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error fetching Whisparr config:", error);
|
||||
return throwError(() => new Error("Failed to load Whisparr configuration"));
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Update Whisparr configuration
|
||||
*/
|
||||
updateWhisparrConfig(config: {failedImportMaxStrikes: number}): Observable<any> {
|
||||
return this.http.put<any>(this.ApplicationPathService.buildApiUrl('/configuration/whisparr'), config).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error updating Whisparr config:", error);
|
||||
return throwError(() => new Error(error.error?.error || "Failed to update Whisparr configuration"));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Download Client configuration
|
||||
*/
|
||||
@@ -565,4 +589,42 @@ export class ConfigurationService {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ===== WHISPARR INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
* Create a new Whisparr instance
|
||||
*/
|
||||
createWhisparrInstance(instance: CreateArrInstanceDto): Observable<ArrInstance> {
|
||||
return this.http.post<ArrInstance>(this.ApplicationPathService.buildApiUrl('/configuration/whisparr/instances'), instance).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error creating Whisparr instance:", error);
|
||||
return throwError(() => new Error(error.error?.error || "Failed to create Whisparr instance"));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a Whisparr instance by ID
|
||||
*/
|
||||
updateWhisparrInstance(id: string, instance: CreateArrInstanceDto): Observable<ArrInstance> {
|
||||
return this.http.put<ArrInstance>(this.ApplicationPathService.buildApiUrl(`/configuration/whisparr/instances/${id}`), instance).pipe(
|
||||
catchError((error) => {
|
||||
console.error(`Error updating Whisparr instance with ID ${id}:`, error);
|
||||
return throwError(() => new Error(error.error?.error || `Failed to update Whisparr instance with ID ${id}`));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Whisparr instance by ID
|
||||
*/
|
||||
deleteWhisparrInstance(id: string): Observable<void> {
|
||||
return this.http.delete<void>(this.ApplicationPathService.buildApiUrl(`/configuration/whisparr/instances/${id}`)).pipe(
|
||||
catchError((error) => {
|
||||
console.error(`Error deleting Whisparr instance with ID ${id}:`, error);
|
||||
return throwError(() => new Error(error.error?.error || `Failed to delete Whisparr instance with ID ${id}`));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export class DocumentationService {
|
||||
'notifications': {
|
||||
'notifiarr.apiKey': 'notifiarr-api-key',
|
||||
'notifiarr.channelId': 'notifiarr-channel-id',
|
||||
'apprise.url': 'apprise-url',
|
||||
'apprise.fullUrl': 'apprise-url',
|
||||
'apprise.key': 'apprise-key',
|
||||
'eventTriggers': 'event-triggers'
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
</div>
|
||||
<span>Readarr</span>
|
||||
</a>
|
||||
<a [routerLink]="['/whisparr']" class="nav-item" [class.active]="router.url.includes('/whisparr')" (click)="onNavItemClick()">
|
||||
<div class="nav-icon-wrapper">
|
||||
<i class="pi pi-lock"></i>
|
||||
</div>
|
||||
<span>Whisparr</span>
|
||||
</a>
|
||||
<a [routerLink]="['/download-clients']" class="nav-item" [class.active]="router.url.includes('/download-clients')" (click)="onNavItemClick()">
|
||||
<div class="nav-icon-wrapper">
|
||||
<i class="pi pi-download"></i>
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<div class="field-input">
|
||||
<input type="text" pInputText formControlName="cronExpression" placeholder="0 0/5 * ? * * *" />
|
||||
</div>
|
||||
<small *ngIf="contentBlockerForm.get('cronExpression')?.hasError('required') && contentBlockerForm.get('cronExpression')?.touched" class="p-error">Cron expression is required</small>
|
||||
<small *ngIf="hasError('cronExpression', 'required')" class="p-error">Cron expression is required</small>
|
||||
<small class="form-helper-text">Enter a valid Quartz cron expression (e.g., "0 0/5 * ? * * *" runs every 5 minutes)</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,6 +414,76 @@
|
||||
</div>
|
||||
</p-accordion-content>
|
||||
</p-accordion-panel>
|
||||
|
||||
<!-- Whisparr Settings -->
|
||||
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="4">
|
||||
<p-accordion-header>
|
||||
<ng-template #toggleicon let-active="active">
|
||||
@if (active) {
|
||||
<i class="pi pi-chevron-up"></i>
|
||||
} @else {
|
||||
<i class="pi pi-chevron-down"></i>
|
||||
}
|
||||
</ng-template>
|
||||
Whisparr Settings
|
||||
</p-accordion-header>
|
||||
<p-accordion-content>
|
||||
<div formGroupName="whisparr">
|
||||
<div class="field-row">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-info-circle field-info-icon"
|
||||
(click)="openFieldDocs('whisparr.enabled')"
|
||||
title="Click for documentation"></i>
|
||||
Enable Whisparr Blocklist
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="enabled" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, the Whisparr blocklist will be used for content filtering</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-info-circle field-info-icon"
|
||||
(click)="openFieldDocs('whisparr.blocklistPath')"
|
||||
title="Click for documentation"></i>
|
||||
Blocklist Path
|
||||
</label>
|
||||
<p-fluid>
|
||||
<div class="field-input">
|
||||
<input pInputText formControlName="blocklistPath" placeholder="Path to blocklist file or URL" />
|
||||
</div>
|
||||
<small *ngIf="hasNestedError('whisparr', 'blocklistPath', 'required')" class="p-error">Path is required when Whisparr blocklist is enabled</small>
|
||||
<small class="form-helper-text">Path to the blocklist file or URL</small>
|
||||
</p-fluid>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-info-circle field-info-icon"
|
||||
(click)="openFieldDocs('whisparr.blocklistType')"
|
||||
title="Click for documentation"></i>
|
||||
Blocklist Type
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-select
|
||||
formControlName="blocklistType"
|
||||
[options]="[
|
||||
{ label: 'Blacklist', value: 'Blacklist' },
|
||||
{ label: 'Whitelist', value: 'Whitelist' }
|
||||
]"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
appendTo="body"
|
||||
></p-select>
|
||||
<small class="form-helper-text"
|
||||
>Type of blocklist: Blacklist (block matches) or Whitelist (only allow matches)</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p-accordion-content>
|
||||
</p-accordion-panel>
|
||||
</p-accordion>
|
||||
|
||||
<!-- Action buttons -->
|
||||
|
||||
@@ -154,6 +154,11 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
blocklistPath: [{ value: "", disabled: true }],
|
||||
blocklistType: [{ value: BlocklistType.Blacklist, disabled: true }],
|
||||
}),
|
||||
whisparr: this.formBuilder.group({
|
||||
enabled: [{ value: false, disabled: true }],
|
||||
blocklistPath: [{ value: "", disabled: true }],
|
||||
blocklistType: [{ value: BlocklistType.Blacklist, disabled: true }],
|
||||
}),
|
||||
});
|
||||
|
||||
// Create an effect to update the form when the configuration changes
|
||||
@@ -175,6 +180,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
radarr: config.radarr,
|
||||
lidarr: config.lidarr,
|
||||
readarr: config.readarr,
|
||||
whisparr: config.whisparr,
|
||||
});
|
||||
|
||||
// Update all form control states
|
||||
@@ -284,7 +290,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
}
|
||||
|
||||
// Listen for changes to blocklist enabled states
|
||||
['sonarr', 'radarr', 'lidarr', 'readarr'].forEach(arrType => {
|
||||
['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr'].forEach(arrType => {
|
||||
const enabledControl = this.contentBlockerForm.get(`${arrType}.enabled`);
|
||||
|
||||
if (enabledControl) {
|
||||
@@ -355,6 +361,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
this.updateBlocklistDependentControls('radarr', config.radarr?.enabled || false);
|
||||
this.updateBlocklistDependentControls('lidarr', config.lidarr?.enabled || false);
|
||||
this.updateBlocklistDependentControls('readarr', config.readarr?.enabled || false);
|
||||
this.updateBlocklistDependentControls('whisparr', config.whisparr?.enabled || false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,17 +422,20 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
this.contentBlockerForm.get("radarr.enabled")?.enable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("lidarr.enabled")?.enable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("readarr.enabled")?.enable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("whisparr.enabled")?.enable({ onlySelf: true });
|
||||
|
||||
// Update dependent controls based on current enabled states
|
||||
const sonarrEnabled = this.contentBlockerForm.get("sonarr.enabled")?.value || false;
|
||||
const radarrEnabled = this.contentBlockerForm.get("radarr.enabled")?.value || false;
|
||||
const lidarrEnabled = this.contentBlockerForm.get("lidarr.enabled")?.value || false;
|
||||
const readarrEnabled = this.contentBlockerForm.get("readarr.enabled")?.value || false;
|
||||
const whisparrEnabled = this.contentBlockerForm.get("whisparr.enabled")?.value || false;
|
||||
|
||||
this.updateBlocklistDependentControls('sonarr', sonarrEnabled);
|
||||
this.updateBlocklistDependentControls('radarr', radarrEnabled);
|
||||
this.updateBlocklistDependentControls('lidarr', lidarrEnabled);
|
||||
this.updateBlocklistDependentControls('readarr', readarrEnabled);
|
||||
this.updateBlocklistDependentControls('whisparr', whisparrEnabled);
|
||||
} else {
|
||||
// Disable all scheduling controls
|
||||
cronExpressionControl?.disable();
|
||||
@@ -453,6 +463,9 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
this.contentBlockerForm.get("readarr.enabled")?.disable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("readarr.blocklistPath")?.disable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("readarr.blocklistType")?.disable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("whisparr.enabled")?.disable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("whisparr.blocklistPath")?.disable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("whisparr.blocklistType")?.disable({ onlySelf: true });
|
||||
|
||||
// Save current active accordion state before clearing it
|
||||
this.activeAccordionIndices = [];
|
||||
@@ -501,6 +514,11 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
blocklistPath: "",
|
||||
blocklistType: BlocklistType.Blacklist,
|
||||
},
|
||||
whisparr: formValue.whisparr || {
|
||||
enabled: false,
|
||||
blocklistPath: "",
|
||||
blocklistType: BlocklistType.Blacklist,
|
||||
},
|
||||
};
|
||||
|
||||
// Save the configuration
|
||||
@@ -574,6 +592,11 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
blocklistPath: "",
|
||||
blocklistType: BlocklistType.Blacklist,
|
||||
},
|
||||
whisparr: {
|
||||
enabled: false,
|
||||
blocklistPath: "",
|
||||
blocklistType: BlocklistType.Blacklist,
|
||||
},
|
||||
});
|
||||
|
||||
// Manually update control states after reset
|
||||
@@ -582,6 +605,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
this.updateBlocklistDependentControls('radarr', false);
|
||||
this.updateBlocklistDependentControls('lidarr', false);
|
||||
this.updateBlocklistDependentControls('readarr', false);
|
||||
this.updateBlocklistDependentControls('whisparr', false);
|
||||
|
||||
// Mark form as dirty so the save button is enabled after reset
|
||||
this.contentBlockerForm.markAsDirty();
|
||||
@@ -605,7 +629,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
*/
|
||||
hasError(controlName: string, errorName: string): boolean {
|
||||
const control = this.contentBlockerForm.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,7 +657,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
}
|
||||
|
||||
const control = parentControl.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<div class="field-input">
|
||||
<input type="text" pInputText formControlName="cronExpression" placeholder="0 0/5 * ? * * *" />
|
||||
</div>
|
||||
<small *ngIf="downloadCleanerForm.get('cronExpression')?.hasError('required') && downloadCleanerForm.get('cronExpression')?.touched" class="p-error">Cron expression is required</small>
|
||||
<small *ngIf="hasError('cronExpression', 'required')" class="p-error">Cron expression is required</small>
|
||||
<small class="form-helper-text">Enter a valid Quartz cron expression (e.g., "0 0/5 * ? * * *" runs every 5 minutes)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -660,14 +660,14 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
*/
|
||||
hasError(controlName: string, errorName: string): boolean {
|
||||
const control = this.downloadCleanerForm.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the form has the unlinked categories validation error
|
||||
*/
|
||||
hasUnlinkedCategoriesError(): boolean {
|
||||
return this.downloadCleanerForm.touched && this.downloadCleanerForm.hasError('unlinkedCategoriesRequired');
|
||||
return this.downloadCleanerForm.dirty && this.downloadCleanerForm.hasError('unlinkedCategoriesRequired');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -695,7 +695,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
}
|
||||
|
||||
const control = parentControl.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -706,7 +706,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
if (!categoryGroup) return false;
|
||||
|
||||
const control = categoryGroup.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -715,7 +715,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
hasCategoryControlError(categoryIndex: number, controlName: string, errorName: string): boolean {
|
||||
const categoryGroup = this.categoriesFormArray.at(categoryIndex);
|
||||
const control = categoryGroup.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -723,7 +723,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
*/
|
||||
hasCategoryGroupError(categoryIndex: number, errorName: string): boolean {
|
||||
const categoryGroup = this.categoriesFormArray.at(categoryIndex);
|
||||
return categoryGroup ? categoryGroup.touched && categoryGroup.hasError(errorName) : false;
|
||||
return categoryGroup ? categoryGroup.dirty && categoryGroup.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user