using Cleanuparr.Domain.Entities;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence.Converters;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
using Cleanuparr.Persistence.Models.State;
using Cleanuparr.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Serilog.Events;
namespace Cleanuparr.Persistence;
///
/// Database context for configuration data
///
public class DataContext : DbContext
{
public static SemaphoreSlim Lock { get; } = new(1, 1);
public DbSet GeneralConfigs { get; set; }
public DbSet DownloadClients { get; set; }
public DbSet QueueCleanerConfigs { get; set; }
public DbSet StallRules { get; set; }
public DbSet SlowRules { get; set; }
public DbSet ContentBlockerConfigs { get; set; }
public DbSet DownloadCleanerConfigs { get; set; }
public DbSet SeedingRules { get; set; }
public DbSet ArrConfigs { get; set; }
public DbSet ArrInstances { get; set; }
public DbSet NotificationConfigs { get; set; }
public DbSet NotifiarrConfigs { get; set; }
public DbSet AppriseConfigs { get; set; }
public DbSet NtfyConfigs { get; set; }
public DbSet PushoverConfigs { get; set; }
public DbSet TelegramConfigs { get; set; }
public DbSet DiscordConfigs { get; set; }
public DbSet GotifyConfigs { get; set; }
public DbSet BlacklistSyncHistory { get; set; }
public DbSet BlacklistSyncConfigs { get; set; }
public DataContext()
{
}
public DataContext(DbContextOptions options) : base(options)
{
}
public static DataContext CreateStaticInstance()
{
var optionsBuilder = new DbContextOptionsBuilder();
SetDbContextOptions(optionsBuilder);
return new DataContext(optionsBuilder.Options);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
SetDbContextOptions(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(entity =>
{
entity.ComplexProperty(e => e.Log, cp =>
{
cp.Property(l => l.Level).HasConversion>();
});
entity.ComplexProperty(e => e.Auth, cp =>
{
cp.Property(a => a.TrustedNetworks)
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
});
});
modelBuilder.Entity(entity =>
{
entity.ComplexProperty(e => e.FailedImport, cp =>
{
cp.Property(x => x.PatternMode).HasConversion>();
});
});
modelBuilder.Entity(entity =>
{
entity.ComplexProperty(e => e.Sonarr, cp =>
{
cp.Property(s => s.BlocklistType).HasConversion>();
});
entity.ComplexProperty(e => e.Radarr, cp =>
{
cp.Property(s => s.BlocklistType).HasConversion>();
});
entity.ComplexProperty(e => e.Lidarr, cp =>
{
cp.Property(s => s.BlocklistType).HasConversion>();
});
entity.ComplexProperty(e => e.Readarr, cp =>
{
cp.Property(s => s.BlocklistType).HasConversion>();
});
});
// Configure ArrConfig -> ArrInstance relationship
modelBuilder.Entity(entity =>
{
entity.HasMany(a => a.Instances)
.WithOne(i => i.ArrConfig)
.HasForeignKey(i => i.ArrConfigId)
.OnDelete(DeleteBehavior.Cascade);
});
// Configure new notification system relationships
modelBuilder.Entity(entity =>
{
entity.Property(e => e.Type).HasConversion(new LowercaseEnumConverter());
entity.HasOne(p => p.NotifiarrConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.AppriseConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.NtfyConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.PushoverConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.TelegramConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.DiscordConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.GotifyConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(p => p.Name).IsUnique();
});
// Configure PushoverConfig List conversions
modelBuilder.Entity(entity =>
{
entity.Property(p => p.Devices)
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
entity.Property(p => p.Tags)
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
});
// Configure BlacklistSyncState relationships and indexes
modelBuilder.Entity(entity =>
{
// FK to DownloadClientConfig by DownloadClientId with cascade on delete
entity.HasOne(s => s.DownloadClient)
.WithMany()
.HasForeignKey(s => s.DownloadClientId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(s => new { s.Hash, DownloadClientId = s.DownloadClientId }).IsUnique();
entity.HasIndex(s => s.Hash);
});
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
// Use OriginalString for Uri properties to preserve the exact input (including embedded credentials)
foreach (var property in entityType.GetProperties().Where(p => p.ClrType == typeof(Uri)))
{
property.SetValueConverter(
new ValueConverter(
v => v.OriginalString,
v => new Uri(v, UriKind.RelativeOrAbsolute)));
property.SetValueComparer(new ValueComparer(
(u1, u2) => u1 != null && u2 != null
? u1.OriginalString == u2.OriginalString
: u1 == null && u2 == null,
u => u == null ? 0 : u.OriginalString.GetHashCode(),
u => u == null ? null! : new Uri(u.OriginalString, UriKind.RelativeOrAbsolute)));
}
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)
{
var enumType = property.PropertyType.IsEnum
? property.PropertyType
: property.PropertyType.GetGenericArguments()[0];
var converterType = typeof(LowercaseEnumConverter<>).MakeGenericType(enumType);
var converter = Activator.CreateInstance(converterType);
modelBuilder.Entity(entityType.ClrType)
.Property(property.Name)
.HasConversion((ValueConverter)converter!);
}
}
}
private static void SetDbContextOptions(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}
var dbPath = Path.Combine(ConfigurationPathProvider.GetConfigPath(), "cleanuparr.db");
optionsBuilder
.UseSqlite($"Data Source={dbPath}")
.UseLowerCaseNamingConvention()
.UseSnakeCaseNamingConvention();
}
}