From 033b50519b4cf5dc79a4a8577ecedfe08f151500 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Sat, 14 Jun 2025 01:20:37 +0300 Subject: [PATCH] db config checkpoint --- code/Common/Configuration/Arr/ArrConfig.cs | 7 +- code/Common/Configuration/Arr/ArrInstance.cs | 7 +- .../DownloadCleaner/CleanCategory.cs | 8 +- .../DownloadCleaner/DownloadCleanerConfig.cs | 4 +- .../ClientConfig.cs => DownloadClient.cs} | 29 ++-- .../DownloadClient/DownloadClientConfig.cs | 54 ------- .../Configuration/General/DryRunConfig.cs | 9 -- .../Configuration/General/GeneralConfig.cs | 22 ++- .../Notification/AppriseConfig.cs | 2 +- .../Notification/NotifiarrConfig.cs | 8 +- ...icationConfig.cs => NotificationConfig.cs} | 2 +- .../Notification/NotificationsConfig.cs | 8 - .../QueueCleaner/BlocklistSettings.cs | 3 + .../QueueCleaner/ContentBlockerConfig.cs | 3 + .../QueueCleaner/FailedImportConfig.cs | 4 +- .../QueueCleaner/QueueCleanerConfig.cs | 4 + .../Configuration/QueueCleaner/SlowConfig.cs | 4 +- .../QueueCleaner/StalledConfig.cs | 4 +- code/Data/DataContext.cs | 55 ++++++- code/Data/EventsContext.cs | 16 ++ .../20250613154308_InitialEvents.Designer.cs | 77 ---------- .../20250613154308_InitialEvents.cs | 60 -------- .../Migrations/EventsContextModelSnapshot.cs | 74 --------- .../Controllers/ConfigurationController.cs | 9 +- .../Controllers/StatusController.cs | 5 +- .../Health/HealthCheckServiceFixture.cs | 10 +- .../Health/HealthCheckServiceTests.cs | 5 +- .../Http/DynamicHttpClientProviderFixture.cs | 14 +- .../Http/DynamicHttpClientProviderTests.cs | 1 - .../Health/HealthCheckService.cs | 35 ++--- .../Http/DynamicHttpClientProvider.cs | 42 +++--- .../Http/IDynamicHttpClientProvider.cs | 6 +- .../DownloadClient/DownloadServiceFactory.cs | 27 ++-- .../Factory/DownloadClientFactory.cs | 19 ++- .../DownloadClient/IDownloadService.cs | 5 +- .../DownloadClient/QBittorrent/QBitService.cs | 19 ++- .../Transmission/TransmissionService.cs | 23 ++- .../DownloadRemover/QueueItemRemover.cs | 15 +- .../Verticals/Jobs/GenericHandler.cs | 142 ++++++++++-------- .../Notifications/INotificationProvider.cs | 2 +- .../Notifications/NotificationProvider.cs | 2 +- 41 files changed, 327 insertions(+), 518 deletions(-) rename code/Common/Configuration/{DownloadClient/ClientConfig.cs => DownloadClient.cs} (78%) delete mode 100644 code/Common/Configuration/DownloadClient/DownloadClientConfig.cs delete mode 100644 code/Common/Configuration/General/DryRunConfig.cs rename code/Common/Configuration/Notification/{BaseNotificationConfig.cs => NotificationConfig.cs} (92%) delete mode 100644 code/Common/Configuration/Notification/NotificationsConfig.cs delete mode 100644 code/Data/Migrations/20250613154308_InitialEvents.Designer.cs delete mode 100644 code/Data/Migrations/20250613154308_InitialEvents.cs delete mode 100644 code/Data/Migrations/EventsContextModelSnapshot.cs diff --git a/code/Common/Configuration/Arr/ArrConfig.cs b/code/Common/Configuration/Arr/ArrConfig.cs index 7aed605c..56626109 100644 --- a/code/Common/Configuration/Arr/ArrConfig.cs +++ b/code/Common/Configuration/Arr/ArrConfig.cs @@ -1,9 +1,14 @@ -using Microsoft.Extensions.Configuration; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Common.Configuration.Arr; public abstract class ArrConfig : IConfig { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; init; } = Guid.NewGuid(); + public bool Enabled { get; init; } public short FailedImportMaxStrikes { get; init; } = -1; diff --git a/code/Common/Configuration/Arr/ArrInstance.cs b/code/Common/Configuration/Arr/ArrInstance.cs index f2c5136c..f0fc236c 100644 --- a/code/Common/Configuration/Arr/ArrInstance.cs +++ b/code/Common/Configuration/Arr/ArrInstance.cs @@ -1,10 +1,13 @@ -using Common.Attributes; -using Common.Configuration; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Common.Attributes; namespace Common.Configuration.Arr; public sealed class ArrInstance { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } = Guid.NewGuid(); public required string Name { get; set; } diff --git a/code/Common/Configuration/DownloadCleaner/CleanCategory.cs b/code/Common/Configuration/DownloadCleaner/CleanCategory.cs index df9f6cf9..f7354aac 100644 --- a/code/Common/Configuration/DownloadCleaner/CleanCategory.cs +++ b/code/Common/Configuration/DownloadCleaner/CleanCategory.cs @@ -1,9 +1,15 @@ -using Common.Exceptions; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using ValidationException = Common.Exceptions.ValidationException; namespace Common.Configuration.DownloadCleaner; public sealed record CleanCategory : IConfig { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; init; } = Guid.NewGuid(); + public required string Name { get; init; } /// diff --git a/code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs b/code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs index f77b14b8..9641edc2 100644 --- a/code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs +++ b/code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs @@ -4,6 +4,8 @@ namespace Common.Configuration.DownloadCleaner; public sealed record DownloadCleanerConfig : IJobConfig { + public Guid Id { get; init; } = Guid.NewGuid(); + public bool Enabled { get; init; } public string CronExpression { get; init; } = "0 0 * * * ?"; @@ -11,7 +13,7 @@ public sealed record DownloadCleanerConfig : IJobConfig /// /// Indicates whether to use the CronExpression directly or convert from a user-friendly schedule /// - public bool UseAdvancedScheduling { get; init; } = false; + public bool UseAdvancedScheduling { get; init; } public List Categories { get; init; } = []; diff --git a/code/Common/Configuration/DownloadClient/ClientConfig.cs b/code/Common/Configuration/DownloadClient.cs similarity index 78% rename from code/Common/Configuration/DownloadClient/ClientConfig.cs rename to code/Common/Configuration/DownloadClient.cs index c2db665d..565863e6 100644 --- a/code/Common/Configuration/DownloadClient/ClientConfig.cs +++ b/code/Common/Configuration/DownloadClient.cs @@ -1,29 +1,31 @@ +using System.ComponentModel.DataAnnotations.Schema; using Common.Attributes; using Common.Enums; using Common.Exceptions; using Newtonsoft.Json; -namespace Common.Configuration.DownloadClient; +namespace Common.Configuration; /// /// Configuration for a specific download client /// -public sealed record ClientConfig +public sealed record DownloadClient { - /// - /// Whether this client is enabled - /// - public bool Enabled { get; init; } = true; - /// /// Unique identifier for this client /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; init; } = Guid.NewGuid(); + /// + /// Whether this client is enabled + /// + public bool Enabled { get; init; } = false; + /// /// Friendly name for this client /// - public string Name { get; init; } = string.Empty; + public required string Name { get; init; } /// /// Type of download client @@ -39,19 +41,19 @@ public sealed record ClientConfig /// Username for authentication /// [SensitiveData] - public string Username { get; init; } = string.Empty; + public string? Username { get; init; } /// /// Password for authentication /// [SensitiveData] - public string Password { get; init; } = string.Empty; + public string? Password { get; init; } /// /// The base URL path component, used by clients like Transmission and Deluge /// [JsonProperty("url_base")] - public string UrlBase { get; init; } = string.Empty; + public string? UrlBase { get; init; } /// /// The computed full URL for the client @@ -63,11 +65,6 @@ public sealed record ClientConfig /// public void Validate() { - if (Id == Guid.Empty) - { - throw new ValidationException("Client ID cannot be empty"); - } - if (string.IsNullOrWhiteSpace(Name)) { throw new ValidationException($"Client name cannot be empty for client ID: {Id}"); diff --git a/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs b/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs deleted file mode 100644 index cdcc898e..00000000 --- a/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Common.Exceptions; - -namespace Common.Configuration.DownloadClient; - -public sealed record DownloadClientConfig : IConfig -{ - /// - /// Collection of download clients configured for the application - /// - public List Clients { get; init; } = new(); - - /// - /// Gets a client configuration by id - /// - /// The client id - /// The client configuration or null if not found - public ClientConfig? GetClientConfig(Guid id) - { - return Clients.FirstOrDefault(c => c.Id == id); - } - - /// - /// Gets all enabled clients - /// - /// Collection of enabled client configurations - public IEnumerable GetEnabledClients() - { - return Clients.Where(c => c.Enabled); - } - - /// - /// Validates the configuration to ensure it meets requirements - /// - public void Validate() - { - // Validate clients have unique IDs - var duplicateNames = Clients - .GroupBy(c => c.Name) - .Where(g => g.Count() > 1) - .Select(g => g.Key) - .ToList(); - - if (duplicateNames.Any()) - { - throw new ValidationException($"Duplicate client names found: {string.Join(", ", duplicateNames)}"); - } - - // Validate each client configuration - foreach (var client in Clients) - { - client.Validate(); - } - } -} \ No newline at end of file diff --git a/code/Common/Configuration/General/DryRunConfig.cs b/code/Common/Configuration/General/DryRunConfig.cs deleted file mode 100644 index 22b9c419..00000000 --- a/code/Common/Configuration/General/DryRunConfig.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace Common.Configuration.General; - -public sealed record DryRunConfig -{ - [ConfigurationKeyName("DRY_RUN")] - public bool IsDryRun { get; init; } -} \ No newline at end of file diff --git a/code/Common/Configuration/General/GeneralConfig.cs b/code/Common/Configuration/General/GeneralConfig.cs index 8c6964eb..f2f14181 100644 --- a/code/Common/Configuration/General/GeneralConfig.cs +++ b/code/Common/Configuration/General/GeneralConfig.cs @@ -1,26 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using Common.Enums; -using Common.Exceptions; using Serilog.Events; +using ValidationException = Common.Exceptions.ValidationException; namespace Common.Configuration.General; public sealed record GeneralConfig : IConfig { - public bool DryRun { get; init; } + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } = Guid.NewGuid(); - public ushort HttpMaxRetries { get; init; } + public bool DryRun { get; set; } - public ushort HttpTimeout { get; init; } = 100; + public ushort HttpMaxRetries { get; set; } - public CertificateValidationType HttpCertificateValidation { get; init; } = CertificateValidationType.Enabled; + public ushort HttpTimeout { get; set; } = 100; + + public CertificateValidationType HttpCertificateValidation { get; set; } = CertificateValidationType.Enabled; - public bool SearchEnabled { get; init; } = true; + public bool SearchEnabled { get; set; } = true; - public ushort SearchDelay { get; init; } = 30; + public ushort SearchDelay { get; set; } = 30; public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; - public string EncryptionKey { get; init; } = Guid.NewGuid().ToString(); + public string EncryptionKey { get; set; } = Guid.NewGuid().ToString(); public List IgnoredDownloads { get; set; } = []; diff --git a/code/Common/Configuration/Notification/AppriseConfig.cs b/code/Common/Configuration/Notification/AppriseConfig.cs index 6b26eff0..bdbfeaea 100644 --- a/code/Common/Configuration/Notification/AppriseConfig.cs +++ b/code/Common/Configuration/Notification/AppriseConfig.cs @@ -1,6 +1,6 @@ namespace Common.Configuration.Notification; -public sealed record AppriseConfig : BaseNotificationConfig +public sealed record AppriseConfig : NotificationConfig { public Uri? Url { get; init; } diff --git a/code/Common/Configuration/Notification/NotifiarrConfig.cs b/code/Common/Configuration/Notification/NotifiarrConfig.cs index 72b6633f..b52b6cbd 100644 --- a/code/Common/Configuration/Notification/NotifiarrConfig.cs +++ b/code/Common/Configuration/Notification/NotifiarrConfig.cs @@ -1,15 +1,9 @@ -using Microsoft.Extensions.Configuration; - namespace Common.Configuration.Notification; -public sealed record NotifiarrConfig : BaseNotificationConfig +public sealed record NotifiarrConfig : NotificationConfig { - public const string SectionName = "Notifiarr"; - - [ConfigurationKeyName("API_KEY")] public string? ApiKey { get; init; } - [ConfigurationKeyName("CHANNEL_ID")] public string? ChannelId { get; init; } public override bool IsValid() diff --git a/code/Common/Configuration/Notification/BaseNotificationConfig.cs b/code/Common/Configuration/Notification/NotificationConfig.cs similarity index 92% rename from code/Common/Configuration/Notification/BaseNotificationConfig.cs rename to code/Common/Configuration/Notification/NotificationConfig.cs index 401386ca..781dda0c 100644 --- a/code/Common/Configuration/Notification/BaseNotificationConfig.cs +++ b/code/Common/Configuration/Notification/NotificationConfig.cs @@ -1,6 +1,6 @@ namespace Common.Configuration.Notification; -public abstract record BaseNotificationConfig +public abstract record NotificationConfig { public bool OnFailedImportStrike { get; init; } diff --git a/code/Common/Configuration/Notification/NotificationsConfig.cs b/code/Common/Configuration/Notification/NotificationsConfig.cs deleted file mode 100644 index 98556730..00000000 --- a/code/Common/Configuration/Notification/NotificationsConfig.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Common.Configuration.Notification; - -public sealed record NotificationsConfig -{ - public NotifiarrConfig Notifiarr { get; init; } = new(); - - public AppriseConfig Apprise { get; init; } = new(); -} \ No newline at end of file diff --git a/code/Common/Configuration/QueueCleaner/BlocklistSettings.cs b/code/Common/Configuration/QueueCleaner/BlocklistSettings.cs index 536da6b9..db1e313a 100644 --- a/code/Common/Configuration/QueueCleaner/BlocklistSettings.cs +++ b/code/Common/Configuration/QueueCleaner/BlocklistSettings.cs @@ -1,8 +1,11 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace Common.Configuration.QueueCleaner; /// /// Settings for a blocklist /// +[ComplexType] public sealed record BlocklistSettings { public BlocklistType BlocklistType { get; init; } diff --git a/code/Common/Configuration/QueueCleaner/ContentBlockerConfig.cs b/code/Common/Configuration/QueueCleaner/ContentBlockerConfig.cs index 6571efb0..6275af59 100644 --- a/code/Common/Configuration/QueueCleaner/ContentBlockerConfig.cs +++ b/code/Common/Configuration/QueueCleaner/ContentBlockerConfig.cs @@ -1,5 +1,8 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace Common.Configuration.QueueCleaner; +[ComplexType] public sealed record ContentBlockerConfig { public bool Enabled { get; init; } diff --git a/code/Common/Configuration/QueueCleaner/FailedImportConfig.cs b/code/Common/Configuration/QueueCleaner/FailedImportConfig.cs index e80fa7aa..6945b956 100644 --- a/code/Common/Configuration/QueueCleaner/FailedImportConfig.cs +++ b/code/Common/Configuration/QueueCleaner/FailedImportConfig.cs @@ -1,7 +1,9 @@ -using Common.Exceptions; +using System.ComponentModel.DataAnnotations.Schema; +using Common.Exceptions; namespace Common.Configuration.QueueCleaner; +[ComplexType] public sealed record FailedImportConfig { public ushort MaxStrikes { get; init; } diff --git a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs b/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs index 4245f313..33da03a7 100644 --- a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs +++ b/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs @@ -1,7 +1,11 @@ +using System.ComponentModel.DataAnnotations; + namespace Common.Configuration.QueueCleaner; public sealed record QueueCleanerConfig : IJobConfig { + public Guid Id { get; init; } = Guid.NewGuid(); + public bool Enabled { get; init; } public string CronExpression { get; init; } = "0 0/5 * * * ?"; diff --git a/code/Common/Configuration/QueueCleaner/SlowConfig.cs b/code/Common/Configuration/QueueCleaner/SlowConfig.cs index 5d4f8f2c..2a80b2fa 100644 --- a/code/Common/Configuration/QueueCleaner/SlowConfig.cs +++ b/code/Common/Configuration/QueueCleaner/SlowConfig.cs @@ -1,9 +1,11 @@ -using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; using Common.CustomDataTypes; using Common.Exceptions; namespace Common.Configuration.QueueCleaner; +[ComplexType] public sealed record SlowConfig { public ushort MaxStrikes { get; init; } diff --git a/code/Common/Configuration/QueueCleaner/StalledConfig.cs b/code/Common/Configuration/QueueCleaner/StalledConfig.cs index d418a113..ae2ce862 100644 --- a/code/Common/Configuration/QueueCleaner/StalledConfig.cs +++ b/code/Common/Configuration/QueueCleaner/StalledConfig.cs @@ -1,7 +1,9 @@ -using Common.Exceptions; +using System.ComponentModel.DataAnnotations.Schema; +using Common.Exceptions; namespace Common.Configuration.QueueCleaner; +[ComplexType] public sealed record StalledConfig { public ushort MaxStrikes { get; init; } diff --git a/code/Data/DataContext.cs b/code/Data/DataContext.cs index b386abb6..02926e95 100644 --- a/code/Data/DataContext.cs +++ b/code/Data/DataContext.cs @@ -1,4 +1,9 @@ -using Common.Configuration.QueueCleaner; +using Common.Configuration; +using Common.Configuration.Arr; +using Common.Configuration.DownloadCleaner; +using Common.Configuration.General; +using Common.Configuration.Notification; +using Common.Configuration.QueueCleaner; using Common.Helpers; using Microsoft.EntityFrameworkCore; @@ -9,8 +14,24 @@ namespace Data; /// public class DataContext : DbContext { + public DbSet GeneralConfigs { get; set; } + + public DbSet DownloadClients { get; set; } + public DbSet QueueCleanerConfigs { get; set; } + public DbSet DownloadCleanerConfigs { get; set; } + + public DbSet SonarrConfigs { get; set; } + + public DbSet RadarrConfigs { get; set; } + + public DbSet LidarrConfigs { get; set; } + + public DbSet AppriseConfigs { get; set; } + + public DbSet NotifiarrConfigs { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (optionsBuilder.IsConfigured) @@ -27,5 +48,37 @@ public class DataContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity(entity => + { + entity.ComplexProperty(e => e.FailedImport); + entity.ComplexProperty(e => e.Stalled); + entity.ComplexProperty(e => e.Slow); + entity.ComplexProperty(e => e.ContentBlocker); + }); + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var enumProperties = entityType.ClrType.GetProperties() + .Where(p => p.PropertyType.IsEnum || + (p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) && + p.PropertyType.GetGenericArguments()[0].IsEnum)); + + foreach (var property in enumProperties) + { + modelBuilder.Entity(entityType.ClrType) + .Property(property.Name) + .HasConversion(); + } + } + + modelBuilder.Entity().HasData(new QueueCleanerConfig()); + modelBuilder.Entity().HasData(new DownloadCleanerConfig()); + modelBuilder.Entity().HasData(new GeneralConfig()); + modelBuilder.Entity().HasData(new SonarrConfig()); + modelBuilder.Entity().HasData(new RadarrConfig()); + modelBuilder.Entity().HasData(new LidarrConfig()); + modelBuilder.Entity().HasData(new AppriseConfig()); + modelBuilder.Entity().HasData(new NotifiarrConfig()); } } \ No newline at end of file diff --git a/code/Data/EventsContext.cs b/code/Data/EventsContext.cs index b3faaad7..2ff18cf6 100644 --- a/code/Data/EventsContext.cs +++ b/code/Data/EventsContext.cs @@ -33,6 +33,22 @@ public class EventsContext : DbContext entity.Property(e => e.Timestamp) .HasConversion(new UtcDateTimeConverter()); }); + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var enumProperties = entityType.ClrType.GetProperties() + .Where(p => p.PropertyType.IsEnum || + (p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) && + p.PropertyType.GetGenericArguments()[0].IsEnum)); + + foreach (var property in enumProperties) + { + modelBuilder.Entity(entityType.ClrType) + .Property(property.Name) + .HasConversion(); + } + } } public class UtcDateTimeConverter : ValueConverter diff --git a/code/Data/Migrations/20250613154308_InitialEvents.Designer.cs b/code/Data/Migrations/20250613154308_InitialEvents.Designer.cs deleted file mode 100644 index 362f6f3e..00000000 --- a/code/Data/Migrations/20250613154308_InitialEvents.Designer.cs +++ /dev/null @@ -1,77 +0,0 @@ -// -using System; -using Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Data.Migrations -{ - [DbContext(typeof(EventsContext))] - [Migration("20250613154308_InitialEvents")] - partial class InitialEvents - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.5"); - - modelBuilder.Entity("Data.Models.Events.AppEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasColumnName("id"); - - b.Property("Data") - .HasColumnType("TEXT") - .HasColumnName("data"); - - b.Property("EventType") - .HasColumnType("INTEGER") - .HasColumnName("event_type"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("TEXT") - .HasColumnName("message"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); - - b.Property("Timestamp") - .HasColumnType("TEXT") - .HasColumnName("timestamp"); - - b.Property("TrackingId") - .HasColumnType("TEXT") - .HasColumnName("tracking_id"); - - b.HasKey("Id") - .HasName("pk_events"); - - b.HasIndex("EventType") - .HasDatabaseName("ix_events_event_type"); - - b.HasIndex("Message") - .HasDatabaseName("ix_events_message"); - - b.HasIndex("Severity") - .HasDatabaseName("ix_events_severity"); - - b.HasIndex("Timestamp") - .IsDescending() - .HasDatabaseName("ix_events_timestamp"); - - b.ToTable("events", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/Data/Migrations/20250613154308_InitialEvents.cs b/code/Data/Migrations/20250613154308_InitialEvents.cs deleted file mode 100644 index 61454def..00000000 --- a/code/Data/Migrations/20250613154308_InitialEvents.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Data.Migrations -{ - /// - public partial class InitialEvents : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "events", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - timestamp = table.Column(type: "TEXT", nullable: false), - event_type = table.Column(type: "INTEGER", nullable: false), - message = table.Column(type: "TEXT", maxLength: 1000, nullable: false), - data = table.Column(type: "TEXT", nullable: true), - severity = table.Column(type: "INTEGER", nullable: false), - tracking_id = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_events", x => x.id); - }); - - migrationBuilder.CreateIndex( - name: "ix_events_event_type", - table: "events", - column: "event_type"); - - migrationBuilder.CreateIndex( - name: "ix_events_message", - table: "events", - column: "message"); - - migrationBuilder.CreateIndex( - name: "ix_events_severity", - table: "events", - column: "severity"); - - migrationBuilder.CreateIndex( - name: "ix_events_timestamp", - table: "events", - column: "timestamp", - descending: new bool[0]); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "events"); - } - } -} diff --git a/code/Data/Migrations/EventsContextModelSnapshot.cs b/code/Data/Migrations/EventsContextModelSnapshot.cs deleted file mode 100644 index ca7f1ad0..00000000 --- a/code/Data/Migrations/EventsContextModelSnapshot.cs +++ /dev/null @@ -1,74 +0,0 @@ -// -using System; -using Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Data.Migrations -{ - [DbContext(typeof(EventsContext))] - partial class EventsContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.5"); - - modelBuilder.Entity("Data.Models.Events.AppEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasColumnName("id"); - - b.Property("Data") - .HasColumnType("TEXT") - .HasColumnName("data"); - - b.Property("EventType") - .HasColumnType("INTEGER") - .HasColumnName("event_type"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("TEXT") - .HasColumnName("message"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); - - b.Property("Timestamp") - .HasColumnType("TEXT") - .HasColumnName("timestamp"); - - b.Property("TrackingId") - .HasColumnType("TEXT") - .HasColumnName("tracking_id"); - - b.HasKey("Id") - .HasName("pk_events"); - - b.HasIndex("EventType") - .HasDatabaseName("ix_events_event_type"); - - b.HasIndex("Message") - .HasDatabaseName("ix_events_message"); - - b.HasIndex("Severity") - .HasDatabaseName("ix_events_severity"); - - b.HasIndex("Timestamp") - .IsDescending() - .HasDatabaseName("ix_events_timestamp"); - - b.ToTable("events", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/Executable/Controllers/ConfigurationController.cs b/code/Executable/Controllers/ConfigurationController.cs index 3f92f5cf..bb6f71c5 100644 --- a/code/Executable/Controllers/ConfigurationController.cs +++ b/code/Executable/Controllers/ConfigurationController.cs @@ -1,7 +1,6 @@ using Common.Configuration; using Common.Configuration.Arr; using Common.Configuration.DownloadCleaner; -using Common.Configuration.DownloadClient; using Common.Configuration.General; using Common.Configuration.Notification; using Common.Configuration.QueueCleaner; @@ -53,7 +52,7 @@ public class ConfigurationController : ControllerBase [HttpGet("download_client")] public async Task GetDownloadClientConfig() { - var config = await _configManager.GetConfigurationAsync(); + var config = await _configManager.GetConfigurationAsync(); return Ok(config); } @@ -195,13 +194,13 @@ public class ConfigurationController : ControllerBase } [HttpPut("download_client")] - public async Task UpdateDownloadClientConfig(DownloadClientConfig newConfig) + public async Task UpdateDownloadClientConfig(DownloadClientConfigs newConfigs) { // Validate the configuration - newConfig.Validate(); + newConfigs.Validate(); // Persist the configuration - var result = await _configManager.SaveConfigurationAsync(newConfig); + var result = await _configManager.SaveConfigurationAsync(newConfigs); if (!result) { return StatusCode(500, "Failed to save DownloadClient configuration"); diff --git a/code/Executable/Controllers/StatusController.cs b/code/Executable/Controllers/StatusController.cs index c4241fb8..ae4f564a 100644 --- a/code/Executable/Controllers/StatusController.cs +++ b/code/Executable/Controllers/StatusController.cs @@ -1,5 +1,4 @@ using Common.Configuration.Arr; -using Common.Configuration.DownloadClient; using Infrastructure.Configuration; using Infrastructure.Verticals.Arr; using Infrastructure.Verticals.DownloadClient; @@ -38,7 +37,7 @@ public class StatusController : ControllerBase var process = Process.GetCurrentProcess(); // Get configuration - var downloadClientConfig = await _configManager.GetConfigurationAsync(); + var downloadClientConfig = await _configManager.GetConfigurationAsync(); var sonarrConfig = await _configManager.GetConfigurationAsync(); var radarrConfig = await _configManager.GetConfigurationAsync(); var lidarrConfig = await _configManager.GetConfigurationAsync(); @@ -91,7 +90,7 @@ public class StatusController : ControllerBase { try { - var downloadClientConfig = await _configManager.GetConfigurationAsync(); + var downloadClientConfig = await _configManager.GetConfigurationAsync(); var result = new Dictionary(); // Check for configured clients diff --git a/code/Infrastructure.Tests/Health/HealthCheckServiceFixture.cs b/code/Infrastructure.Tests/Health/HealthCheckServiceFixture.cs index 1c10069f..eab4dbea 100644 --- a/code/Infrastructure.Tests/Health/HealthCheckServiceFixture.cs +++ b/code/Infrastructure.Tests/Health/HealthCheckServiceFixture.cs @@ -1,4 +1,4 @@ -using Common.Configuration.DownloadClient; +using Common.Configuration; using Common.Enums; using Infrastructure.Configuration; using Infrastructure.Health; @@ -16,7 +16,7 @@ public class HealthCheckServiceFixture : IDisposable public IConfigManager ConfigManager { get; } public IDownloadClientFactory ClientFactory { get; } public IDownloadService MockClient { get; } - public DownloadClientConfig DownloadClientConfig { get; } + public DownloadClientConfigs DownloadClientConfigs { get; } public HealthCheckServiceFixture() { @@ -27,9 +27,9 @@ public class HealthCheckServiceFixture : IDisposable Guid clientId = Guid.NewGuid(); // Set up test download client config - DownloadClientConfig = new DownloadClientConfig + DownloadClientConfigs = new DownloadClientConfigs { - Clients = new List + Clients = new List { new() { @@ -64,7 +64,7 @@ public class HealthCheckServiceFixture : IDisposable MockClient.GetClientId().Returns(clientId); // Set up mock config manager - ConfigManager.GetConfiguration().Returns(DownloadClientConfig); + ConfigManager.GetConfiguration().Returns(DownloadClientConfigs); } public HealthCheckService CreateSut() diff --git a/code/Infrastructure.Tests/Health/HealthCheckServiceTests.cs b/code/Infrastructure.Tests/Health/HealthCheckServiceTests.cs index e79b14c3..827ef227 100644 --- a/code/Infrastructure.Tests/Health/HealthCheckServiceTests.cs +++ b/code/Infrastructure.Tests/Health/HealthCheckServiceTests.cs @@ -1,4 +1,3 @@ -using Common.Configuration.DownloadClient; using Infrastructure.Health; using NSubstitute; using Shouldly; @@ -59,8 +58,8 @@ public class HealthCheckServiceTests : IClassFixture var sut = _fixture.CreateSut(); // Configure the ConfigManager to return null for the client config - _fixture.ConfigManager.GetConfigurationAsync().Returns( - Task.FromResult(new()) + _fixture.ConfigManager.GetConfigurationAsync().Returns( + Task.FromResult(new()) ); // Act diff --git a/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs b/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs index 43ffbc1f..9d21e8e4 100644 --- a/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs +++ b/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs @@ -1,4 +1,4 @@ -using Common.Configuration.DownloadClient; +using Common.Configuration; using Common.Enums; using Infrastructure.Configuration; using Infrastructure.Http; @@ -30,9 +30,9 @@ public class DynamicHttpClientProviderFixture : IDisposable certificateValidationService); } - public ClientConfig CreateQBitClientConfig() + public DownloadClient CreateQBitClientConfig() { - return new ClientConfig + return new DownloadClient { Id = Guid.NewGuid(), Name = "QBit Test", @@ -44,9 +44,9 @@ public class DynamicHttpClientProviderFixture : IDisposable }; } - public ClientConfig CreateTransmissionClientConfig() + public DownloadClient CreateTransmissionClientConfig() { - return new ClientConfig + return new DownloadClient { Id = Guid.NewGuid(), Name = "Transmission Test", @@ -59,9 +59,9 @@ public class DynamicHttpClientProviderFixture : IDisposable }; } - public ClientConfig CreateDelugeClientConfig() + public DownloadClient CreateDelugeClientConfig() { - return new ClientConfig + return new DownloadClient { Id = Guid.NewGuid(), Name = "Deluge Test", diff --git a/code/Infrastructure.Tests/Http/DynamicHttpClientProviderTests.cs b/code/Infrastructure.Tests/Http/DynamicHttpClientProviderTests.cs index 497404cf..be3df3de 100644 --- a/code/Infrastructure.Tests/Http/DynamicHttpClientProviderTests.cs +++ b/code/Infrastructure.Tests/Http/DynamicHttpClientProviderTests.cs @@ -1,5 +1,4 @@ using System.Net; -using Common.Configuration.DownloadClient; using Common.Enums; using Infrastructure.Http; using Shouldly; diff --git a/code/Infrastructure/Health/HealthCheckService.cs b/code/Infrastructure/Health/HealthCheckService.cs index 433c7c94..3d073014 100644 --- a/code/Infrastructure/Health/HealthCheckService.cs +++ b/code/Infrastructure/Health/HealthCheckService.cs @@ -1,7 +1,6 @@ -using Common.Configuration.DownloadClient; -using Infrastructure.Configuration; -using Infrastructure.Verticals.DownloadClient; +using Data; using Infrastructure.Verticals.DownloadClient.Factory; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Infrastructure.Health; @@ -12,7 +11,7 @@ namespace Infrastructure.Health; public class HealthCheckService : IHealthCheckService { private readonly ILogger _logger; - private readonly IConfigManager _configManager; + private readonly DataContext _dataContext; private readonly IDownloadClientFactory _clientFactory; private readonly Dictionary _healthStatuses = new(); private readonly object _lockObject = new(); @@ -22,19 +21,13 @@ public class HealthCheckService : IHealthCheckService /// public event EventHandler? ClientHealthChanged; - /// - /// Initializes a new instance of the class - /// - /// The logger - /// The configuration manager - /// The download client factory public HealthCheckService( ILogger logger, - IConfigManager configManager, + DataContext dataContext, IDownloadClientFactory clientFactory) { _logger = logger; - _configManager = configManager; + _dataContext = dataContext; _clientFactory = clientFactory; } @@ -46,8 +39,11 @@ public class HealthCheckService : IHealthCheckService try { // Get the client configuration - var config = await GetClientConfigAsync(clientId); - if (config == null) + var config = await _dataContext.DownloadClients + .Where(x => x.Id == clientId) + .FirstOrDefaultAsync(); + + if (config is null) { _logger.LogWarning("Client {clientId} not found in configuration", clientId); var notFoundStatus = new HealthStatus @@ -135,8 +131,9 @@ public class HealthCheckService : IHealthCheckService try { // Get all enabled client configurations - var config = await _configManager.GetConfigurationAsync(); - var enabledClients = config.GetEnabledClients(); + var enabledClients = await _dataContext.DownloadClients + .Where(x => x.Enabled) + .ToListAsync(); var results = new Dictionary(); // Check health of each enabled client @@ -173,12 +170,6 @@ public class HealthCheckService : IHealthCheckService } } - private async Task GetClientConfigAsync(Guid clientId) - { - var config = await _configManager.GetConfigurationAsync(); - return config.GetClientConfig(clientId); - } - private void UpdateHealthStatus(HealthStatus newStatus) { HealthStatus? previousStatus = null; diff --git a/code/Infrastructure/Http/DynamicHttpClientProvider.cs b/code/Infrastructure/Http/DynamicHttpClientProvider.cs index ea1eef43..e39b5789 100644 --- a/code/Infrastructure/Http/DynamicHttpClientProvider.cs +++ b/code/Infrastructure/Http/DynamicHttpClientProvider.cs @@ -1,7 +1,7 @@ using System.Net; -using Common.Configuration.DownloadClient; +using Common.Configuration; using Common.Configuration.General; -using Infrastructure.Configuration; +using Data; using Infrastructure.Services; using Microsoft.Extensions.Logging; using Polly; @@ -15,62 +15,62 @@ namespace Infrastructure.Http; public class DynamicHttpClientProvider : IDynamicHttpClientProvider { private readonly ILogger _logger; + private readonly DataContext _dataContext; private readonly IHttpClientFactory _httpClientFactory; - private readonly IConfigManager _configManager; private readonly CertificateValidationService _certificateValidationService; public DynamicHttpClientProvider( ILogger logger, + DataContext dataContext, IHttpClientFactory httpClientFactory, - IConfigManager configManager, CertificateValidationService certificateValidationService) { _logger = logger; + _dataContext = dataContext; _httpClientFactory = httpClientFactory; - _configManager = configManager; _certificateValidationService = certificateValidationService; } /// - public HttpClient CreateClient(ClientConfig clientConfig) + public HttpClient CreateClient(DownloadClient downloadClient) { - if (clientConfig == null) + if (downloadClient == null) { - throw new ArgumentNullException(nameof(clientConfig)); + throw new ArgumentNullException(nameof(downloadClient)); } // Try to use named client if it exists try { - string clientName = GetClientName(clientConfig); + string clientName = GetClientName(downloadClient); return _httpClientFactory.CreateClient(clientName); } catch (InvalidOperationException) { - _logger.LogWarning("Named HTTP client for {clientId} not found, creating generic client", clientConfig.Id); - return CreateGenericClient(clientConfig); + _logger.LogWarning("Named HTTP client for {clientId} not found, creating generic client", downloadClient.Id); + return CreateGenericClient(downloadClient); } } /// /// Gets the client name for a specific client configuration /// - /// The client configuration + /// The client configuration /// The client name for use with IHttpClientFactory - private string GetClientName(ClientConfig clientConfig) + private string GetClientName(DownloadClient downloadClient) { - return $"DownloadClient_{clientConfig.Id}"; + return $"DownloadClient_{downloadClient.Id}"; } /// /// Creates a generic HTTP client with appropriate configuration /// - /// The client configuration + /// The client configuration /// A configured HttpClient instance - private HttpClient CreateGenericClient(ClientConfig clientConfig) + private HttpClient CreateGenericClient(DownloadClient downloadClient) { // TODO - var httpConfig = _configManager.GetConfiguration(); + var httpConfig = _dataContext.GeneralConfigs.First(); // Create handler with certificate validation var handler = new HttpClientHandler @@ -86,7 +86,7 @@ public class DynamicHttpClientProvider : IDynamicHttpClientProvider UseDefaultCredentials = false }; - if (clientConfig.Type == Common.Enums.DownloadClientType.Deluge) + if (downloadClient.Type == Common.Enums.DownloadClientType.Deluge) { handler.AllowAutoRedirect = true; handler.UseCookies = true; @@ -101,13 +101,13 @@ public class DynamicHttpClientProvider : IDynamicHttpClientProvider }; // Set base address if needed - if (clientConfig.Url != null) + if (downloadClient.Url != null) { - client.BaseAddress = clientConfig.Url; + client.BaseAddress = downloadClient.Url; } _logger.LogDebug("Created generic HTTP client for client {clientId} with base address {baseAddress}", - clientConfig.Id, client.BaseAddress); + downloadClient.Id, client.BaseAddress); return client; } diff --git a/code/Infrastructure/Http/IDynamicHttpClientProvider.cs b/code/Infrastructure/Http/IDynamicHttpClientProvider.cs index ec410a38..20a6d6f0 100644 --- a/code/Infrastructure/Http/IDynamicHttpClientProvider.cs +++ b/code/Infrastructure/Http/IDynamicHttpClientProvider.cs @@ -1,4 +1,4 @@ -using Common.Configuration.DownloadClient; +using Common.Configuration; namespace Infrastructure.Http; @@ -10,7 +10,7 @@ public interface IDynamicHttpClientProvider /// /// Creates an HTTP client configured for the specified download client /// - /// The client configuration + /// The client configuration /// A configured HttpClient instance - HttpClient CreateClient(ClientConfig clientConfig); + HttpClient CreateClient(DownloadClient downloadClient); } diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs index 34710f75..0414b7c0 100644 --- a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs +++ b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs @@ -1,6 +1,4 @@ -using Common.Configuration.DownloadClient; using Common.Enums; -using Infrastructure.Configuration; using Infrastructure.Verticals.DownloadClient.Deluge; using Infrastructure.Verticals.DownloadClient.QBittorrent; using Infrastructure.Verticals.DownloadClient.Transmission; @@ -15,16 +13,13 @@ namespace Infrastructure.Verticals.DownloadClient; public sealed class DownloadServiceFactory { private readonly IServiceProvider _serviceProvider; - private readonly IConfigManager _configManager; private readonly ILogger _logger; public DownloadServiceFactory( IServiceProvider serviceProvider, - IConfigManager configManager, ILogger logger) { _serviceProvider = serviceProvider; - _configManager = configManager; _logger = logger; } @@ -56,21 +51,21 @@ public sealed class DownloadServiceFactory /// /// Creates a download service using the specified client configuration /// - /// The client configuration to use + /// The client configuration to use /// An implementation of IDownloadService or null if the client is not available - public IDownloadService? GetDownloadService(ClientConfig clientConfig) + public IDownloadService? GetDownloadService(Common.Configuration.DownloadClient downloadClient) { - if (!clientConfig.Enabled) + if (!downloadClient.Enabled) { - _logger.LogWarning("Download client {clientId} is disabled", clientConfig.Id); + _logger.LogWarning("Download client {clientId} is disabled", downloadClient.Id); return null; } - return clientConfig.Type switch + return downloadClient.Type switch { - DownloadClientType.QBittorrent => CreateClientService(clientConfig), - DownloadClientType.Deluge => CreateClientService(clientConfig), - DownloadClientType.Transmission => CreateClientService(clientConfig), + DownloadClientType.QBittorrent => CreateClientService(downloadClient), + DownloadClientType.Deluge => CreateClientService(downloadClient), + DownloadClientType.Transmission => CreateClientService(downloadClient), _ => null }; } @@ -79,12 +74,12 @@ public sealed class DownloadServiceFactory /// Creates a download client service for a specific client type /// /// The type of download service to create - /// The client configuration + /// The client configuration /// An implementation of IDownloadService - private T CreateClientService(ClientConfig clientConfig) where T : IDownloadService + private T CreateClientService(Common.Configuration.DownloadClient downloadClient) where T : IDownloadService { var service = _serviceProvider.GetRequiredService(); - service.Initialize(clientConfig); + service.Initialize(downloadClient); return service; } } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs b/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs index fa989e97..c90f42f4 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Factory/DownloadClientFactory.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using Common.Configuration.DownloadClient; using Common.Enums; using Infrastructure.Configuration; using Infrastructure.Http; @@ -51,7 +50,7 @@ public class DownloadClientFactory : IDownloadClientFactory /// public IEnumerable GetAllEnabledClients() { - var downloadClientConfig = _configManager.GetConfiguration(); + var downloadClientConfig = _configManager.GetConfiguration(); foreach (var client in downloadClientConfig.GetEnabledClients()) { @@ -62,7 +61,7 @@ public class DownloadClientFactory : IDownloadClientFactory /// public IEnumerable GetClientsByType(DownloadClientType clientType) { - var downloadClientConfig = _configManager.GetConfiguration(); + var downloadClientConfig = _configManager.GetConfiguration(); foreach (var client in downloadClientConfig.GetEnabledClients().Where(c => c.Type == clientType)) { @@ -100,7 +99,7 @@ public class DownloadClientFactory : IDownloadClientFactory private IDownloadService CreateClient(Guid clientId) { - var downloadClientConfig = _configManager.GetConfiguration(); + var downloadClientConfig = _configManager.GetConfiguration(); var clientConfig = downloadClientConfig.GetClientConfig(clientId); @@ -126,24 +125,24 @@ public class DownloadClientFactory : IDownloadClientFactory return service; } - private QBitService CreateQBitService(ClientConfig clientConfig) + private QBitService CreateQBitService(Common.Configuration.DownloadClient downloadClient) { var client = _serviceProvider.GetRequiredService(); - client.Initialize(clientConfig); + client.Initialize(downloadClient); return client; } - private TransmissionService CreateTransmissionService(ClientConfig clientConfig) + private TransmissionService CreateTransmissionService(Common.Configuration.DownloadClient downloadClient) { var client = _serviceProvider.GetRequiredService(); - client.Initialize(clientConfig); + client.Initialize(downloadClient); return client; } - private DelugeService CreateDelugeService(ClientConfig clientConfig) + private DelugeService CreateDelugeService(Common.Configuration.DownloadClient downloadClient) { var client = _serviceProvider.GetRequiredService(); - client.Initialize(clientConfig); + client.Initialize(downloadClient); return client; } } diff --git a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs index 0b00d014..6f5eb20b 100644 --- a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using System.Text.RegularExpressions; using Common.Configuration.DownloadCleaner; -using Common.Configuration.DownloadClient; using Common.Configuration.QueueCleaner; using Data.Enums; using Infrastructure.Interceptors; @@ -20,8 +19,8 @@ public interface IDownloadService : IDisposable /// /// Initializes the download service with client-specific configuration /// - /// The client configuration - public void Initialize(ClientConfig clientConfig); + /// The client configuration + public void Initialize(Common.Configuration.DownloadClient downloadClient); public Task LoginAsync(); diff --git a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs index 772dc563..c83d1ea7 100644 --- a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Text.RegularExpressions; using Common.Attributes; using Common.Configuration.DownloadCleaner; -using Common.Configuration.DownloadClient; using Common.Configuration.QueueCleaner; using Common.CustomDataTypes; using Common.Helpers; @@ -48,16 +47,16 @@ public partial class QBitService : DownloadService, IQBitService } /// - public override void Initialize(ClientConfig clientConfig) + public override void Initialize(Common.Configuration.DownloadClient downloadClient) { // Initialize base service first - base.Initialize(clientConfig); + base.Initialize(downloadClient); // Create QBittorrent client - _client = new QBittorrentClient(_httpClient, clientConfig.Url); + _client = new QBittorrentClient(_httpClient, downloadClient.Url); _logger.LogInformation("Initialized QBittorrent service for client {clientName} ({clientId})", - clientConfig.Name, clientConfig.Id); + downloadClient.Name, downloadClient.Id); } public override async Task LoginAsync() @@ -67,20 +66,20 @@ public partial class QBitService : DownloadService, IQBitService throw new InvalidOperationException("QBittorrent client is not initialized"); } - if (string.IsNullOrEmpty(_clientConfig.Username) && string.IsNullOrEmpty(_clientConfig.Password)) + if (string.IsNullOrEmpty(_downloadClient.Username) && string.IsNullOrEmpty(_downloadClient.Password)) { - _logger.LogDebug("No credentials configured for client {clientId}, skipping login", _clientConfig.Id); + _logger.LogDebug("No credentials configured for client {clientId}, skipping login", _downloadClient.Id); return; } try { - await _client.LoginAsync(_clientConfig.Username, _clientConfig.Password); - _logger.LogDebug("Successfully logged in to QBittorrent client {clientId}", _clientConfig.Id); + await _client.LoginAsync(_downloadClient.Username, _downloadClient.Password); + _logger.LogDebug("Successfully logged in to QBittorrent client {clientId}", _downloadClient.Id); } catch (Exception ex) { - _logger.LogError(ex, "Failed to login to QBittorrent client {clientId}", _clientConfig.Id); + _logger.LogError(ex, "Failed to login to QBittorrent client {clientId}", _downloadClient.Id); throw; } } diff --git a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs index 2474f9f2..a22fd41d 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Text.RegularExpressions; using Common.Attributes; using Common.Configuration.DownloadCleaner; -using Common.Configuration.DownloadClient; using Common.Configuration.QueueCleaner; using Common.CustomDataTypes; using Common.Helpers; @@ -69,15 +68,15 @@ public partial class TransmissionService : DownloadService, ITransmissionService } /// - public override void Initialize(ClientConfig clientConfig) + public override void Initialize(Common.Configuration.DownloadClient downloadClient) { // Initialize base service first - base.Initialize(clientConfig); + base.Initialize(downloadClient); // Ensure client type is correct - if (clientConfig.Type != Common.Enums.DownloadClientType.Transmission) + if (downloadClient.Type != Common.Enums.DownloadClientType.Transmission) { - throw new InvalidOperationException($"Cannot initialize TransmissionService with client type {clientConfig.Type}"); + throw new InvalidOperationException($"Cannot initialize TransmissionService with client type {downloadClient.Type}"); } if (_httpClient == null) @@ -86,18 +85,18 @@ public partial class TransmissionService : DownloadService, ITransmissionService } // Create the RPC path - string rpcPath = string.IsNullOrEmpty(clientConfig.UrlBase) + string rpcPath = string.IsNullOrEmpty(downloadClient.UrlBase) ? "/rpc" - : $"/{clientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc"; + : $"/{downloadClient.UrlBase.TrimStart('/').TrimEnd('/')}/rpc"; // Create full RPC URL - string rpcUrl = new UriBuilder(clientConfig.Url) { Path = rpcPath }.Uri.ToString(); + string rpcUrl = new UriBuilder(downloadClient.Url) { Path = rpcPath }.Uri.ToString(); // Create Transmission client - _client = new Client(_httpClient, rpcUrl, login: clientConfig.Username, password: clientConfig.Password); + _client = new Client(_httpClient, rpcUrl, login: downloadClient.Username, password: downloadClient.Password); _logger.LogInformation("Initialized Transmission service for client {clientName} ({clientId})", - clientConfig.Name, clientConfig.Id); + downloadClient.Name, downloadClient.Id); } public override async Task LoginAsync() @@ -110,11 +109,11 @@ public partial class TransmissionService : DownloadService, ITransmissionService try { await _client.GetSessionInformationAsync(); - _logger.LogDebug("Successfully logged in to Transmission client {clientId}", _clientConfig.Id); + _logger.LogDebug("Successfully logged in to Transmission client {clientId}", _downloadClient.Id); } catch (Exception ex) { - _logger.LogError(ex, "Failed to login to Transmission client {clientId}", _clientConfig.Id); + _logger.LogError(ex, "Failed to login to Transmission client {clientId}", _downloadClient.Id); throw; } } diff --git a/code/Infrastructure/Verticals/DownloadRemover/QueueItemRemover.cs b/code/Infrastructure/Verticals/DownloadRemover/QueueItemRemover.cs index 4310c7d3..0a93f12b 100644 --- a/code/Infrastructure/Verticals/DownloadRemover/QueueItemRemover.cs +++ b/code/Infrastructure/Verticals/DownloadRemover/QueueItemRemover.cs @@ -1,5 +1,5 @@ using Common.Configuration.Arr; -using Common.Configuration.General; +using Data; using Data.Enums; using Data.Models.Arr; using Data.Models.Arr.Queue; @@ -10,25 +10,25 @@ using Infrastructure.Verticals.Context; using Infrastructure.Verticals.DownloadRemover.Interfaces; using Infrastructure.Verticals.DownloadRemover.Models; using Microsoft.Extensions.Caching.Memory; -using Infrastructure.Configuration; +using Microsoft.EntityFrameworkCore; namespace Infrastructure.Verticals.DownloadRemover; public sealed class QueueItemRemover : IQueueItemRemover { - private readonly GeneralConfig _generalConfig; + private readonly DataContext _dataContext; private readonly IMemoryCache _cache; private readonly ArrClientFactory _arrClientFactory; private readonly EventPublisher _eventPublisher; public QueueItemRemover( - IConfigManager configManager, + DataContext dataContext, IMemoryCache cache, ArrClientFactory arrClientFactory, EventPublisher eventPublisher ) { - _generalConfig = configManager.GetConfiguration(); + _dataContext = dataContext; _cache = cache; _arrClientFactory = arrClientFactory; _eventPublisher = eventPublisher; @@ -39,6 +39,7 @@ public sealed class QueueItemRemover : IQueueItemRemover { try { + var generalConfig = await _dataContext.GeneralConfigs.FirstAsync(); var arrClient = _arrClientFactory.GetClient(request.InstanceType); await arrClient.DeleteQueueItemAsync(request.Instance, request.Record, request.RemoveFromClient, request.DeleteReason); @@ -52,7 +53,7 @@ public sealed class QueueItemRemover : IQueueItemRemover // Use the new centralized EventPublisher method await _eventPublisher.PublishQueueItemDeleted(request.RemoveFromClient, request.DeleteReason); - if (!_generalConfig.SearchEnabled) + if (!generalConfig.SearchEnabled) { return; } @@ -60,7 +61,7 @@ public sealed class QueueItemRemover : IQueueItemRemover await arrClient.SearchItemsAsync(request.Instance, [request.SearchItem]); // prevent tracker spamming - await Task.Delay(TimeSpan.FromSeconds(_generalConfig.SearchDelay)); + await Task.Delay(TimeSpan.FromSeconds(generalConfig.SearchDelay)); } finally { diff --git a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs index f3508203..cc39a8b4 100644 --- a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs +++ b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs @@ -1,29 +1,27 @@ using Common.Configuration.Arr; -using Common.Configuration.DownloadClient; +using Common.Configuration.DownloadCleaner; using Common.Configuration.General; +using Common.Configuration.QueueCleaner; +using Data; using Data.Enums; using Data.Models.Arr; using Data.Models.Arr.Queue; -using Infrastructure.Configuration; using Infrastructure.Events; using Infrastructure.Verticals.Arr; +using Infrastructure.Verticals.Context; using Infrastructure.Verticals.DownloadClient; using Infrastructure.Verticals.DownloadRemover.Models; -using Infrastructure.Verticals.Notifications; using MassTransit; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Infrastructure.Verticals.Jobs; -public abstract class GenericHandler : IHandler, IDisposable +public abstract class GenericHandler : IHandler { protected readonly ILogger _logger; - protected readonly GeneralConfig _generalConfig; - protected readonly DownloadClientConfig _downloadClientConfig; - protected readonly SonarrConfig _sonarrConfig; - protected readonly RadarrConfig _radarrConfig; - protected readonly LidarrConfig _lidarrConfig; + protected readonly DataContext _dataContext; protected readonly IMemoryCache _cache; protected readonly IBus _messageBus; protected readonly ArrClientFactory _arrClientFactory; @@ -31,9 +29,6 @@ public abstract class GenericHandler : IHandler, IDisposable protected readonly DownloadServiceFactory _downloadServiceFactory; private readonly EventPublisher _eventPublisher; - // Collection of download services for use with multiple clients - protected readonly List _downloadServices = []; - protected GenericHandler( ILogger logger, IMemoryCache cache, @@ -41,7 +36,7 @@ public abstract class GenericHandler : IHandler, IDisposable ArrClientFactory arrClientFactory, ArrQueueIterator arrArrQueueIterator, DownloadServiceFactory downloadServiceFactory, - IConfigManager configManager, + DataContext dataContext, EventPublisher eventPublisher ) { @@ -52,31 +47,51 @@ public abstract class GenericHandler : IHandler, IDisposable _arrArrQueueIterator = arrArrQueueIterator; _downloadServiceFactory = downloadServiceFactory; _eventPublisher = eventPublisher; - _generalConfig = configManager.GetConfiguration(); - _downloadClientConfig = configManager.GetConfiguration(); - _sonarrConfig = configManager.GetConfiguration(); - _radarrConfig = configManager.GetConfiguration(); - _lidarrConfig = configManager.GetConfiguration(); + _dataContext = dataContext; + // _generalConfig = configManager.GetConfiguration(); + // _downloadClientConfigs = configManager.GetConfiguration(); + // _sonarrConfig = configManager.GetConfiguration(); + // _radarrConfig = configManager.GetConfiguration(); + // _lidarrConfig = configManager.GetConfiguration(); } /// /// Initialize download services based on configuration /// - protected virtual void InitializeDownloadServices() + protected async Task> GetDownloadServices() { - // Clear any existing services - DisposeDownloadServices(); - _downloadServices.Clear(); + var clients = await _dataContext.DownloadClients + .AsNoTracking() + .ToListAsync(); + + if (clients.Count is 0) + { + _logger.LogWarning("No download clients configured"); + return []; + } + + var enabledClients = await _dataContext.DownloadClients + .Where(c => c.Enabled) + .ToListAsync(); + + if (enabledClients.Count == 0) + { + _logger.LogWarning("No enabled download clients available"); + return []; + } + + List downloadServices = []; // Add all enabled clients - foreach (var client in _downloadClientConfig.GetEnabledClients()) + foreach (var client in enabledClients) { try { var service = _downloadServiceFactory.GetDownloadService(client); if (service != null) { - _downloadServices.Add(service); + await service.LoginAsync(); + downloadServices.Add(service); _logger.LogDebug("Initialized download client: {name}", client.Name); } else @@ -90,54 +105,52 @@ public abstract class GenericHandler : IHandler, IDisposable } } - if (_downloadServices.Count == 0) + if (downloadServices.Count == 0) { - _logger.LogWarning("No enabled download clients found"); + _logger.LogWarning("No valid download clients found"); } else { - _logger.LogDebug("Initialized {count} download clients", _downloadServices.Count); + _logger.LogDebug("Initialized {count} download clients", downloadServices.Count); } + + return downloadServices; } - public virtual async Task ExecuteAsync() + public async Task ExecuteAsync() { - // Initialize download services - InitializeDownloadServices(); + ContextProvider.Set(nameof(GeneralConfig), await _dataContext.GeneralConfigs.FirstAsync()); + ContextProvider.Set(nameof(SonarrConfig), await _dataContext.SonarrConfigs.FirstAsync()); + ContextProvider.Set(nameof(RadarrConfig), await _dataContext.RadarrConfigs.FirstAsync()); + ContextProvider.Set(nameof(LidarrConfig), await _dataContext.LidarrConfigs.FirstAsync()); + ContextProvider.Set(nameof(QueueCleanerConfig), await _dataContext.QueueCleanerConfigs.FirstAsync()); + ContextProvider.Set(nameof(DownloadCleanerConfig), await _dataContext.DownloadCleanerConfigs.FirstAsync()); - if (_downloadServices.Count == 0) - { - _logger.LogWarning("No download clients available, skipping execution"); - return; - } - - // Login to all download services - foreach (var downloadService in _downloadServices) - { - await downloadService.LoginAsync(); - } - - await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr); - await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr); - await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr); + await ExecuteInternalAsync(); } + // { + // // Initialize download services + // GetDownloadServices(); + // + // if (_downloadServices.Count == 0) + // { + // _logger.LogWarning("No download clients available, skipping execution"); + // return; + // } + // + // // Login to all download services + // foreach (var downloadService in _downloadServices) + // { + // await downloadService.LoginAsync(); + // } + // + // await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr); + // await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr); + // await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr); + // } - public virtual void Dispose() - { - DisposeDownloadServices(); - } + protected abstract Task ExecuteInternalAsync(); - /// - /// Dispose all download services - /// - protected void DisposeDownloadServices() - { - foreach (var service in _downloadServices) - { - service.Dispose(); - } - } - protected abstract Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config); protected async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType, bool throwOnFailure = false) @@ -210,27 +223,28 @@ public abstract class GenericHandler : IHandler, IDisposable protected SearchItem GetRecordSearchItem(InstanceType type, QueueRecord record, bool isPack = false) { + var sonarrConfig = ContextProvider.Get(nameof(SonarrConfig)); return type switch { - InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Episode && !isPack => new SonarrSearchItem + InstanceType.Sonarr when sonarrConfig.SearchType is SonarrSearchType.Episode && !isPack => new SonarrSearchItem { Id = record.EpisodeId, SeriesId = record.SeriesId, SearchType = SonarrSearchType.Episode }, - InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Episode && isPack => new SonarrSearchItem + InstanceType.Sonarr when sonarrConfig.SearchType is SonarrSearchType.Episode && isPack => new SonarrSearchItem { Id = record.SeasonNumber, SeriesId = record.SeriesId, SearchType = SonarrSearchType.Season }, - InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Season => new SonarrSearchItem + InstanceType.Sonarr when sonarrConfig.SearchType is SonarrSearchType.Season => new SonarrSearchItem { Id = record.SeasonNumber, SeriesId = record.SeriesId, SearchType = SonarrSearchType.Series }, - InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Series => new SonarrSearchItem + InstanceType.Sonarr when sonarrConfig.SearchType is SonarrSearchType.Series => new SonarrSearchItem { Id = record.SeriesId }, diff --git a/code/Infrastructure/Verticals/Notifications/INotificationProvider.cs b/code/Infrastructure/Verticals/Notifications/INotificationProvider.cs index d1a783f0..02b94709 100644 --- a/code/Infrastructure/Verticals/Notifications/INotificationProvider.cs +++ b/code/Infrastructure/Verticals/Notifications/INotificationProvider.cs @@ -5,7 +5,7 @@ namespace Infrastructure.Verticals.Notifications; public interface INotificationProvider { - BaseNotificationConfig Config { get; } + NotificationConfig Config { get; } string Name { get; } diff --git a/code/Infrastructure/Verticals/Notifications/NotificationProvider.cs b/code/Infrastructure/Verticals/Notifications/NotificationProvider.cs index 5c3cd201..2065d3b7 100644 --- a/code/Infrastructure/Verticals/Notifications/NotificationProvider.cs +++ b/code/Infrastructure/Verticals/Notifications/NotificationProvider.cs @@ -10,7 +10,7 @@ public abstract class NotificationProvider : INotificationProvider private readonly IConfigManager _configManager; protected readonly NotificationsConfig _config; - public abstract BaseNotificationConfig Config { get; } + public abstract NotificationConfig Config { get; } protected NotificationProvider(IConfigManager configManager) {