using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Kyoo.Models.Watch; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Npgsql; namespace Kyoo { public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions options) : base(options) { } public DbSet Libraries { get; set; } public DbSet Collections { get; set; } public DbSet Shows { get; set; } public DbSet Seasons { get; set; } public DbSet Episodes { get; set; } public DbSet Tracks { get; set; } public DbSet Genres { get; set; } public DbSet People { get; set; } public DbSet Studios { get; set; } public DbSet Providers { get; set; } public DbSet MetadataIds { get; set; } public DbSet PeopleRoles { get; set; } public DbSet LibraryLinks { get; set; } public DbSet CollectionLinks { get; set; } public DbSet GenreLinks { get; set; } public DbSet ProviderLinks { get; set; } public DatabaseContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); } private readonly ValueComparer> _stringArrayComparer = new ValueComparer>( (l1, l2) => l1.SequenceEqual(l2), arr => arr.Aggregate(0, (i, s) => s.GetHashCode())); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Entity() .Property(x => x.Paths) .HasColumnType("text[]") .Metadata.SetValueComparer(_stringArrayComparer); modelBuilder.Entity() .Property(x => x.Aliases) .HasColumnType("text[]") .Metadata.SetValueComparer(_stringArrayComparer); modelBuilder.Entity() .Property(t => t.IsDefault) .ValueGeneratedNever(); modelBuilder.Entity() .Property(t => t.IsForced) .ValueGeneratedNever(); modelBuilder.Entity() .HasKey(x => new {x.ShowID, x.GenreID}); modelBuilder.Entity() .Ignore(x => x.Shows) .Ignore(x => x.Collections) .Ignore(x => x.Providers); modelBuilder.Entity() .Ignore(x => x.Shows) .Ignore(x => x.Libraries); modelBuilder.Entity() .Ignore(x => x.Genres) .Ignore(x => x.Libraries) .Ignore(x => x.Collections); modelBuilder.Entity() .Ignore(x => x.Slug) .Ignore(x => x.Name) .Ignore(x => x.Poster) .Ignore(x => x.ExternalIDs); modelBuilder.Entity() .Ignore(x => x.Shows); modelBuilder.Entity() .HasOne(x => x.Library as LibraryDE) .WithMany(x => x.Links) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Show as ShowDE) .WithMany(x => x.LibraryLinks) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Collection as CollectionDE) .WithMany(x => x.LibraryLinks) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Collection as CollectionDE) .WithMany(x => x.Links) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Show as ShowDE) .WithMany(x => x.CollectionLinks) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Genre as GenreDE) .WithMany(x => x.Links) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Show as ShowDE) .WithMany(x => x.GenreLinks) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Library as LibraryDE) .WithMany(x => x.ProviderLinks) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Show as ShowDE) .WithMany(x => x.Seasons); modelBuilder.Entity() .HasOne(x => x.Show as ShowDE) .WithMany(x => x.Episodes); modelBuilder.Entity() .HasOne(x => x.Show as ShowDE) .WithMany(x => x.People); modelBuilder.Entity() .HasOne(x => x.Show as ShowDE) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Season) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.Episode) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasOne(x => x.People) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber}) .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.LibraryID, x.ShowID}) .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.LibraryID, x.CollectionID}) .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.CollectionID, x.ShowID}) .IsUnique(); } public override int SaveChanges() { try { return base.SaveChanges(); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } public override int SaveChanges(bool acceptAllChangesOnSuccess) { try { return base.SaveChanges(acceptAllChangesOnSuccess); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } public int SaveChanges(string duplicateMessage) { try { return base.SaveChanges(); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(duplicateMessage); throw; } } public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken()) { try { return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { try { return await base.SaveChangesAsync(cancellationToken); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } public async Task SaveChangesAsync(string duplicateMessage, CancellationToken cancellationToken = new CancellationToken()) { try { return await base.SaveChangesAsync(cancellationToken); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(duplicateMessage); throw; } } public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new CancellationToken()) { try { return await SaveChangesAsync(cancellationToken); } catch (DuplicatedItemException) { return -1; } } public static bool IsDuplicateException(DbUpdateException ex) { return ex.InnerException is PostgresException inner && inner.SqlState == PostgresErrorCodes.UniqueViolation; } public void DiscardChanges() { foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged && x.State != EntityState.Detached)) { entry.State = EntityState.Detached; } } } }