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;
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; }
///
/// 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; }
///
/// The list of library items (shows and collections that are part of a library - or the global one)
///
///
/// This set is ready only, on most database this will be a view.
///
public DbSet LibraryItems { get; set; }
///
/// Get all metadataIDs (ExternalIDs) of a given resource. See .
///
/// The metadata of this type will be returned.
/// A queryable of metadata ids for a type.
public DbSet> MetadataIds()
where T : class, IResource
{
return 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()
.HasMany(x => x.Seasons)
.WithOne(x => x.Show)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity()
.HasMany(x => x.Episodes)
.WithOne(x => x.Show)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity()
.HasMany(x => x.Episodes)
.WithOne(x => x.Season)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity()
.HasMany(x => x.Tracks)
.WithOne(x => x.Episode)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity()
.HasOne(x => x.Studio)
.WithMany(x => x.Shows)
.OnDelete(DeleteBehavior.SetNull);
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>()
.HasKey(MetadataID.PrimaryKey);
modelBuilder.Entity>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>()
.HasKey(MetadataID.PrimaryKey);
modelBuilder.Entity>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>()
.HasKey(MetadataID.PrimaryKey);
modelBuilder.Entity>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>()
.HasKey(MetadataID.PrimaryKey);
modelBuilder.Entity>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity>().HasOne(x => x.Second).WithMany()
.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 => x.Slug)
.IsUnique();
modelBuilder.Entity()
.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
.IsUnique();
modelBuilder.Entity()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity()
.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
.IsUnique();
modelBuilder.Entity()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity()
.Property(x => x.Slug)
.ValueGeneratedOnAddOrUpdate();
modelBuilder.Entity()
.Property(x => x.Slug)
.ValueGeneratedOnAddOrUpdate();
modelBuilder.Entity()
.Property(x => x.Slug)
.ValueGeneratedOnAddOrUpdate();
}
///
/// 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;
}
}
///
/// 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);
}
}