mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-11-04 11:33:15 -05:00
344 lines
9.4 KiB
C#
344 lines
9.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Kyoo.Models;
|
|
using Kyoo.Models.Exceptions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
using Npgsql;
|
|
|
|
namespace Kyoo
|
|
{
|
|
//TODO disable lazy loading a provide a LoadAsync method in the library manager.
|
|
|
|
public class DatabaseContext : DbContext
|
|
{
|
|
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { }
|
|
|
|
public DbSet<LibraryDE> Libraries { get; set; }
|
|
public DbSet<CollectionDE> Collections { get; set; }
|
|
public DbSet<ShowDE> Shows { get; set; }
|
|
public DbSet<Season> Seasons { get; set; }
|
|
public DbSet<Episode> Episodes { get; set; }
|
|
public DbSet<Track> Tracks { get; set; }
|
|
public DbSet<GenreDE> Genres { get; set; }
|
|
public DbSet<People> People { get; set; }
|
|
public DbSet<Studio> Studios { get; set; }
|
|
public DbSet<ProviderID> Providers { get; set; }
|
|
public DbSet<MetadataID> MetadataIds { get; set; }
|
|
|
|
public DbSet<PeopleRole> PeopleRoles { get; set; }
|
|
|
|
|
|
public DbSet<LibraryLink> LibraryLinks { get; set; }
|
|
public DbSet<CollectionLink> CollectionLinks { get; set; }
|
|
public DbSet<GenreLink> GenreLinks { get; set; }
|
|
public DbSet<ProviderLink> ProviderLinks { get; set; }
|
|
|
|
public DatabaseContext()
|
|
{
|
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
|
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
|
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
|
|
}
|
|
|
|
private readonly ValueComparer<IEnumerable<string>> _stringArrayComparer = new(
|
|
(l1, l2) => l1.SequenceEqual(l2),
|
|
arr => arr.Aggregate(0, (i, s) => s.GetHashCode())
|
|
);
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
modelBuilder.HasPostgresEnum<Status>();
|
|
modelBuilder.HasPostgresEnum<ItemType>();
|
|
modelBuilder.HasPostgresEnum<StreamType>();
|
|
|
|
modelBuilder.Ignore<Library>();
|
|
modelBuilder.Ignore<Collection>();
|
|
modelBuilder.Ignore<Show>();
|
|
modelBuilder.Ignore<Genre>();
|
|
|
|
modelBuilder.Entity<LibraryDE>()
|
|
.Property(x => x.Paths)
|
|
.HasColumnType("text[]")
|
|
.Metadata.SetValueComparer(_stringArrayComparer);
|
|
|
|
modelBuilder.Entity<ShowDE>()
|
|
.Property(x => x.Aliases)
|
|
.HasColumnType("text[]")
|
|
.Metadata.SetValueComparer(_stringArrayComparer);
|
|
|
|
modelBuilder.Entity<Track>()
|
|
.Property(t => t.IsDefault)
|
|
.ValueGeneratedNever();
|
|
|
|
modelBuilder.Entity<Track>()
|
|
.Property(t => t.IsForced)
|
|
.ValueGeneratedNever();
|
|
|
|
|
|
modelBuilder.Entity<GenreLink>()
|
|
.HasKey(x => new {ShowID = x.ParentID, GenreID = x.ChildID});
|
|
|
|
modelBuilder.Entity<CollectionLink>()
|
|
.HasKey(x => new {CollectionID = x.ParentID, ShowID = x.ChildID});
|
|
|
|
modelBuilder.Entity<ProviderLink>()
|
|
.HasKey(x => new {LibraryID = x.ParentID, ProviderID = x.ChildID});
|
|
|
|
|
|
modelBuilder.Entity<LibraryDE>()
|
|
.Ignore(x => x.Shows)
|
|
.Ignore(x => x.Collections)
|
|
.Ignore(x => x.Providers);
|
|
|
|
modelBuilder.Entity<CollectionDE>()
|
|
.Ignore(x => x.Shows)
|
|
.Ignore(x => x.Libraries);
|
|
|
|
modelBuilder.Entity<ShowDE>()
|
|
.Ignore(x => x.Genres)
|
|
.Ignore(x => x.Libraries)
|
|
.Ignore(x => x.Collections);
|
|
|
|
modelBuilder.Entity<PeopleRole>()
|
|
.Ignore(x => x.Slug)
|
|
.Ignore(x => x.Name)
|
|
.Ignore(x => x.Poster)
|
|
.Ignore(x => x.ExternalIDs);
|
|
|
|
modelBuilder.Entity<GenreDE>()
|
|
.Ignore(x => x.Shows);
|
|
|
|
|
|
modelBuilder.Entity<LibraryLink>()
|
|
.HasOne(x => x.Library as LibraryDE)
|
|
.WithMany(x => x.Links)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
modelBuilder.Entity<LibraryLink>()
|
|
.HasOne(x => x.Show as ShowDE)
|
|
.WithMany(x => x.LibraryLinks)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
modelBuilder.Entity<LibraryLink>()
|
|
.HasOne(x => x.Collection as CollectionDE)
|
|
.WithMany(x => x.LibraryLinks)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
modelBuilder.Entity<CollectionLink>()
|
|
.HasOne(x => x.Parent as CollectionDE)
|
|
.WithMany(x => x.Links)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
modelBuilder.Entity<CollectionLink>()
|
|
.HasOne(x => x.Child as ShowDE)
|
|
.WithMany(x => x.CollectionLinks)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
modelBuilder.Entity<GenreLink>()
|
|
.HasOne(x => x.Child as GenreDE)
|
|
.WithMany(x => x.Links)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
modelBuilder.Entity<GenreLink>()
|
|
.HasOne(x => x.Parent as ShowDE)
|
|
.WithMany(x => x.GenreLinks)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
modelBuilder.Entity<ProviderLink>()
|
|
.HasOne(x => x.Parent as LibraryDE)
|
|
.WithMany(x => x.ProviderLinks)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
modelBuilder.Entity<Season>()
|
|
.HasOne(x => x.Show as ShowDE)
|
|
.WithMany(x => x.Seasons);
|
|
modelBuilder.Entity<Episode>()
|
|
.HasOne(x => x.Show as ShowDE)
|
|
.WithMany(x => x.Episodes);
|
|
modelBuilder.Entity<PeopleRole>()
|
|
.HasOne(x => x.Show as ShowDE)
|
|
.WithMany(x => x.People);
|
|
|
|
|
|
|
|
modelBuilder.Entity<MetadataID>()
|
|
.HasOne(x => x.Show as ShowDE)
|
|
.WithMany(x => x.ExternalIDs)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
modelBuilder.Entity<MetadataID>()
|
|
.HasOne(x => x.Season)
|
|
.WithMany(x => x.ExternalIDs)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
modelBuilder.Entity<MetadataID>()
|
|
.HasOne(x => x.Episode)
|
|
.WithMany(x => x.ExternalIDs)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
modelBuilder.Entity<MetadataID>()
|
|
.HasOne(x => x.People)
|
|
.WithMany(x => x.ExternalIDs)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
modelBuilder.Entity<CollectionDE>().Property(x => x.Slug).IsRequired();
|
|
modelBuilder.Entity<GenreDE>().Property(x => x.Slug).IsRequired();
|
|
modelBuilder.Entity<LibraryDE>().Property(x => x.Slug).IsRequired();
|
|
modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired();
|
|
modelBuilder.Entity<ProviderID>().Property(x => x.Slug).IsRequired();
|
|
modelBuilder.Entity<ShowDE>().Property(x => x.Slug).IsRequired();
|
|
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
|
|
|
|
modelBuilder.Entity<CollectionDE>()
|
|
.HasIndex(x => x.Slug)
|
|
.IsUnique();
|
|
modelBuilder.Entity<GenreDE>()
|
|
.HasIndex(x => x.Slug)
|
|
.IsUnique();
|
|
modelBuilder.Entity<LibraryDE>()
|
|
.HasIndex(x => x.Slug)
|
|
.IsUnique();
|
|
modelBuilder.Entity<People>()
|
|
.HasIndex(x => x.Slug)
|
|
.IsUnique();
|
|
modelBuilder.Entity<ShowDE>()
|
|
.HasIndex(x => x.Slug)
|
|
.IsUnique();
|
|
modelBuilder.Entity<Studio>()
|
|
.HasIndex(x => x.Slug)
|
|
.IsUnique();
|
|
modelBuilder.Entity<ProviderID>()
|
|
.HasIndex(x => x.Slug)
|
|
.IsUnique();
|
|
modelBuilder.Entity<Season>()
|
|
.HasIndex(x => new {x.ShowID, x.SeasonNumber})
|
|
.IsUnique();
|
|
modelBuilder.Entity<Episode>()
|
|
.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
|
|
.IsUnique();
|
|
modelBuilder.Entity<LibraryLink>()
|
|
.HasIndex(x => new {x.LibraryID, x.ShowID})
|
|
.IsUnique();
|
|
modelBuilder.Entity<LibraryLink>()
|
|
.HasIndex(x => new {x.LibraryID, x.CollectionID})
|
|
.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<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
|
|
CancellationToken cancellationToken = new())
|
|
{
|
|
try
|
|
{
|
|
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
}
|
|
catch (DbUpdateException ex)
|
|
{
|
|
DiscardChanges();
|
|
if (IsDuplicateException(ex))
|
|
throw new DuplicatedItemException();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
|
|
{
|
|
try
|
|
{
|
|
return await base.SaveChangesAsync(cancellationToken);
|
|
}
|
|
catch (DbUpdateException ex)
|
|
{
|
|
DiscardChanges();
|
|
if (IsDuplicateException(ex))
|
|
throw new DuplicatedItemException();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<int> SaveChangesAsync(string duplicateMessage,
|
|
CancellationToken cancellationToken = new())
|
|
{
|
|
try
|
|
{
|
|
return await base.SaveChangesAsync(cancellationToken);
|
|
}
|
|
catch (DbUpdateException ex)
|
|
{
|
|
DiscardChanges();
|
|
if (IsDuplicateException(ex))
|
|
throw new DuplicatedItemException(duplicateMessage);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = new())
|
|
{
|
|
try
|
|
{
|
|
return await SaveChangesAsync(cancellationToken);
|
|
}
|
|
catch (DuplicatedItemException)
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
private static bool IsDuplicateException(Exception ex)
|
|
{
|
|
return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation};
|
|
}
|
|
|
|
private void DiscardChanges()
|
|
{
|
|
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged
|
|
&& x.State != EntityState.Detached))
|
|
{
|
|
entry.State = EntityState.Detached;
|
|
}
|
|
}
|
|
}
|
|
} |