using System; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; namespace Kyoo { /// /// The database handle used for all local repositories. /// This is an abstract class. It is meant to be implemented by plugins. This allow the core to be database agnostic. /// /// /// It should not be used directly, to access the database use a or repositories. /// public abstract class DatabaseContext : DbContext { /// /// All libraries of Kyoo. See . /// public DbSet Libraries { get; set; } /// /// All collections of Kyoo. See . /// public DbSet Collections { get; set; } /// /// All shows of Kyoo. See . /// public DbSet Shows { get; set; } /// /// All seasons of Kyoo. See . /// public DbSet Seasons { get; set; } /// /// All episodes of Kyoo. See . /// public DbSet Episodes { get; set; } /// /// All tracks of Kyoo. See . /// public DbSet Tracks { get; set; } /// /// All genres of Kyoo. See . /// public DbSet Genres { get; set; } /// /// All people of Kyoo. See . /// public DbSet People { get; set; } /// /// All studios of Kyoo. See . /// public DbSet Studios { get; set; } /// /// All providers of Kyoo. See . /// public DbSet Providers { get; set; } /// /// All metadataIDs (ExternalIDs) of Kyoo. See . /// public DbSet MetadataIds { get; set; } /// /// The list of registered users. /// public DbSet Users { get; set; } /// /// All people's role. See . /// public DbSet PeopleRoles { get; set; } /// /// Episodes with a watch percentage. See /// public DbSet WatchedEpisodes { get; set; } /// /// Get a generic link between two resource types. /// /// Types are order dependant. You can't inverse the order. Please always put the owner first. /// The first resource type of the relation. It is the owner of the second /// The second resource type of the relation. It is the contained resource. /// All links between the two types. public DbSet> Links() where T1 : class, IResource where T2 : class, IResource { return Set>(); } /// /// The default constructor /// protected DatabaseContext() { } /// /// Create a new using specific options /// /// The options to use. protected DatabaseContext(DbContextOptions options) : base(options) { } /// /// Set basic configurations (like preventing query tracking) /// /// An option builder to fill. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); } /// /// Set database parameters to support every types of Kyoo. /// /// The database's model builder. protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity() .Property(t => t.IsDefault) .ValueGeneratedNever(); modelBuilder.Entity() .Property(t => t.IsForced) .ValueGeneratedNever(); modelBuilder.Entity() .HasMany(x => x.Libraries) .WithMany(x => x.Providers) .UsingEntity>( y => y .HasOne(x => x.First) .WithMany(x => x.ProviderLinks), y => y .HasOne(x => x.Second) .WithMany(x => x.LibraryLinks), y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasMany(x => x.Libraries) .WithMany(x => x.Collections) .UsingEntity>( y => y .HasOne(x => x.First) .WithMany(x => x.CollectionLinks), y => y .HasOne(x => x.Second) .WithMany(x => x.LibraryLinks), y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasMany(x => x.Libraries) .WithMany(x => x.Shows) .UsingEntity>( y => y .HasOne(x => x.First) .WithMany(x => x.ShowLinks), y => y .HasOne(x => x.Second) .WithMany(x => x.LibraryLinks), y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasMany(x => x.Collections) .WithMany(x => x.Shows) .UsingEntity>( y => y .HasOne(x => x.First) .WithMany(x => x.ShowLinks), y => y .HasOne(x => x.Second) .WithMany(x => x.CollectionLinks), y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasMany(x => x.Shows) .WithMany(x => x.Genres) .UsingEntity>( y => y .HasOne(x => x.First) .WithMany(x => x.GenreLinks), y => y .HasOne(x => x.Second) .WithMany(x => x.ShowLinks), y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasMany(x => x.Watched) .WithMany("users") .UsingEntity>( y => y .HasOne(x => x.Second) .WithMany(), y => y .HasOne(x => x.First) .WithMany(x => x.ShowLinks), y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasOne(x => x.Show) .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() .HasOne(x => x.Provider) .WithMany(x => x.MetadataLinks) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasKey(x => new {First = x.FirstID, Second = x.SecondID}); 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().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.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); } /// /// Return a new or an in cache temporary object wih the same ID as the one given /// /// If a resource with the same ID is found in the database, it will be used. /// will be used otherwise /// The type of the resource /// A resource that is now tracked by this context. public T GetTemporaryObject(T model) where T : class, IResource { T tmp = Set().Local.FirstOrDefault(x => x.ID == model.ID); if (tmp != null) return tmp; Entry(model).State = EntityState.Unchanged; return model; } /// /// Save changes that are applied to this context. /// /// A duplicated item has been found. /// The number of state entries written to the database. public override int SaveChanges() { try { return base.SaveChanges(); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } /// /// Save changes that are applied to this context. /// /// Indicates whether AcceptAllChanges() is called after the changes /// have been sent successfully to the database. /// A duplicated item has been found. /// The number of state entries written to the database. public override int SaveChanges(bool acceptAllChangesOnSuccess) { try { return base.SaveChanges(acceptAllChangesOnSuccess); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } /// /// Save changes that are applied to this context. /// /// The message that will have the /// (if a duplicate is found). /// A duplicated item has been found. /// The number of state entries written to the database. public int SaveChanges(string duplicateMessage) { try { return base.SaveChanges(); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(duplicateMessage); throw; } } /// /// Save changes that are applied to this context. /// /// Indicates whether AcceptAllChanges() is called after the changes /// have been sent successfully to the database. /// A to observe while waiting for the task to complete /// A duplicated item has been found. /// The number of state entries written to the database. public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new()) { try { return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } /// /// Save changes that are applied to this context. /// /// A to observe while waiting for the task to complete /// A duplicated item has been found. /// The number of state entries written to the database. public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) { try { return await base.SaveChangesAsync(cancellationToken); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(); throw; } } /// /// Save changes that are applied to this context. /// /// The message that will have the /// (if a duplicate is found). /// A to observe while waiting for the task to complete /// A duplicated item has been found. /// The number of state entries written to the database. public async Task SaveChangesAsync(string duplicateMessage, CancellationToken cancellationToken = new()) { try { return await base.SaveChangesAsync(cancellationToken); } catch (DbUpdateException ex) { DiscardChanges(); if (IsDuplicateException(ex)) throw new DuplicatedItemException(duplicateMessage); throw; } } /// /// Save changes if no duplicates are found. If one is found, no change are saved but the current changes are no discarded. /// The current context will still hold those invalid changes. /// /// A to observe while waiting for the task to complete /// The number of state entries written to the database or -1 if a duplicate exist. public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new()) { try { return await SaveChangesAsync(cancellationToken); } catch (DuplicatedItemException) { return -1; } } /// /// Save items or retry with a custom method if a duplicate is found. /// /// The item to save (other changes of this context will also be saved) /// A function to run on fail, the param wil be mapped. /// The second parameter is the current retry number. /// A to observe while waiting for the task to complete /// The type of the item to save /// The number of state entries written to the database. public Task SaveOrRetry(T obj, Func onFail, CancellationToken cancellationToken = new()) { return SaveOrRetry(obj, onFail, 0, cancellationToken); } /// /// Save items or retry with a custom method if a duplicate is found. /// /// The item to save (other changes of this context will also be saved) /// A function to run on fail, the param wil be mapped. /// The second parameter is the current retry number. /// The current retry number. /// A to observe while waiting for the task to complete /// The type of the item to save /// The number of state entries written to the database. private async Task SaveOrRetry(T obj, Func onFail, int recurse, CancellationToken cancellationToken = new()) { try { await base.SaveChangesAsync(true, cancellationToken); return obj; } catch (DbUpdateException ex) when (IsDuplicateException(ex)) { recurse++; return await SaveOrRetry(onFail(obj, recurse), onFail, recurse, cancellationToken); } catch (DbUpdateException) { DiscardChanges(); throw; } } /// /// Check if the exception is a duplicated exception. /// /// The exception to check /// True if the exception is a duplicate exception. False otherwise protected abstract bool IsDuplicateException(Exception ex); /// /// Delete every changes that are on this context. /// private void DiscardChanges() { foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged && x.State != EntityState.Detached)) { entry.State = EntityState.Detached; } } /// /// Perform a case insensitive like operation. /// /// An accessor to get the item that will be checked. /// The second operator of the like format. /// The type of the item to query /// An expression representing the like query. It can directly be passed to a where call. public abstract Expression> Like(Expression> query, string format); } }