mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-01 18:37:48 -05:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1aeb3da31 | ||
|
|
283b09e8f1 | ||
|
|
b03c96249b | ||
|
|
2971445090 | ||
|
|
55c23419cd | ||
|
|
c4b9d9503a |
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
@@ -28,6 +29,8 @@ public static class MainDI
|
||||
{
|
||||
config.AddConsumer<DownloadRemoverConsumer<SearchItem>>();
|
||||
config.AddConsumer<DownloadRemoverConsumer<SeriesSearchItem>>();
|
||||
config.AddConsumer<DownloadHunterConsumer<SearchItem>>();
|
||||
config.AddConsumer<DownloadHunterConsumer<SeriesSearchItem>>();
|
||||
|
||||
config.AddConsumer<NotificationConsumer<FailedImportStrikeNotification>>();
|
||||
config.AddConsumer<NotificationConsumer<StalledStrikeNotification>>();
|
||||
@@ -50,6 +53,14 @@ public static class MainDI
|
||||
{
|
||||
e.ConfigureConsumer<DownloadRemoverConsumer<SearchItem>>(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,33 +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<WhisparrClient>()
|
||||
.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
|
||||
|
||||
@@ -52,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;
|
||||
@@ -183,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)
|
||||
@@ -206,7 +210,7 @@ public sealed class ContentBlocker : GenericHandler
|
||||
record,
|
||||
group.Count() > 1,
|
||||
removeFromClient,
|
||||
DeleteReason.AllFilesBlocked
|
||||
result.DeleteReason
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,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)
|
||||
@@ -141,6 +141,10 @@ 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>();
|
||||
|
||||
@@ -11,4 +11,5 @@ public enum DeleteReason
|
||||
AllFilesSkipped,
|
||||
AllFilesSkippedByQBit,
|
||||
AllFilesBlocked,
|
||||
MalwareFileFound,
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -1,34 +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 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;
|
||||
@@ -39,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,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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user