From 19e64e8d72c4017e20d1698ace3541a7c7326d63 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 29 May 2021 14:45:34 +0200 Subject: [PATCH 01/57] Adding an sqlite plugin --- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 1 - Kyoo.SqLite/Kyoo.SqLite.csproj | 41 + .../20210529124408_Initial.Designer.cs | 878 ++++++++++++++++++ .../Migrations/20210529124408_Initial.cs | 693 ++++++++++++++ .../Migrations/SqLiteContextModelSnapshot.cs | 876 +++++++++++++++++ Kyoo.SqLite/SqLiteContext.cs | 134 +++ Kyoo.SqLite/SqLiteModule.cs | 72 ++ Kyoo.sln | 6 + 8 files changed, 2700 insertions(+), 1 deletion(-) create mode 100644 Kyoo.SqLite/Kyoo.SqLite.csproj create mode 100644 Kyoo.SqLite/Migrations/20210529124408_Initial.Designer.cs create mode 100644 Kyoo.SqLite/Migrations/20210529124408_Initial.cs create mode 100644 Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs create mode 100644 Kyoo.SqLite/SqLiteContext.cs create mode 100644 Kyoo.SqLite/SqLiteModule.cs diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 52c9041b..9dcbba0b 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -1,5 +1,4 @@ - net5.0 diff --git a/Kyoo.SqLite/Kyoo.SqLite.csproj b/Kyoo.SqLite/Kyoo.SqLite.csproj new file mode 100644 index 00000000..c48664bc --- /dev/null +++ b/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -0,0 +1,41 @@ + + + net5.0 + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + Kyoo.SQLite + + + + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/sqlite + false + false + false + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + false + runtime + + + all + false + runtime + + + diff --git a/Kyoo.SqLite/Migrations/20210529124408_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210529124408_Initial.Designer.cs new file mode 100644 index 00000000..ffdeefad --- /dev/null +++ b/Kyoo.SqLite/Migrations/20210529124408_Initial.Designer.cs @@ -0,0 +1,878 @@ +// +using System; +using Kyoo.SqLite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Kyoo.SQLite.Migrations +{ + [DbContext(typeof(SqLiteContext))] + [Migration("20210529124408_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.6"); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteNumber") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Runtime") + .HasColumnType("INTEGER"); + + b.Property("SeasonID") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Paths") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("EpisodeID") + .HasColumnType("INTEGER"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("PeopleID") + .HasColumnType("INTEGER"); + + b.Property("ProviderID") + .HasColumnType("INTEGER"); + + b.Property("SeasonID") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ProviderID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID"); + + b.ToTable("MetadataIds"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("PeopleID") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("LogoExtension") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Aliases") + .HasColumnType("TEXT"); + + b.Property("Backdrop") + .HasColumnType("TEXT"); + + b.Property("EndYear") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartYear") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StudioID") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrailerUrl") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("EpisodeID") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("ExtraData") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("WatchedPercentage") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID"); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("GenreLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("ExternalIDs") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.People", "People") + .WithMany("ExternalIDs") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Provider", "Provider") + .WithMany("MetadataLinks") + .HasForeignKey("ProviderID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("ExternalIDs") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("ExternalIDs") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Episode"); + + b.Navigation("People"); + + b.Navigation("Provider"); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("MetadataLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.SqLite/Migrations/20210529124408_Initial.cs b/Kyoo.SqLite/Migrations/20210529124408_Initial.cs new file mode 100644 index 00000000..68a28ec5 --- /dev/null +++ b/Kyoo.SqLite/Migrations/20210529124408_Initial.cs @@ -0,0 +1,693 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Kyoo.SQLite.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Collections", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Poster = table.Column(type: "TEXT", nullable: true), + Overview = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Collections", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Genres", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Genres", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Libraries", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Paths = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Libraries", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "People", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Poster = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_People", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Providers", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Logo = table.Column(type: "TEXT", nullable: true), + LogoExtension = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Providers", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Studios", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Studios", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Username = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + Password = table.Column(type: "TEXT", nullable: true), + Permissions = table.Column(type: "TEXT", nullable: true), + ExtraData = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Collections_SecondID", + column: x => x.SecondID, + principalTable: "Collections", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, + principalTable: "Libraries", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, + principalTable: "Libraries", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Shows", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", nullable: true), + Aliases = table.Column(type: "TEXT", nullable: true), + Path = table.Column(type: "TEXT", nullable: true), + Overview = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "INTEGER", nullable: true), + TrailerUrl = table.Column(type: "TEXT", nullable: true), + StartYear = table.Column(type: "INTEGER", nullable: true), + EndYear = table.Column(type: "INTEGER", nullable: true), + Poster = table.Column(type: "TEXT", nullable: true), + Logo = table.Column(type: "TEXT", nullable: true), + Backdrop = table.Column(type: "TEXT", nullable: true), + IsMovie = table.Column(type: "INTEGER", nullable: false), + StudioID = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Shows", x => x.ID); + table.ForeignKey( + name: "FK_Shows_Studios_StudioID", + column: x => x.StudioID, + principalTable: "Studios", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Collections_FirstID", + column: x => x.FirstID, + principalTable: "Collections", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, + principalTable: "Libraries", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Genres_SecondID", + column: x => x.SecondID, + principalTable: "Genres", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Shows_FirstID", + column: x => x.FirstID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Users_FirstID", + column: x => x.FirstID, + principalTable: "Users", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PeopleRoles", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PeopleID = table.Column(type: "INTEGER", nullable: false), + ShowID = table.Column(type: "INTEGER", nullable: false), + Role = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PeopleRoles", x => x.ID); + table.ForeignKey( + name: "FK_PeopleRoles_People_PeopleID", + column: x => x.PeopleID, + principalTable: "People", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PeopleRoles_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Seasons", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ShowID = table.Column(type: "INTEGER", nullable: false), + SeasonNumber = table.Column(type: "INTEGER", nullable: false), + Title = table.Column(type: "TEXT", nullable: true), + Overview = table.Column(type: "TEXT", nullable: true), + Year = table.Column(type: "INTEGER", nullable: true), + Poster = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Seasons", x => x.ID); + table.ForeignKey( + name: "FK_Seasons_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Episodes", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ShowID = table.Column(type: "INTEGER", nullable: false), + SeasonID = table.Column(type: "INTEGER", nullable: true), + SeasonNumber = table.Column(type: "INTEGER", nullable: false), + EpisodeNumber = table.Column(type: "INTEGER", nullable: false), + AbsoluteNumber = table.Column(type: "INTEGER", nullable: false), + Path = table.Column(type: "TEXT", nullable: true), + Thumb = table.Column(type: "TEXT", nullable: true), + Title = table.Column(type: "TEXT", nullable: true), + Overview = table.Column(type: "TEXT", nullable: true), + ReleaseDate = table.Column(type: "TEXT", nullable: true), + Runtime = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Episodes", x => x.ID); + table.ForeignKey( + name: "FK_Episodes_Seasons_SeasonID", + column: x => x.SeasonID, + principalTable: "Seasons", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Episodes_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MetadataIds", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProviderID = table.Column(type: "INTEGER", nullable: false), + ShowID = table.Column(type: "INTEGER", nullable: true), + EpisodeID = table.Column(type: "INTEGER", nullable: true), + SeasonID = table.Column(type: "INTEGER", nullable: true), + PeopleID = table.Column(type: "INTEGER", nullable: true), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataIds", x => x.ID); + table.ForeignKey( + name: "FK_MetadataIds_Episodes_EpisodeID", + column: x => x.EpisodeID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_People_PeopleID", + column: x => x.PeopleID, + principalTable: "People", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_Providers_ProviderID", + column: x => x.ProviderID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_Seasons_SeasonID", + column: x => x.SeasonID, + principalTable: "Seasons", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tracks", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EpisodeID = table.Column(type: "INTEGER", nullable: false), + TrackIndex = table.Column(type: "INTEGER", nullable: false), + IsDefault = table.Column(type: "INTEGER", nullable: false), + IsForced = table.Column(type: "INTEGER", nullable: false), + IsExternal = table.Column(type: "INTEGER", nullable: false), + Title = table.Column(type: "TEXT", nullable: true), + Language = table.Column(type: "TEXT", nullable: true), + Codec = table.Column(type: "TEXT", nullable: true), + Path = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tracks", x => x.ID); + table.ForeignKey( + name: "FK_Tracks_Episodes_EpisodeID", + column: x => x.EpisodeID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "WatchedEpisodes", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + WatchedPercentage = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WatchedEpisodes", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_WatchedEpisodes_Episodes_SecondID", + column: x => x.SecondID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_WatchedEpisodes_Users_FirstID", + column: x => x.FirstID, + principalTable: "Users", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Collections_Slug", + table: "Collections", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Episodes_SeasonID", + table: "Episodes", + column: "SeasonID"); + + migrationBuilder.CreateIndex( + name: "IX_Episodes_ShowID_SeasonNumber_EpisodeNumber_AbsoluteNumber", + table: "Episodes", + columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Genres_Slug", + table: "Genres", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Libraries_Slug", + table: "Libraries", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_EpisodeID", + table: "MetadataIds", + column: "EpisodeID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_PeopleID", + table: "MetadataIds", + column: "PeopleID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_ProviderID", + table: "MetadataIds", + column: "ProviderID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_SeasonID", + table: "MetadataIds", + column: "SeasonID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_ShowID", + table: "MetadataIds", + column: "ShowID"); + + migrationBuilder.CreateIndex( + name: "IX_People_Slug", + table: "People", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PeopleRoles_PeopleID", + table: "PeopleRoles", + column: "PeopleID"); + + migrationBuilder.CreateIndex( + name: "IX_PeopleRoles_ShowID", + table: "PeopleRoles", + column: "ShowID"); + + migrationBuilder.CreateIndex( + name: "IX_Providers_Slug", + table: "Providers", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Seasons_ShowID_SeasonNumber", + table: "Seasons", + columns: new[] { "ShowID", "SeasonNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shows_Slug", + table: "Shows", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shows_StudioID", + table: "Shows", + column: "StudioID"); + + migrationBuilder.CreateIndex( + name: "IX_Studios_Slug", + table: "Studios", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced", + table: "Tracks", + columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Slug", + table: "Users", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WatchedEpisodes_SecondID", + table: "WatchedEpisodes", + column: "SecondID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "MetadataIds"); + + migrationBuilder.DropTable( + name: "PeopleRoles"); + + migrationBuilder.DropTable( + name: "Tracks"); + + migrationBuilder.DropTable( + name: "WatchedEpisodes"); + + migrationBuilder.DropTable( + name: "Collections"); + + migrationBuilder.DropTable( + name: "Libraries"); + + migrationBuilder.DropTable( + name: "Genres"); + + migrationBuilder.DropTable( + name: "Providers"); + + migrationBuilder.DropTable( + name: "People"); + + migrationBuilder.DropTable( + name: "Episodes"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Seasons"); + + migrationBuilder.DropTable( + name: "Shows"); + + migrationBuilder.DropTable( + name: "Studios"); + } + } +} diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs new file mode 100644 index 00000000..60379986 --- /dev/null +++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs @@ -0,0 +1,876 @@ +// +using System; +using Kyoo.SqLite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Kyoo.SQLite.Migrations +{ + [DbContext(typeof(SqLiteContext))] + partial class SqLiteContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.6"); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteNumber") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Runtime") + .HasColumnType("INTEGER"); + + b.Property("SeasonID") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Paths") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("EpisodeID") + .HasColumnType("INTEGER"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("PeopleID") + .HasColumnType("INTEGER"); + + b.Property("ProviderID") + .HasColumnType("INTEGER"); + + b.Property("SeasonID") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ProviderID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID"); + + b.ToTable("MetadataIds"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("PeopleID") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("LogoExtension") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Aliases") + .HasColumnType("TEXT"); + + b.Property("Backdrop") + .HasColumnType("TEXT"); + + b.Property("EndYear") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartYear") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StudioID") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrailerUrl") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("EpisodeID") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("ExtraData") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("WatchedPercentage") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID"); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("GenreLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("ExternalIDs") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.People", "People") + .WithMany("ExternalIDs") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Provider", "Provider") + .WithMany("MetadataLinks") + .HasForeignKey("ProviderID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("ExternalIDs") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("ExternalIDs") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Episode"); + + b.Navigation("People"); + + b.Navigation("Provider"); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("MetadataLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.SqLite/SqLiteContext.cs b/Kyoo.SqLite/SqLiteContext.cs new file mode 100644 index 00000000..ca1a651e --- /dev/null +++ b/Kyoo.SqLite/SqLiteContext.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Kyoo.Models; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; + +namespace Kyoo.SqLite +{ + /// + /// A sqlite implementation of . + /// + public class SqLiteContext : DatabaseContext + { + /// + /// The connection string to use. + /// + private readonly string _connection; + + /// + /// Is this instance in debug mode? + /// + private readonly bool _debugMode; + + /// + /// Should the configure step be skipped? This is used when the database is created via DbContextOptions. + /// + private readonly bool _skipConfigure; + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + public SqLiteContext() + { } + + /// + /// Create a new using specific options + /// + /// The options to use. + public SqLiteContext(DbContextOptions options) + : base(options) + { + _skipConfigure = true; + } + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + /// The connection string to use + /// Is this instance in debug mode? + public SqLiteContext(string connection, bool debugMode) + { + _connection = connection; + _debugMode = debugMode; + } + + /// + /// Set connection information for this database context + /// + /// An option builder to fill. + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!_skipConfigure) + { + if (_connection != null) + optionsBuilder.UseSqlite(_connection); + else + optionsBuilder.UseSqlite(); + if (_debugMode) + optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); + } + + base.OnConfiguring(optionsBuilder); + } + + /// + /// Set database parameters to support every types of Kyoo. + /// + /// The database's model builder. + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // modelBuilder.HasPostgresEnum(); + // modelBuilder.HasPostgresEnum(); + // modelBuilder.HasPostgresEnum(); + + ValueConverter arrayConvertor = new( + x => string.Join(";", x), + x => x.Split(';', StringSplitOptions.None)); + modelBuilder.Entity() + .Property(x => x.Paths) + .HasConversion(arrayConvertor); + modelBuilder.Entity() + .Property(x => x.Aliases) + .HasConversion(arrayConvertor); + modelBuilder.Entity() + .Property(x => x.Permissions) + .HasConversion(arrayConvertor); + + modelBuilder.Entity() + .Property(x => x.Status) + .HasConversion(); + modelBuilder.Entity() + .Property(x => x.Type) + .HasConversion(); + + ValueConverter, string> jsonConvertor = new( + x => JsonConvert.SerializeObject(x), + x => JsonConvert.DeserializeObject>(x)); + modelBuilder.Entity() + .Property(x => x.ExtraData) + .HasConversion(jsonConvertor); + + base.OnModelCreating(modelBuilder); + } + + /// + protected override bool IsDuplicateException(Exception ex) + { + return ex.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 /*SQLITE_CONSTRAINT_UNIQUE*/}; + } + + /// + public override Expression> Like(Expression> query, string format) + { + MethodInfo iLike = MethodOfUtils.MethodOf(EF.Functions.Like); + MethodCallExpression call = Expression.Call(iLike, query.Body, Expression.Constant(format)); + + return Expression.Lambda>(call, query.Parameters); + } + } +} \ No newline at end of file diff --git a/Kyoo.SqLite/SqLiteModule.cs b/Kyoo.SqLite/SqLiteModule.cs new file mode 100644 index 00000000..d9815093 --- /dev/null +++ b/Kyoo.SqLite/SqLiteModule.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Kyoo.Controllers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Kyoo.SqLite +{ + /// + /// A module to add sqlite capacity to the app. + /// + public class SqLiteModule : IPlugin + { + /// + public string Slug => "sqlite"; + + /// + public string Name => "SqLite"; + + /// + public string Description => "A database context for sqlite."; + + /// + public ICollection Provides => new[] + { + typeof(DatabaseContext) + }; + + /// + public ICollection ConditionalProvides => ArraySegment.Empty; + + /// + public ICollection Requires => ArraySegment.Empty; + + + /// + /// The configuration to use. The database connection string is pulled from it. + /// + private readonly IConfiguration _configuration; + + /// + /// The host environment to check if the app is in debug mode. + /// + private readonly IWebHostEnvironment _environment; + + /// + /// Create a new postgres module instance and use the given configuration and environment. + /// + /// The configuration to use + /// The environment that will be used (if the env is in development mode, more information will be displayed on errors. + public SqLiteModule(IConfiguration configuration, IWebHostEnvironment env) + { + _configuration = configuration; + _environment = env; + } + + + /// + public void Configure(IServiceCollection services, ICollection availableTypes) + { + services.AddDbContext(x => + { + x.UseSqlite(_configuration.GetDatabaseConnection("sqlite")); + if (_environment.IsDevelopment()) + x.EnableDetailedErrors().EnableSensitiveDataLogging(); + }); + } + } +} \ No newline at end of file diff --git a/Kyoo.sln b/Kyoo.sln index 3f814bd3..60998b55 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Pos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\Kyoo.SqLite.csproj", "{6515380E-1E57-42DA-B6E3-E1C8A848818A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,5 +43,9 @@ Global {7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.Build.0 = Release|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From d05e7a24b71215d3ee473488d05d029fccfdd6d7 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 29 May 2021 15:10:59 +0200 Subject: [PATCH 02/57] Making sqlite work --- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 16 ++++++++-------- Kyoo.SqLite/Kyoo.SqLite.csproj | 18 +++++++++--------- Kyoo.SqLite/SqLiteModule.cs | 11 +++++++++-- Kyoo.Tests/Kyoo.Tests.csproj | 1 - Kyoo/Kyoo.csproj | 3 +++ Kyoo/Startup.cs | 4 +++- Kyoo/settings.json | 4 ++++ 7 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 9dcbba0b..3ef6e4c2 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -8,14 +8,14 @@ default - - ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql - false - false - false - false - true - + + + + + + + + diff --git a/Kyoo.SqLite/Kyoo.SqLite.csproj b/Kyoo.SqLite/Kyoo.SqLite.csproj index c48664bc..43c2634f 100644 --- a/Kyoo.SqLite/Kyoo.SqLite.csproj +++ b/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -6,17 +6,17 @@ Zoe Roux https://github.com/AnonymusRaccoon/Kyoo default - Kyoo.SQLite + Kyoo.SqLite - - ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/sqlite - false - false - false - false - true - + + + + + + + + diff --git a/Kyoo.SqLite/SqLiteModule.cs b/Kyoo.SqLite/SqLiteModule.cs index d9815093..34802b20 100644 --- a/Kyoo.SqLite/SqLiteModule.cs +++ b/Kyoo.SqLite/SqLiteModule.cs @@ -56,8 +56,8 @@ namespace Kyoo.SqLite _configuration = configuration; _environment = env; } - - + + /// public void Configure(IServiceCollection services, ICollection availableTypes) { @@ -68,5 +68,12 @@ namespace Kyoo.SqLite x.EnableDetailedErrors().EnableSensitiveDataLogging(); }); } + + /// + public void Initialize(IServiceProvider provider) + { + DatabaseContext context = provider.GetRequiredService(); + context.Database.Migrate(); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index b5e3dd82..dfb1d2c4 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -14,7 +14,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 22fd6e21..583341f6 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -46,6 +46,9 @@ + + + diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 8bd517cc..27c05c56 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -5,6 +5,7 @@ using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Options; using Kyoo.Postgresql; +using Kyoo.SqLite; using Kyoo.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -46,7 +47,8 @@ namespace Kyoo // TODO remove postgres from here and load it like a normal plugin. _plugins.LoadPlugins(new IPlugin[] { new CoreModule(configuration), - new PostgresModule(configuration, host), + // new PostgresModule(configuration, host), + new SqLiteModule(configuration, host), new AuthenticationModule(configuration, loggerFactory, host) }); } diff --git a/Kyoo/settings.json b/Kyoo/settings.json index ff7dff63..2ff0e24b 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -10,6 +10,10 @@ }, "database": { + "sqlite": { + "data Source": "kyoo.db", + "cache": "Shared" + }, "postgres": { "server": "127.0.0.1", "port": "5432", From 81945b4e370c3e407aa3f356f542fcdf0e716d89 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 29 May 2021 18:28:38 +0200 Subject: [PATCH 03/57] Adding some tests --- Kyoo.Tests/KAssert.cs | 14 +++ Kyoo.Tests/Library/RepositoryTests.cs | 110 ++++++++++++++++++ Kyoo.Tests/Library/SetupTests.cs | 17 --- Kyoo.Tests/Library/TestContext.cs | 160 +++++++++++++------------- Kyoo.Tests/Library/TestSample.cs | 45 ++++++++ 5 files changed, 250 insertions(+), 96 deletions(-) create mode 100644 Kyoo.Tests/KAssert.cs create mode 100644 Kyoo.Tests/Library/RepositoryTests.cs delete mode 100644 Kyoo.Tests/Library/SetupTests.cs create mode 100644 Kyoo.Tests/Library/TestSample.cs diff --git a/Kyoo.Tests/KAssert.cs b/Kyoo.Tests/KAssert.cs new file mode 100644 index 00000000..b6168251 --- /dev/null +++ b/Kyoo.Tests/KAssert.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using Xunit; + +namespace Kyoo.Tests +{ + public static class KAssert + { + public static void DeepEqual(T expected, T value) + { + foreach (PropertyInfo property in typeof(T).GetProperties()) + Assert.Equal(property.GetValue(expected), property.GetValue(value)); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs new file mode 100644 index 00000000..9261d56f --- /dev/null +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; + +namespace Kyoo.Tests +{ + public class RepositoryActivator : IDisposable, IAsyncDisposable + { + public TestContext Context { get; init; } + public ILibraryManager LibraryManager { get; init; } + + + private readonly DatabaseContext _database; + + public RepositoryActivator() + { + Context = new TestContext(); + _database = Context.New(); + + ProviderRepository provider = new(_database); + LibraryRepository library = new(_database, provider); + CollectionRepository collection = new(_database); + GenreRepository genre = new(_database); + StudioRepository studio = new(_database); + PeopleRepository people = new(_database, provider, + new Lazy(() => LibraryManager.ShowRepository)); + ShowRepository show = new(_database, studio, people, genre, provider, + new Lazy(() => LibraryManager.SeasonRepository), + new Lazy(() => LibraryManager.EpisodeRepository)); + SeasonRepository season = new(_database, provider, show, + new Lazy(() => LibraryManager.EpisodeRepository)); + LibraryItemRepository libraryItem = new(_database, + new Lazy(() => LibraryManager.LibraryRepository), + new Lazy(() => LibraryManager.ShowRepository), + new Lazy(() => LibraryManager.CollectionRepository)); + TrackRepository track = new(_database); + EpisodeRepository episode = new(_database, provider, show, track); + + LibraryManager = new LibraryManager(new IBaseRepository[] { + provider, + library, + libraryItem, + collection, + show, + season, + episode, + track, + people, + studio, + genre + }); + + Context.AddTest(); + } + + public void Dispose() + { + _database.Dispose(); + Context.Dispose(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await _database.DisposeAsync(); + await Context.DisposeAsync(); + } + } + + + public abstract class RepositoryTests : IClassFixture + where T : class, IResource + { + private readonly RepositoryActivator _repositories; + private readonly IRepository _repository; + + protected RepositoryTests(RepositoryActivator repositories) + { + _repositories = repositories; + _repository = _repositories.LibraryManager.GetRepository(); + } + + // TODO test libraries & repositories via a on-memory SQLite database. + + [Fact] + public async Task FillTest() + { + await using DatabaseContext database = _repositories.Context.New(); + + Assert.Equal(1, database.Shows.Count()); + } + + [Fact] + public async Task GetByIdTest() + { + T value = await _repository.Get(TestSample.Get().Slug); + KAssert.DeepEqual(TestSample.Get(), value); + } + } + + public class ShowTests : RepositoryTests + { + public ShowTests(RepositoryActivator repositories) + : base(repositories) + {} + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SetupTests.cs b/Kyoo.Tests/Library/SetupTests.cs deleted file mode 100644 index ec9ed12a..00000000 --- a/Kyoo.Tests/Library/SetupTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Kyoo.Tests -{ - public class SetupTests - { - // TODO test libraries & repositories via a on-memory SQLite database. - // TODO Requires: Kyoo should be database agonistic and database implementations should be available via a plugin. - - // [Fact] - // public void Get_Test() - // { - // TestContext context = new(); - // using DatabaseContext database = context.New(); - // - // Assert.Equal(1, database.Shows.Count()); - // } - } -} \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index e3cabc03..50db0b6b 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -1,79 +1,81 @@ -// using Kyoo.Models; -// using Microsoft.Data.Sqlite; -// using Microsoft.EntityFrameworkCore; -// -// namespace Kyoo.Tests -// { -// /// -// /// Class responsible to fill and create in memory databases for unit tests. -// /// -// public class TestContext -// { -// /// -// /// The context's options that specify to use an in memory Sqlite database. -// /// -// private readonly DbContextOptions _context; -// -// /// -// /// Create a new database and fill it with information. -// /// -// public TestContext() -// { -// SqliteConnection connection = new("DataSource=:memory:"); -// connection.Open(); -// -// try -// { -// _context = new DbContextOptionsBuilder() -// .UseSqlite(connection) -// .Options; -// FillDatabase(); -// } -// finally -// { -// connection.Close(); -// } -// } -// -// /// -// /// Fill the database with pre defined values using a clean context. -// /// -// private void FillDatabase() -// { -// using DatabaseContext context = new(_context); -// context.Shows.Add(new Show -// { -// ID = 67, -// Slug = "anohana", -// Title = "Anohana: The Flower We Saw That Day", -// Aliases = new[] -// { -// "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", -// "AnoHana", -// "We Still Don't Know the Name of the Flower We Saw That Day." -// }, -// Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + -// "In time, however, these childhood friends drifted apart, and when they became high " + -// "school students, they had long ceased to think of each other as friends.", -// Status = Status.Finished, -// TrailerUrl = null, -// StartYear = 2011, -// EndYear = 2011, -// Poster = "poster", -// Logo = "logo", -// Backdrop = "backdrop", -// IsMovie = false, -// Studio = null -// }); -// } -// -// /// -// /// Get a new database context connected to a in memory Sqlite database. -// /// -// /// A valid DatabaseContext -// public DatabaseContext New() -// { -// return new(_context); -// } -// } -// } \ No newline at end of file +using System; +using System.Threading.Tasks; +using Kyoo.SqLite; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Kyoo.Tests +{ + /// + /// Class responsible to fill and create in memory databases for unit tests. + /// + public class TestContext : IDisposable, IAsyncDisposable + { + /// + /// The context's options that specify to use an in memory Sqlite database. + /// + private readonly DbContextOptions _context; + + /// + /// The internal sqlite connection used by all context returned by this class. + /// + private readonly SqliteConnection _connection; + + /// + /// Create a new database and fill it with information. + /// + public TestContext() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + _context = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + + using DatabaseContext context = New(); + context.Database.Migrate(); + } + + /// + /// Fill the database with pre defined values using a clean context. + /// + public async Task AddTestAsync() + where T : class + { + await using DatabaseContext context = New(); + await context.Set().AddAsync(TestSample.Get()); + await context.SaveChangesAsync(); + } + + /// + /// Fill the database with pre defined values using a clean context. + /// + public void AddTest() + where T : class + { + using DatabaseContext context = New(); + context.Set().Add(TestSample.Get()); + context.SaveChanges(); + } + + /// + /// Get a new database context connected to a in memory Sqlite database. + /// + /// A valid DatabaseContext + public DatabaseContext New() + { + return new SqLiteContext(_context); + } + + public void Dispose() + { + _connection.Close(); + } + + public async ValueTask DisposeAsync() + { + await _connection.CloseAsync(); + } + } +} diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs new file mode 100644 index 00000000..5642054b --- /dev/null +++ b/Kyoo.Tests/Library/TestSample.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Kyoo.Models; + +namespace Kyoo.Tests +{ + public static class TestSample + { + private static readonly Dictionary Samples = new() + { + { + typeof(Show), + new Show + { + ID = 1, + Slug = "anohana", + Title = "Anohana: The Flower We Saw That Day", + Aliases = new[] + { + "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", + "AnoHana", + "We Still Don't Know the Name of the Flower We Saw That Day." + }, + Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + + "In time, however, these childhood friends drifted apart, and when they became high " + + "school students, they had long ceased to think of each other as friends.", + Status = Status.Finished, + TrailerUrl = null, + StartYear = 2011, + EndYear = 2011, + Poster = "poster", + Logo = "logo", + Backdrop = "backdrop", + IsMovie = false, + Studio = null + } + } + }; + + public static T Get() + { + return (T)Samples[typeof(T)]; + } + } +} \ No newline at end of file From ac07e85f85f4ea1911573ee58c8aa23ada35cfda Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 29 May 2021 19:57:37 +0200 Subject: [PATCH 04/57] Adding more repository tests --- Kyoo.Tests/Library/RepositoryActivator.cs | 70 +++++++++++ Kyoo.Tests/Library/RepositoryTests.cs | 118 ++++++------------ Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 34 +++++ 3 files changed, 143 insertions(+), 79 deletions(-) create mode 100644 Kyoo.Tests/Library/RepositoryActivator.cs create mode 100644 Kyoo.Tests/Library/SpecificTests/ShowTests.cs diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs new file mode 100644 index 00000000..09016c60 --- /dev/null +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; + +namespace Kyoo.Tests +{ + public class RepositoryActivator : IDisposable, IAsyncDisposable + { + public TestContext Context { get; init; } + public ILibraryManager LibraryManager { get; init; } + + + private readonly DatabaseContext _database; + + public RepositoryActivator() + { + Context = new TestContext(); + _database = Context.New(); + + ProviderRepository provider = new(_database); + LibraryRepository library = new(_database, provider); + CollectionRepository collection = new(_database); + GenreRepository genre = new(_database); + StudioRepository studio = new(_database); + PeopleRepository people = new(_database, provider, + new Lazy(() => LibraryManager.ShowRepository)); + ShowRepository show = new(_database, studio, people, genre, provider, + new Lazy(() => LibraryManager.SeasonRepository), + new Lazy(() => LibraryManager.EpisodeRepository)); + SeasonRepository season = new(_database, provider, show, + new Lazy(() => LibraryManager.EpisodeRepository)); + LibraryItemRepository libraryItem = new(_database, + new Lazy(() => LibraryManager.LibraryRepository), + new Lazy(() => LibraryManager.ShowRepository), + new Lazy(() => LibraryManager.CollectionRepository)); + TrackRepository track = new(_database); + EpisodeRepository episode = new(_database, provider, show, track); + + LibraryManager = new LibraryManager(new IBaseRepository[] { + provider, + library, + libraryItem, + collection, + show, + season, + episode, + track, + people, + studio, + genre + }); + + Context.AddTest(); + } + + public void Dispose() + { + _database.Dispose(); + Context.Dispose(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await _database.DisposeAsync(); + await Context.DisposeAsync(); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index 9261d56f..9c350933 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -1,110 +1,70 @@ -using System; using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; +using Kyoo.Models.Exceptions; using Xunit; namespace Kyoo.Tests { - public class RepositoryActivator : IDisposable, IAsyncDisposable - { - public TestContext Context { get; init; } - public ILibraryManager LibraryManager { get; init; } - - - private readonly DatabaseContext _database; - - public RepositoryActivator() - { - Context = new TestContext(); - _database = Context.New(); - - ProviderRepository provider = new(_database); - LibraryRepository library = new(_database, provider); - CollectionRepository collection = new(_database); - GenreRepository genre = new(_database); - StudioRepository studio = new(_database); - PeopleRepository people = new(_database, provider, - new Lazy(() => LibraryManager.ShowRepository)); - ShowRepository show = new(_database, studio, people, genre, provider, - new Lazy(() => LibraryManager.SeasonRepository), - new Lazy(() => LibraryManager.EpisodeRepository)); - SeasonRepository season = new(_database, provider, show, - new Lazy(() => LibraryManager.EpisodeRepository)); - LibraryItemRepository libraryItem = new(_database, - new Lazy(() => LibraryManager.LibraryRepository), - new Lazy(() => LibraryManager.ShowRepository), - new Lazy(() => LibraryManager.CollectionRepository)); - TrackRepository track = new(_database); - EpisodeRepository episode = new(_database, provider, show, track); - - LibraryManager = new LibraryManager(new IBaseRepository[] { - provider, - library, - libraryItem, - collection, - show, - season, - episode, - track, - people, - studio, - genre - }); - - Context.AddTest(); - } - - public void Dispose() - { - _database.Dispose(); - Context.Dispose(); - GC.SuppressFinalize(this); - } - - public async ValueTask DisposeAsync() - { - await _database.DisposeAsync(); - await Context.DisposeAsync(); - } - } - - - public abstract class RepositoryTests : IClassFixture + public abstract class RepositoryTests where T : class, IResource { - private readonly RepositoryActivator _repositories; + protected readonly RepositoryActivator Repositories; private readonly IRepository _repository; protected RepositoryTests(RepositoryActivator repositories) { - _repositories = repositories; - _repository = _repositories.LibraryManager.GetRepository(); + Repositories = repositories; + _repository = Repositories.LibraryManager.GetRepository(); } - - // TODO test libraries & repositories via a on-memory SQLite database. - + [Fact] public async Task FillTest() { - await using DatabaseContext database = _repositories.Context.New(); + await using DatabaseContext database = Repositories.Context.New(); Assert.Equal(1, database.Shows.Count()); } [Fact] public async Task GetByIdTest() + { + T value = await _repository.Get(TestSample.Get().ID); + KAssert.DeepEqual(TestSample.Get(), value); + } + + [Fact] + public async Task GetBySlugTest() { T value = await _repository.Get(TestSample.Get().Slug); KAssert.DeepEqual(TestSample.Get(), value); } - } + + [Fact] + public async Task GetByFakeIdTest() + { + await Assert.ThrowsAsync(() => _repository.Get(2)); + } + + [Fact] + public async Task GetByFakeSlugTest() + { + await Assert.ThrowsAsync(() => _repository.Get("non-existent")); + } - public class ShowTests : RepositoryTests - { - public ShowTests(RepositoryActivator repositories) - : base(repositories) - {} + [Fact] + public async Task DeleteByIdTest() + { + await _repository.Delete(TestSample.Get().ID); + Assert.Equal(0, await _repository.GetCount()); + } + + [Fact] + public async Task DeleteBySlugTest() + { + await _repository.Delete(TestSample.Get().Slug); + Assert.Equal(0, await _repository.GetCount()); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs new file mode 100644 index 00000000..43c7fb39 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Kyoo.Tests.SpecificTests +{ + public class ShowTests : RepositoryTests + { + private readonly IShowRepository _repository; + + public ShowTests() + : base(new RepositoryActivator()) + { + _repository = Repositories.LibraryManager.ShowRepository; + } + // + // [Fact] + // public async Task EditTest() + // { + // Show value = await _repository.Get(TestSample.Get().Slug); + // value.Path = "/super"; + // value.Title = "New Title"; + // Show edited = await _repository.Edit(value, false); + // KAssert.DeepEqual(value, edited); + // + // await using DatabaseContext database = Repositories.Context.New(); + // Show show = await database.Shows.FirstAsync(); + // + // KAssert.DeepEqual(show, value); + // } + } +} \ No newline at end of file From 6763b2c0d17d812fc3896ae1c27783abdf818a34 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 29 May 2021 22:06:33 +0200 Subject: [PATCH 05/57] Splitting Utility in multiple classes --- Kyoo.Common/Utility.cs | 703 ------------------ Kyoo.Common/Utility/EnumerableExtensions.cs | 247 ++++++ Kyoo.Common/Utility/Merger.cs | 178 +++++ Kyoo.Common/Utility/Utility.cs | 325 ++++++++ Kyoo.CommonAPI/LocalRepository.cs | 4 +- Kyoo.Tests/{ => Utility}/UtilityTests.cs | 13 +- Kyoo/Controllers/ProviderManager.cs | 2 +- .../Repositories/EpisodeRepository.cs | 2 +- 8 files changed, 766 insertions(+), 708 deletions(-) delete mode 100644 Kyoo.Common/Utility.cs create mode 100644 Kyoo.Common/Utility/EnumerableExtensions.cs create mode 100644 Kyoo.Common/Utility/Merger.cs create mode 100644 Kyoo.Common/Utility/Utility.cs rename Kyoo.Tests/{ => Utility}/UtilityTests.cs (56%) diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs deleted file mode 100644 index 743cf545..00000000 --- a/Kyoo.Common/Utility.cs +++ /dev/null @@ -1,703 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Kyoo.Models.Attributes; - -namespace Kyoo -{ - /// - /// A set of utility functions that can be used everywhere. - /// - public static class Utility - { - /// - /// Is the lambda expression a member (like x => x.Body). - /// - /// The expression that should be checked - /// True if the expression is a member, false otherwise - public static bool IsPropertyExpression(LambdaExpression ex) - { - return ex == null || - ex.Body is MemberExpression || - ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; - } - - /// - /// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows) - /// - /// The expression - /// The name of the expression - /// If the expression is not a property, ArgumentException is thrown. - public static string GetPropertyName(LambdaExpression ex) - { - if (!IsPropertyExpression(ex)) - throw new ArgumentException($"{ex} is not a property expression."); - MemberExpression member = ex.Body.NodeType == ExpressionType.Convert - ? ((UnaryExpression)ex.Body).Operand as MemberExpression - : ex.Body as MemberExpression; - return member!.Member.Name; - } - - /// - /// Get the value of a member (property or field) - /// - /// The member value - /// The owner of this member - /// The value boxed as an object - /// if or is null. - /// The member is not a field or a property. - public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj) - { - if (member == null) - throw new ArgumentNullException(nameof(member)); - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - return member switch - { - PropertyInfo property => property.GetValue(obj), - FieldInfo field => field.GetValue(obj), - _ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).") - }; - } - - /// - /// Slugify a string (Replace spaces by -, Uniformize accents é -> e) - /// - /// The string to slugify - /// The slug version of the given string - public static string ToSlug(string str) - { - if (str == null) - return null; - - str = str.ToLowerInvariant(); - - string normalizedString = str.Normalize(NormalizationForm.FormD); - StringBuilder stringBuilder = new(); - foreach (char c in normalizedString) - { - UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); - if (unicodeCategory != UnicodeCategory.NonSpacingMark) - stringBuilder.Append(c); - } - str = stringBuilder.ToString().Normalize(NormalizationForm.FormC); - - str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled); - str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); - str = str.Trim('-', '_'); - str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled); - return str; - } - - /// - /// Merge two lists, can keep duplicates or remove them. - /// - /// The first enumerable to merge - /// The second enumerable to merge, if items from this list are equals to one from the first, they are not kept - /// Equality function to compare items. If this is null, duplicated elements are kept - /// The two list merged as an array - public static T[] MergeLists(IEnumerable first, - IEnumerable second, - Func isEqual = null) - { - if (first == null) - return second.ToArray(); - if (second == null) - return first.ToArray(); - if (isEqual == null) - return first.Concat(second).ToArray(); - List list = first.ToList(); - return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray(); - } - - /// - /// Set every fields of first to those of second. Ignore fields marked with the attribute - /// At the end, the OnMerge method of first will be called if first is a - /// - /// The object to assign - /// The object containing new values - /// Fields of T will be used - /// - public static T Assign(T first, T second) - { - Type type = typeof(T); - IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); - - foreach (PropertyInfo property in properties) - { - object value = property.GetValue(second); - property.SetValue(first, value); - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } - - /// - /// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}. - /// At the end, the OnMerge method of first will be called if first is a - /// - /// The object to complete - /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. - /// Filter fields that will be merged - /// Fields of T will be completed - /// - /// If first is null - public static T Complete([NotNull] T first, [CanBeNull] T second, Func where = null) - { - if (first == null) - throw new ArgumentNullException(nameof(first)); - if (second == null) - return first; - - Type type = typeof(T); - IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); - - if (where != null) - properties = properties.Where(where); - - foreach (PropertyInfo property in properties) - { - object value = property.GetValue(second); - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - - if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) - property.SetValue(first, value); - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } - - /// - /// An advanced function. - /// This will set missing values of to the corresponding values of . - /// Enumerable will be merged (concatenated). - /// At the end, the OnMerge method of first will be called if first is a . - /// - /// The object to complete - /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. - /// Fields of T will be merged - /// - public static T Merge(T first, T second) - { - if (first == null) - return second; - if (second == null) - return first; - - Type type = typeof(T); - IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); - - foreach (PropertyInfo property in properties) - { - object oldValue = property.GetValue(first); - object newValue = property.GetValue(second); - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - - if (oldValue?.Equals(defaultValue) != false) - property.SetValue(first, newValue); - else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) - && property.PropertyType != typeof(string)) - { - property.SetValue(first, RunGenericMethod( - typeof(Utility), - nameof(MergeLists), - GetEnumerableType(property.PropertyType), - oldValue, newValue, null)); - } - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } - - /// - /// Set every fields of to the default value. - /// - /// The object to nullify - /// Fields of T will be nullified - /// - public static T Nullify(T obj) - { - Type type = typeof(T); - foreach (PropertyInfo property in type.GetProperties()) - { - if (!property.CanWrite) - continue; - - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - property.SetValue(obj, defaultValue); - } - - return obj; - } - - /// - /// Return every in the inheritance tree of the parameter (interfaces are not returned) - /// - /// The starting type - /// A list of types - /// can't be null - public static IEnumerable GetInheritanceTree([NotNull] this Type type) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - for (; type != null; type = type.BaseType) - yield return type; - } - - /// - /// Check if inherit from a generic type . - /// - /// Does this object's type is a - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// True if obj inherit from genericType. False otherwise - /// obj and genericType can't be null - public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - return IsOfGenericType(obj.GetType(), genericType); - } - - /// - /// Check if inherit from a generic type . - /// - /// The type to check - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// True if obj inherit from genericType. False otherwise - /// obj and genericType can't be null - public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (genericType == null) - throw new ArgumentNullException(nameof(genericType)); - if (!genericType.IsGenericType) - throw new ArgumentException($"{nameof(genericType)} is not a generic type."); - - IEnumerable types = genericType.IsInterface - ? type.GetInterfaces() - : type.GetInheritanceTree(); - return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); - } - - /// - /// Get the generic definition of . - /// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string> - /// - /// The type to check - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// The generic definition of genericType that type inherit or null if type does not implement the generic type. - /// and can't be null - /// must be a generic type - public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (genericType == null) - throw new ArgumentNullException(nameof(genericType)); - if (!genericType.IsGenericType) - throw new ArgumentException($"{nameof(genericType)} is not a generic type."); - - IEnumerable types = genericType.IsInterface - ? type.GetInterfaces() - : type.GetInheritanceTree(); - return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); - } - - /// - /// A Select where the index of the item can be used. - /// - /// The IEnumerable to map. If self is null, an empty list is returned - /// The function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped. - /// mapper can't be null - public static IEnumerable Map([CanBeNull] this IEnumerable self, - [NotNull] Func mapper) - { - if (self == null) - yield break; - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - int index = 0; - - while (enumerator.MoveNext()) - { - yield return mapper(enumerator.Current, index); - index++; - } - } - - /// - /// A map where the mapping function is asynchronous. - /// Note: might interest you. - /// - /// The IEnumerable to map. If self is null, an empty list is returned - /// The asynchronous function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped as an AsyncEnumerable - /// mapper can't be null - public static async IAsyncEnumerable MapAsync([CanBeNull] this IEnumerable self, - [NotNull] Func> mapper) - { - if (self == null) - yield break; - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - int index = 0; - - while (enumerator.MoveNext()) - { - yield return await mapper(enumerator.Current, index); - index++; - } - } - - /// - /// An asynchronous version of Select. - /// - /// The IEnumerable to map - /// The asynchronous function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped as an AsyncEnumerable - /// mapper can't be null - public static async IAsyncEnumerable SelectAsync([CanBeNull] this IEnumerable self, - [NotNull] Func> mapper) - { - if (self == null) - yield break; - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - - while (enumerator.MoveNext()) - yield return await mapper(enumerator.Current); - } - - /// - /// Convert an AsyncEnumerable to a List by waiting for every item. - /// - /// The async list - /// The type of items in the async list and in the returned list. - /// A task that will return a simple list - /// The list can't be null - public static async Task> ToListAsync([NotNull] this IAsyncEnumerable self) - { - if (self == null) - throw new ArgumentNullException(nameof(self)); - - List ret = new(); - - await foreach(T i in self) - ret.Add(i); - return ret; - } - - /// - /// If the enumerable is empty, execute an action. - /// - /// The enumerable to check - /// The action to execute is the list is empty - /// The type of items inside the list - /// - public static IEnumerable IfEmpty(this IEnumerable self, Action action) - { - using IEnumerator enumerator = self.GetEnumerator(); - - if (!enumerator.MoveNext()) - { - action(); - yield break; - } - - do - { - yield return enumerator.Current; - } - while (enumerator.MoveNext()); - } - - /// - /// A foreach used as a function with a little specificity: the list can be null. - /// - /// The list to enumerate. If this is null, the function result in a no-op - /// The action to execute for each arguments - /// The type of items in the list - public static void ForEach([CanBeNull] this IEnumerable self, Action action) - { - if (self == null) - return; - foreach (T i in self) - action(i); - } - - /// - /// A foreach used as a function with a little specificity: the list can be null. - /// - /// The list to enumerate. If this is null, the function result in a no-op - /// The action to execute for each arguments - public static void ForEach([CanBeNull] this IEnumerable self, Action action) - { - if (self == null) - return; - foreach (object i in self) - action(i); - } - - public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) - { - if (self == null) - return; - foreach (T i in self) - await action(i); - } - - public static async Task ForEachAsync([CanBeNull] this IAsyncEnumerable self, Action action) - { - if (self == null) - return; - await foreach (T i in self) - action(i); - } - - public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) - { - if (self == null) - return; - foreach (object i in self) - await action(i); - } - - public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args) - { - MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) - .Where(x => x.Name == name) - .Where(x => x.GetGenericArguments().Length == generics.Length) - .Where(x => x.GetParameters().Length == args.Length) - .IfEmpty(() => throw new NullReferenceException($"A method named {name} with " + - $"{args.Length} arguments and {generics.Length} generic " + - $"types could not be found on {type.Name}.")) - // TODO this won't work but I don't know why. - // .Where(x => - // { - // int i = 0; - // return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++])); - // }) - // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified.")) - - // TODO this won't work for Type because T is specified in arguments but not in the parameters type. - // .Where(x => - // { - // int i = 0; - // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++])); - // }) - // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types.")) - .Take(2) - .ToArray(); - - if (methods.Length == 1) - return methods[0]; - throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints."); - } - - public static T RunGenericMethod( - [NotNull] Type owner, - [NotNull] string methodName, - [NotNull] Type type, - params object[] args) - { - return RunGenericMethod(owner, methodName, new[] {type}, args); - } - - public static T RunGenericMethod( - [NotNull] Type owner, - [NotNull] string methodName, - [NotNull] Type[] types, - params object[] args) - { - if (owner == null) - throw new ArgumentNullException(nameof(owner)); - if (methodName == null) - throw new ArgumentNullException(nameof(methodName)); - if (types == null) - throw new ArgumentNullException(nameof(types)); - if (types.Length < 1) - throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed."); - MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args); - return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray()); - } - - public static T RunGenericMethod( - [NotNull] object instance, - [NotNull] string methodName, - [NotNull] Type type, - params object[] args) - { - return RunGenericMethod(instance, methodName, new[] {type}, args); - } - - public static T RunGenericMethod( - [NotNull] object instance, - [NotNull] string methodName, - [NotNull] Type[] types, - params object[] args) - { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); - if (methodName == null) - throw new ArgumentNullException(nameof(methodName)); - if (types == null || types.Length == 0) - throw new ArgumentNullException(nameof(types)); - MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args); - return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray()); - } - - [NotNull] - public static Type GetEnumerableType([NoEnumeration] [NotNull] IEnumerable list) - { - if (list == null) - throw new ArgumentNullException(nameof(list)); - Type type = list.GetType().GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) - && t.GetGenericArguments().Any()) ?? list.GetType(); - return type.GetGenericArguments().First(); - } - - public static Type GetEnumerableType([NotNull] Type listType) - { - if (listType == null) - throw new ArgumentNullException(nameof(listType)); - if (!typeof(IEnumerable).IsAssignableFrom(listType)) - throw new InvalidOperationException($"The {nameof(listType)} parameter was not an IEnumerable."); - Type type = listType.GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) - && t.GetGenericArguments().Any()) ?? listType; - return type.GetGenericArguments().First(); - } - - public static IEnumerable> BatchBy(this List list, int countPerList) - { - for (int i = 0; i < list.Count; i += countPerList) - yield return list.GetRange(i, Math.Min(list.Count - i, countPerList)); - } - - public static IEnumerable BatchBy(this IEnumerable list, int countPerList) - { - T[] ret = new T[countPerList]; - int i = 0; - - using IEnumerator enumerator = list.GetEnumerator(); - while (enumerator.MoveNext()) - { - ret[i] = enumerator.Current; - i++; - if (i < countPerList) - continue; - i = 0; - yield return ret; - } - - Array.Resize(ref ret, i); - yield return ret; - } - - public static string ToQueryString(this Dictionary query) - { - if (!query.Any()) - return string.Empty; - return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); - } - - [System.Diagnostics.CodeAnalysis.DoesNotReturn] - public static void ReThrow([NotNull] this Exception ex) - { - if (ex == null) - throw new ArgumentNullException(nameof(ex)); - ExceptionDispatchInfo.Capture(ex).Throw(); - } - - public static Task Then(this Task task, Action map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - map(x.Result); - return x.Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Map(this Task task, Func map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return map(x.Result); - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Cast(this Task task) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return (T)((dynamic)x).Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - /// - /// Get a friendly type name (supporting generics) - /// For example a list of string will be displayed as List<string> and not as List`1. - /// - /// The type to use - /// The friendly name of the type - public static string FriendlyName(this Type type) - { - if (!type.IsGenericType) - return type.Name; - string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName())); - return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>"; - } - } -} \ No newline at end of file diff --git a/Kyoo.Common/Utility/EnumerableExtensions.cs b/Kyoo.Common/Utility/EnumerableExtensions.cs new file mode 100644 index 00000000..e4d0379e --- /dev/null +++ b/Kyoo.Common/Utility/EnumerableExtensions.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A set of extensions class for enumerable. + /// + public static class EnumerableExtensions + { + /// + /// A Select where the index of the item can be used. + /// + /// The IEnumerable to map. If self is null, an empty list is returned + /// The function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped or null if the input map was null. + /// mapper can't be null + public static IEnumerable Map([CanBeNull] this IEnumerable self, + [NotNull] Func mapper) + { + if (self == null) + return null; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + static IEnumerable Generator(IEnumerable self, Func mapper) + { + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return mapper(enumerator.Current, index); + index++; + } + } + return Generator(self, mapper); + } + + /// + /// A map where the mapping function is asynchronous. + /// Note: might interest you. + /// + /// The IEnumerable to map. If self is null, an empty list is returned + /// The asynchronous function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped as an AsyncEnumerable + /// mapper can't be null + public static async IAsyncEnumerable MapAsync([CanBeNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return await mapper(enumerator.Current, index); + index++; + } + } + + /// + /// An asynchronous version of Select. + /// + /// The IEnumerable to map + /// The asynchronous function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped as an AsyncEnumerable + /// mapper can't be null + public static async IAsyncEnumerable SelectAsync([CanBeNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + + while (enumerator.MoveNext()) + yield return await mapper(enumerator.Current); + } + + /// + /// Convert an AsyncEnumerable to a List by waiting for every item. + /// + /// The async list + /// The type of items in the async list and in the returned list. + /// A task that will return a simple list + /// The list can't be null + public static async Task> ToListAsync([NotNull] this IAsyncEnumerable self) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + + List ret = new(); + + await foreach(T i in self) + ret.Add(i); + return ret; + } + + /// + /// If the enumerable is empty, execute an action. + /// + /// The enumerable to check + /// The action to execute is the list is empty + /// The type of items inside the list + /// + public static IEnumerable IfEmpty(this IEnumerable self, Action action) + { + using IEnumerator enumerator = self.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + action(); + yield break; + } + + do + { + yield return enumerator.Current; + } + while (enumerator.MoveNext()); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + /// The type of items in the list + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (T i in self) + action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (object i in self) + action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (object i in self) + await action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The asynchronous action to execute for each arguments + /// The type of items in the list. + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (T i in self) + await action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The async list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + /// The type of items in the list. + public static async Task ForEachAsync([CanBeNull] this IAsyncEnumerable self, Action action) + { + if (self == null) + return; + await foreach (T i in self) + action(i); + } + + /// + /// Split a list in a small chunk of data. + /// + /// The list to split + /// The number of items in each chunk + /// The type of data in the initial list. + /// A list of chunks + public static IEnumerable> BatchBy(this List list, int countPerList) + { + for (int i = 0; i < list.Count; i += countPerList) + yield return list.GetRange(i, Math.Min(list.Count - i, countPerList)); + } + + /// + /// Split a list in a small chunk of data. + /// + /// The list to split + /// The number of items in each chunk + /// The type of data in the initial list. + /// A list of chunks + public static IEnumerable BatchBy(this IEnumerable list, int countPerList) + { + T[] ret = new T[countPerList]; + int i = 0; + + using IEnumerator enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + ret[i] = enumerator.Current; + i++; + if (i < countPerList) + continue; + i = 0; + yield return ret; + } + + Array.Resize(ref ret, i); + yield return ret; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs new file mode 100644 index 00000000..047b5b09 --- /dev/null +++ b/Kyoo.Common/Utility/Merger.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; +using Kyoo.Models.Attributes; + +namespace Kyoo +{ + /// + /// A class containing helper methods to merge objects. + /// + public static class Merger + { + /// + /// Merge two lists, can keep duplicates or remove them. + /// + /// The first enumerable to merge + /// The second enumerable to merge, if items from this list are equals to one from the first, they are not kept + /// Equality function to compare items. If this is null, duplicated elements are kept + /// The two list merged as an array + public static T[] MergeLists(IEnumerable first, + IEnumerable second, + Func isEqual = null) + { + if (first == null) + return second.ToArray(); + if (second == null) + return first.ToArray(); + if (isEqual == null) + return first.Concat(second).ToArray(); + List list = first.ToList(); + return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray(); + } + + /// + /// Set every fields of first to those of second. Ignore fields marked with the attribute + /// At the end, the OnMerge method of first will be called if first is a + /// + /// The object to assign + /// The object containing new values + /// Fields of T will be used + /// + public static T Assign(T first, T second) + { + Type type = typeof(T); + IEnumerable properties = type.GetProperties() + .Where(x => x.CanRead && x.CanWrite + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); + + foreach (PropertyInfo property in properties) + { + object value = property.GetValue(second); + property.SetValue(first, value); + } + + if (first is IOnMerge merge) + merge.OnMerge(second); + return first; + } + + /// + /// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}. + /// At the end, the OnMerge method of first will be called if first is a + /// + /// The object to complete + /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. + /// Filter fields that will be merged + /// Fields of T will be completed + /// + /// If first is null + public static T Complete([NotNull] T first, [CanBeNull] T second, Func where = null) + { + if (first == null) + throw new ArgumentNullException(nameof(first)); + if (second == null) + return first; + + Type type = typeof(T); + IEnumerable properties = type.GetProperties() + .Where(x => x.CanRead && x.CanWrite + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); + + if (where != null) + properties = properties.Where(where); + + foreach (PropertyInfo property in properties) + { + object value = property.GetValue(second); + object defaultValue = property.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + + if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) + property.SetValue(first, value); + } + + if (first is IOnMerge merge) + merge.OnMerge(second); + return first; + } + + /// + /// An advanced function. + /// This will set missing values of to the corresponding values of . + /// Enumerable will be merged (concatenated). + /// At the end, the OnMerge method of first will be called if first is a . + /// + /// The object to complete + /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. + /// Fields of T will be merged + /// + public static T Merge(T first, T second) + { + if (first == null) + return second; + if (second == null) + return first; + + Type type = typeof(T); + IEnumerable properties = type.GetProperties() + .Where(x => x.CanRead && x.CanWrite + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); + + foreach (PropertyInfo property in properties) + { + object oldValue = property.GetValue(first); + object newValue = property.GetValue(second); + object defaultValue = property.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + + if (oldValue?.Equals(defaultValue) != false) + property.SetValue(first, newValue); + else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) + && property.PropertyType != typeof(string)) + { + Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>)) + .GenericTypeArguments + .First(); + property.SetValue(first, Utility.RunGenericMethod( + typeof(Utility), + nameof(MergeLists), + enumerableType, + oldValue, newValue, null)); + } + } + + if (first is IOnMerge merge) + merge.OnMerge(second); + return first; + } + + /// + /// Set every fields of to the default value. + /// + /// The object to nullify + /// Fields of T will be nullified + /// + public static T Nullify(T obj) + { + Type type = typeof(T); + foreach (PropertyInfo property in type.GetProperties()) + { + if (!property.CanWrite) + continue; + + object defaultValue = property.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + property.SetValue(obj, defaultValue); + } + + return obj; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs new file mode 100644 index 00000000..e25b595b --- /dev/null +++ b/Kyoo.Common/Utility/Utility.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A set of utility functions that can be used everywhere. + /// + public static class Utility + { + /// + /// Is the lambda expression a member (like x => x.Body). + /// + /// The expression that should be checked + /// True if the expression is a member, false otherwise + public static bool IsPropertyExpression(LambdaExpression ex) + { + if (ex == null) + return false; + return ex.Body is MemberExpression || + ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; + } + + /// + /// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows) + /// + /// The expression + /// The name of the expression + /// If the expression is not a property, ArgumentException is thrown. + public static string GetPropertyName(LambdaExpression ex) + { + if (!IsPropertyExpression(ex)) + throw new ArgumentException($"{ex} is not a property expression."); + MemberExpression member = ex.Body.NodeType == ExpressionType.Convert + ? ((UnaryExpression)ex.Body).Operand as MemberExpression + : ex.Body as MemberExpression; + return member!.Member.Name; + } + + /// + /// Get the value of a member (property or field) + /// + /// The member value + /// The owner of this member + /// The value boxed as an object + /// if or is null. + /// The member is not a field or a property. + public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj) + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + return member switch + { + PropertyInfo property => property.GetValue(obj), + FieldInfo field => field.GetValue(obj), + _ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).") + }; + } + + /// + /// Slugify a string (Replace spaces by -, Uniformize accents é -> e) + /// + /// The string to slugify + /// The slug version of the given string + public static string ToSlug(string str) + { + if (str == null) + return null; + + str = str.ToLowerInvariant(); + + string normalizedString = str.Normalize(NormalizationForm.FormD); + StringBuilder stringBuilder = new(); + foreach (char c in normalizedString) + { + UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + stringBuilder.Append(c); + } + str = stringBuilder.ToString().Normalize(NormalizationForm.FormC); + + str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled); + str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); + str = str.Trim('-', '_'); + str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled); + return str; + } + + /// + /// Return every in the inheritance tree of the parameter (interfaces are not returned) + /// + /// The starting type + /// A list of types + /// can't be null + public static IEnumerable GetInheritanceTree([NotNull] this Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + for (; type != null; type = type.BaseType) + yield return type; + } + + /// + /// Check if inherit from a generic type . + /// + /// Does this object's type is a + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// True if obj inherit from genericType. False otherwise + /// obj and genericType can't be null + public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + return IsOfGenericType(obj.GetType(), genericType); + } + + /// + /// Check if inherit from a generic type . + /// + /// The type to check + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// True if obj inherit from genericType. False otherwise + /// obj and genericType can't be null + public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + if (!genericType.IsGenericType) + throw new ArgumentException($"{nameof(genericType)} is not a generic type."); + + IEnumerable types = genericType.IsInterface + ? type.GetInterfaces() + : type.GetInheritanceTree(); + return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); + } + + /// + /// Get the generic definition of . + /// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string> + /// + /// The type to check + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// The generic definition of genericType that type inherit or null if type does not implement the generic type. + /// and can't be null + /// must be a generic type + public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + if (!genericType.IsGenericType) + throw new ArgumentException($"{nameof(genericType)} is not a generic type."); + + IEnumerable types = genericType.IsInterface + ? type.GetInterfaces() + : type.GetInheritanceTree(); + return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); + } + + public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args) + { + MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) + .Where(x => x.Name == name) + .Where(x => x.GetGenericArguments().Length == generics.Length) + .Where(x => x.GetParameters().Length == args.Length) + .IfEmpty(() => throw new NullReferenceException($"A method named {name} with " + + $"{args.Length} arguments and {generics.Length} generic " + + $"types could not be found on {type.Name}.")) + // TODO this won't work but I don't know why. + // .Where(x => + // { + // int i = 0; + // return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++])); + // }) + // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified.")) + + // TODO this won't work for Type because T is specified in arguments but not in the parameters type. + // .Where(x => + // { + // int i = 0; + // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++])); + // }) + // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types.")) + .Take(2) + .ToArray(); + + if (methods.Length == 1) + return methods[0]; + throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints."); + } + + public static T RunGenericMethod( + [NotNull] Type owner, + [NotNull] string methodName, + [NotNull] Type type, + params object[] args) + { + return RunGenericMethod(owner, methodName, new[] {type}, args); + } + + public static T RunGenericMethod( + [NotNull] Type owner, + [NotNull] string methodName, + [NotNull] Type[] types, + params object[] args) + { + if (owner == null) + throw new ArgumentNullException(nameof(owner)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + if (types == null) + throw new ArgumentNullException(nameof(types)); + if (types.Length < 1) + throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed."); + MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args); + return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray()); + } + + public static T RunGenericMethod( + [NotNull] object instance, + [NotNull] string methodName, + [NotNull] Type type, + params object[] args) + { + return RunGenericMethod(instance, methodName, new[] {type}, args); + } + + public static T RunGenericMethod( + [NotNull] object instance, + [NotNull] string methodName, + [NotNull] Type[] types, + params object[] args) + { + if (instance == null) + throw new ArgumentNullException(nameof(instance)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + if (types == null || types.Length == 0) + throw new ArgumentNullException(nameof(types)); + MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args); + return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray()); + } + + public static string ToQueryString(this Dictionary query) + { + if (!query.Any()) + return string.Empty; + return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); + } + + [System.Diagnostics.CodeAnalysis.DoesNotReturn] + public static void ReThrow([NotNull] this Exception ex) + { + if (ex == null) + throw new ArgumentNullException(nameof(ex)); + ExceptionDispatchInfo.Capture(ex).Throw(); + } + + public static Task Then(this Task task, Action map) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + map(x.Result); + return x.Result; + }, TaskContinuationOptions.ExecuteSynchronously); + } + + public static Task Map(this Task task, Func map) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + return map(x.Result); + }, TaskContinuationOptions.ExecuteSynchronously); + } + + public static Task Cast(this Task task) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + return (T)((dynamic)x).Result; + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// Get a friendly type name (supporting generics) + /// For example a list of string will be displayed as List<string> and not as List`1. + /// + /// The type to use + /// The friendly name of the type + public static string FriendlyName(this Type type) + { + if (!type.IsGenericType) + return type.Name; + string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName())); + return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>"; + } + } +} \ No newline at end of file diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 7498fc14..d63c1b06 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -225,8 +225,8 @@ namespace Kyoo.Controllers T old = await GetWithTracking(edited.ID); if (resetOld) - Utility.Nullify(old); - Utility.Complete(old, edited, x => x.GetCustomAttribute() == null); + Merger.Nullify(old); + Merger.Complete(old, edited, x => x.GetCustomAttribute() == null); await EditRelations(old, edited, resetOld); await Database.SaveChangesAsync(); return old; diff --git a/Kyoo.Tests/UtilityTests.cs b/Kyoo.Tests/Utility/UtilityTests.cs similarity index 56% rename from Kyoo.Tests/UtilityTests.cs rename to Kyoo.Tests/Utility/UtilityTests.cs index e046cdb9..15469411 100644 --- a/Kyoo.Tests/UtilityTests.cs +++ b/Kyoo.Tests/Utility/UtilityTests.cs @@ -13,12 +13,23 @@ namespace Kyoo.Tests Expression> member = x => x.ID; Expression> memberCast = x => x.ID; - Assert.True(Utility.IsPropertyExpression(null)); + Assert.False(Utility.IsPropertyExpression(null)); Assert.True(Utility.IsPropertyExpression(member)); Assert.True(Utility.IsPropertyExpression(memberCast)); Expression> call = x => x.GetID("test"); Assert.False(Utility.IsPropertyExpression(call)); } + + [Fact] + public void GetPropertyName_Test() + { + Expression> member = x => x.ID; + Expression> memberCast = x => x.ID; + + Assert.Equal("ID", Utility.GetPropertyName(member)); + Assert.Equal("ID", Utility.GetPropertyName(memberCast)); + Assert.Throws(() => Utility.GetPropertyName(null)); + } } } \ No newline at end of file diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 61f41593..4a0a0cd1 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -29,7 +29,7 @@ namespace Kyoo.Controllers { try { - ret = Utility.Merge(ret, await providerCall(provider)); + ret = Merger.Merge(ret, await providerCall(provider)); } catch (Exception ex) { await Console.Error.WriteLineAsync( diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 8452b950..6487e6b1 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -213,7 +213,7 @@ namespace Kyoo.Controllers /// Set track's index and ensure that every tracks is well-formed. /// /// The resource to fix. - /// The parameter is returnned. + /// The parameter is returned. private async Task ValidateTracks(Episode resource) { resource.Tracks = await resource.Tracks.MapAsync((x, i) => From d6630f29eadb5ead891e226c09ff20071376cf22 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 30 May 2021 19:04:51 +0200 Subject: [PATCH 06/57] Adding tests for enumerables --- Kyoo.Common/Utility/EnumerableExtensions.cs | 105 ++++++++++++-------- Kyoo.Tests/KAssert.cs | 27 +++++ Kyoo.Tests/Utility/EnumerableTests.cs | 71 +++++++++++++ 3 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 Kyoo.Tests/Utility/EnumerableTests.cs diff --git a/Kyoo.Common/Utility/EnumerableExtensions.cs b/Kyoo.Common/Utility/EnumerableExtensions.cs index e4d0379e..459d5f1c 100644 --- a/Kyoo.Common/Utility/EnumerableExtensions.cs +++ b/Kyoo.Common/Utility/EnumerableExtensions.cs @@ -18,13 +18,13 @@ namespace Kyoo /// The function that will map each items /// The type of items in /// The type of items in the returned list - /// The list mapped or null if the input map was null. - /// mapper can't be null - public static IEnumerable Map([CanBeNull] this IEnumerable self, + /// The list mapped. + /// The list or the mapper can't be null + public static IEnumerable Map([NotNull] this IEnumerable self, [NotNull] Func mapper) { if (self == null) - return null; + throw new ArgumentNullException(nameof(self)); if (mapper == null) throw new ArgumentNullException(nameof(mapper)); @@ -46,28 +46,33 @@ namespace Kyoo /// A map where the mapping function is asynchronous. /// Note: might interest you. /// - /// The IEnumerable to map. If self is null, an empty list is returned + /// The IEnumerable to map. /// The asynchronous function that will map each items /// The type of items in /// The type of items in the returned list /// The list mapped as an AsyncEnumerable - /// mapper can't be null - public static async IAsyncEnumerable MapAsync([CanBeNull] this IEnumerable self, + /// The list or the mapper can't be null + public static IAsyncEnumerable MapAsync([NotNull] this IEnumerable self, [NotNull] Func> mapper) { if (self == null) - yield break; + throw new ArgumentNullException(nameof(self)); if (mapper == null) throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - int index = 0; - while (enumerator.MoveNext()) + static async IAsyncEnumerable Generator(IEnumerable self, Func> mapper) { - yield return await mapper(enumerator.Current, index); - index++; + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return await mapper(enumerator.Current, index); + index++; + } } + + return Generator(self, mapper); } /// @@ -78,19 +83,24 @@ namespace Kyoo /// The type of items in /// The type of items in the returned list /// The list mapped as an AsyncEnumerable - /// mapper can't be null - public static async IAsyncEnumerable SelectAsync([CanBeNull] this IEnumerable self, + /// The list or the mapper can't be null + public static IAsyncEnumerable SelectAsync([NotNull] this IEnumerable self, [NotNull] Func> mapper) { if (self == null) - yield break; + throw new ArgumentNullException(nameof(self)); if (mapper == null) throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - while (enumerator.MoveNext()) - yield return await mapper(enumerator.Current); + static async IAsyncEnumerable Generator(IEnumerable self, Func> mapper) + { + using IEnumerator enumerator = self.GetEnumerator(); + + while (enumerator.MoveNext()) + yield return await mapper(enumerator.Current); + } + + return Generator(self, mapper); } /// @@ -100,16 +110,20 @@ namespace Kyoo /// The type of items in the async list and in the returned list. /// A task that will return a simple list /// The list can't be null - public static async Task> ToListAsync([NotNull] this IAsyncEnumerable self) + public static Task> ToListAsync([NotNull] this IAsyncEnumerable self) { if (self == null) throw new ArgumentNullException(nameof(self)); - - List ret = new(); - - await foreach(T i in self) - ret.Add(i); - return ret; + + static async Task> ToList(IAsyncEnumerable self) + { + List ret = new(); + await foreach (T i in self) + ret.Add(i); + return ret; + } + + return ToList(self); } /// @@ -118,22 +132,33 @@ namespace Kyoo /// The enumerable to check /// The action to execute is the list is empty /// The type of items inside the list - /// - public static IEnumerable IfEmpty(this IEnumerable self, Action action) + /// The iterable and the action can't be null. + /// The iterator proxied, there is no dual iterations. + public static IEnumerable IfEmpty([NotNull] this IEnumerable self, [NotNull] Action action) { - using IEnumerator enumerator = self.GetEnumerator(); + if (self == null) + throw new ArgumentNullException(nameof(self)); + if (action == null) + throw new ArgumentNullException(nameof(action)); - if (!enumerator.MoveNext()) + static IEnumerable Generator(IEnumerable self, Action action) { - action(); - yield break; + using IEnumerator enumerator = self.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + action(); + yield break; + } + + do + { + yield return enumerator.Current; + } + while (enumerator.MoveNext()); } - - do - { - yield return enumerator.Current; - } - while (enumerator.MoveNext()); + + return Generator(self, action); } /// diff --git a/Kyoo.Tests/KAssert.cs b/Kyoo.Tests/KAssert.cs index b6168251..2c97ddce 100644 --- a/Kyoo.Tests/KAssert.cs +++ b/Kyoo.Tests/KAssert.cs @@ -1,14 +1,41 @@ using System.Reflection; using Xunit; +using Xunit.Sdk; namespace Kyoo.Tests { + /// + /// Custom assertions used by Kyoo's tests. + /// public static class KAssert { + /// + /// Check if every property of the item is equal to the other's object. + /// + /// The value to check against + /// The value to check + /// The type to check public static void DeepEqual(T expected, T value) { foreach (PropertyInfo property in typeof(T).GetProperties()) Assert.Equal(property.GetValue(expected), property.GetValue(value)); } + + /// + /// Explicitly fail a test. + /// + public static void Fail() + { + throw new XunitException(); + } + + /// + /// Explicitly fail a test. + /// + /// The message that will be seen in the test report + public static void Fail(string message) + { + throw new XunitException(message); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Utility/EnumerableTests.cs b/Kyoo.Tests/Utility/EnumerableTests.cs new file mode 100644 index 00000000..9cdd8a00 --- /dev/null +++ b/Kyoo.Tests/Utility/EnumerableTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Kyoo.Tests +{ + public class EnumerableTests + { + [Fact] + public void MapTest() + { + int[] list = {1, 2, 3, 4}; + Assert.All(list.Map((x, i) => (x, i)), x => Assert.Equal(x.x - 1, x.i)); + Assert.Throws(() => list.Map(((Func)null)!)); + list = null; + Assert.Throws(() => list!.Map((x, _) => x + 1)); + } + + [Fact] + public async Task MapAsyncTest() + { + int[] list = {1, 2, 3, 4}; + await foreach((int x, int i) in list.MapAsync((x, i) => Task.FromResult((x, i)))) + { + Assert.Equal(x - 1, i); + } + Assert.Throws(() => list.MapAsync(((Func>)null)!)); + list = null; + Assert.Throws(() => list!.MapAsync((x, _) => Task.FromResult(x + 1))); + } + + [Fact] + public async Task SelectAsyncTest() + { + int[] list = {1, 2, 3, 4}; + int i = 2; + await foreach(int x in list.SelectAsync(x => Task.FromResult(x + 1))) + { + Assert.Equal(i++, x); + } + Assert.Throws(() => list.SelectAsync(((Func>)null)!)); + list = null; + Assert.Throws(() => list!.SelectAsync(x => Task.FromResult(x + 1))); + } + + [Fact] + public async Task ToListAsyncTest() + { + int[] expected = {1, 2, 3, 4}; + IAsyncEnumerable list = expected.SelectAsync(Task.FromResult); + Assert.Equal(expected, await list.ToListAsync()); + list = null; + await Assert.ThrowsAsync(() => list!.ToListAsync()); + } + + [Fact] + public void IfEmptyTest() + { + int[] list = {1, 2, 3, 4}; + list = list.IfEmpty(() => KAssert.Fail("Empty action should not be triggered.")).ToArray(); + Assert.Throws(() => list.IfEmpty(null!).ToList()); + list = null; + Assert.Throws(() => list!.IfEmpty(() => {}).ToList()); + list = Array.Empty(); + Assert.Throws(() => list.IfEmpty(() => throw new ArgumentException()).ToList()); + Assert.Empty(list.IfEmpty(() => {})); + } + } +} \ No newline at end of file From 2bc559424c992c7560688852c8d27002545b3887 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 30 May 2021 23:42:14 +0200 Subject: [PATCH 07/57] Splitting task tests and adding documentation to resources --- Kyoo.Common/Controllers/ILibraryManager.cs | 12 ++ .../Models/Attributes/LinkAttribute.cs | 13 +++ .../Models/Attributes/RelationAttributes.cs | 19 +++- Kyoo.Common/Models/Resources/Collection.cs | 56 +++++++--- Kyoo.Common/Models/Resources/Episode.cs | 103 ++++++++++++++++-- Kyoo.Common/Models/Resources/Genre.cs | 43 +++++--- Kyoo.Common/Models/Resources/IResource.cs | 6 + Kyoo.Common/Models/Resources/Library.cs | 48 +++++++- Kyoo.Common/Models/Resources/People.cs | 33 +++++- Kyoo.Common/Models/Resources/Provider.cs | 57 ++++++++-- Kyoo.Common/Models/Resources/Season.cs | 62 ++++++++++- Kyoo.Common/Models/Resources/User.cs | 3 +- Kyoo.Common/Utility/TaskUtils.cs | 69 ++++++++++++ Kyoo.Common/Utility/Utility.cs | 38 ------- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 56 ++++++++-- Kyoo.Tests/Utility/TaskTests.cs | 76 +++++++++++++ .../Repositories/ShowRepository.cs | 6 +- 17 files changed, 584 insertions(+), 116 deletions(-) create mode 100644 Kyoo.Common/Models/Attributes/LinkAttribute.cs create mode 100644 Kyoo.Common/Utility/TaskUtils.cs create mode 100644 Kyoo.Tests/Utility/TaskTests.cs diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 2cd0c909..53c7061a 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -242,6 +242,9 @@ namespace Kyoo.Controllers /// The type of the source object /// The related resource's type /// The param + /// + /// + /// Task Load([NotNull] T obj, Expression> member) where T : class, IResource where T2 : class, IResource, new(); @@ -254,6 +257,9 @@ namespace Kyoo.Controllers /// The type of the source object /// The related resource's type /// The param + /// + /// + /// Task Load([NotNull] T obj, Expression>> member) where T : class, IResource where T2 : class, new(); @@ -265,6 +271,9 @@ namespace Kyoo.Controllers /// The name of the resource to load (case sensitive) /// The type of the source object /// The param + /// + /// + /// Task Load([NotNull] T obj, string memberName) where T : class, IResource; @@ -273,6 +282,9 @@ namespace Kyoo.Controllers /// /// The source object. /// The name of the resource to load (case sensitive) + /// + /// + /// Task Load([NotNull] IResource obj, string memberName); /// diff --git a/Kyoo.Common/Models/Attributes/LinkAttribute.cs b/Kyoo.Common/Models/Attributes/LinkAttribute.cs new file mode 100644 index 00000000..d98ad90a --- /dev/null +++ b/Kyoo.Common/Models/Attributes/LinkAttribute.cs @@ -0,0 +1,13 @@ +using System; +using JetBrains.Annotations; +using Kyoo.Models.Attributes; + +namespace Kyoo.Common.Models.Attributes +{ + /// + /// An attribute to mark Link properties on resource. + /// + [AttributeUsage(AttributeTargets.Property)] + [MeansImplicitUse] + public class LinkAttribute : SerializeIgnoreAttribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/RelationAttributes.cs b/Kyoo.Common/Models/Attributes/RelationAttributes.cs index aac0e633..ef84f5e5 100644 --- a/Kyoo.Common/Models/Attributes/RelationAttributes.cs +++ b/Kyoo.Common/Models/Attributes/RelationAttributes.cs @@ -1,17 +1,34 @@ using System; +using Kyoo.Controllers; namespace Kyoo.Models.Attributes { - [AttributeUsage(AttributeTargets.Property, Inherited = false)] + /// + /// The targeted relation can be edited via calls to the repository's method. + /// + [AttributeUsage(AttributeTargets.Property)] public class EditableRelationAttribute : Attribute { } + /// + /// The targeted relation can be loaded via a call to . + /// [AttributeUsage(AttributeTargets.Property)] public class LoadableRelationAttribute : Attribute { + /// + /// The name of the field containing the related resource's ID. + /// public string RelationID { get; } + /// + /// Create a new . + /// public LoadableRelationAttribute() {} + /// + /// Create a new with a baking relationID field. + /// + /// The name of the RelationID field. public LoadableRelationAttribute(string relationID) { RelationID = relationID; diff --git a/Kyoo.Common/Models/Resources/Collection.cs b/Kyoo.Common/Models/Resources/Collection.cs index 3c7bed25..8162ff16 100644 --- a/Kyoo.Common/Models/Resources/Collection.cs +++ b/Kyoo.Common/Models/Resources/Collection.cs @@ -1,31 +1,59 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A class representing collections of . + /// A collection can also be stored in a . + /// public class Collection : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this collection. + /// public string Name { get; set; } + + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The description of this collection. + /// public string Overview { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } - [LoadableRelation] public virtual ICollection Libraries { get; set; } + + /// + /// The list of shows contained in this collection. + /// + [LoadableRelation] public ICollection Shows { get; set; } + + /// + /// The list of libraries that contains this collection. + /// + [LoadableRelation] public ICollection Libraries { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } - [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } -#endif - public Collection() { } - - public Collection(string slug, string name, string overview, string poster) - { - Slug = slug; - Name = name; - Overview = overview; - Poster = poster; - } + /// + /// The internal link between this collection and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } + + /// + /// The internal link between this collection and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } +#endif } } diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 29aeab06..e5d0d7f1 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,40 +1,126 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A class to represent a single show's episode. + /// This is also used internally for movies (their number is juste set to -1). + /// public class Episode : IResource, IOnMerge { + /// public int ID { get; set; } + + /// public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + + /// + /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. + /// [SerializeIgnore] public string ShowSlug { private get; set; } + + /// + /// The ID of the Show containing this episode. This value is only set when the has been loaded. + /// [SerializeIgnore] public int ShowID { get; set; } - [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } + /// + /// The show that contains this episode. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } + + /// + /// The ID of the Season containing this episode. This value is only set when the has been loaded. + /// [SerializeIgnore] public int? SeasonID { get; set; } - [LoadableRelation(nameof(SeasonID))] public virtual Season Season { get; set; } + /// + /// The season that contains this episode. This must be explicitly loaded via a call to . + /// This can be null if the season is unknown and the episode is only identified by it's . + /// + [LoadableRelation(nameof(SeasonID))] public Season Season { get; set; } + /// + /// The season in witch this episode is in. This defaults to -1 if not specified. + /// public int SeasonNumber { get; set; } = -1; + + /// + /// The number of this episode is it's season. This defaults to -1 if not specified. + /// public int EpisodeNumber { get; set; } = -1; + + /// + /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. + /// This defaults to -1 if not specified. + /// public int AbsoluteNumber { get; set; } = -1; + + /// + /// The path of the video file for this episode. Any format supported by a is allowed. + /// [SerializeIgnore] public string Path { get; set; } + /// + /// The path of this episode's thumbnail. + /// By default, the http path for the thumbnail is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; } + + /// + /// The title of this episode. + /// public string Title { get; set; } + + /// + /// The overview of this episode. + /// public string Overview { get; set; } + + /// + /// The release date of this episode. It can be null if unknown. + /// public DateTime? ReleaseDate { get; set; } - public int Runtime { get; set; } //This runtime variable should be in minutes + /// + /// The link to metadata providers that this episode has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - - [EditableRelation] [LoadableRelation] public virtual ICollection Tracks { get; set; } + /// + /// The list of tracks this episode has. This lists video, audio and subtitles available. + /// + [EditableRelation] [LoadableRelation] public ICollection Tracks { get; set; } - public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber) + /// + /// Get the slug of an episode. + /// + /// The slug of the show. It can't be null. + /// + /// The season in which the episode is. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to -1. + /// + /// + /// The number of the episode in it's season. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to -1. + /// + /// + /// The absolute number of this show. + /// If you don't know it or this is a movie, use -1 + /// + /// The slug corresponding to the given arguments + /// The given show slug was null. + public static string GetSlug([NotNull] string showSlug, + int seasonNumber = -1, + int episodeNumber = -1, + int absoluteNumber = -1) { if (showSlug == null) - throw new ArgumentException("Show's slug is null. Can't find episode's slug."); + throw new ArgumentNullException(nameof(showSlug)); return seasonNumber switch { -1 when absoluteNumber == -1 => showSlug, @@ -43,6 +129,7 @@ namespace Kyoo.Models }; } + /// public void OnMerge(object merged) { Episode other = (Episode)merged; diff --git a/Kyoo.Common/Models/Resources/Genre.cs b/Kyoo.Common/Models/Resources/Genre.cs index cd6086df..c7aaa76f 100644 --- a/Kyoo.Common/Models/Resources/Genre.cs +++ b/Kyoo.Common/Models/Resources/Genre.cs @@ -1,40 +1,51 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A genre that allow one to specify categories for shows. + /// public class Genre : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this genre. + /// public string Name { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } + /// + /// The list of shows that have this genre. + /// + [LoadableRelation] public ICollection Shows { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } + /// + /// The internal link between this genre and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } #endif - + /// + /// Create a new, empty . + /// public Genre() {} + /// + /// Create a new and specify it's . + /// The is automatically calculated from it's name. + /// + /// The name of this genre. public Genre(string name) { Slug = Utility.ToSlug(name); Name = name; } - - public Genre(string slug, string name) - { - Slug = slug; - Name = name; - } - - public Genre(int id, string slug, string name) - { - ID = id; - Slug = slug; - Name = name; - } } } diff --git a/Kyoo.Common/Models/Resources/IResource.cs b/Kyoo.Common/Models/Resources/IResource.cs index c4c4231b..4867bc0a 100644 --- a/Kyoo.Common/Models/Resources/IResource.cs +++ b/Kyoo.Common/Models/Resources/IResource.cs @@ -1,3 +1,5 @@ +using Kyoo.Controllers; + namespace Kyoo.Models { /// @@ -8,6 +10,10 @@ namespace Kyoo.Models /// /// A unique ID for this type of resource. This can't be changed and duplicates are not allowed. /// + /// + /// You don't need to specify an ID manually when creating a new resource, + /// this field is automatically assigned by the . + /// public int ID { get; set; } /// diff --git a/Kyoo.Common/Models/Resources/Library.cs b/Kyoo.Common/Models/Resources/Library.cs index c8148544..a72d6a37 100644 --- a/Kyoo.Common/Models/Resources/Library.cs +++ b/Kyoo.Common/Models/Resources/Library.cs @@ -1,24 +1,60 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A library containing and . + /// public class Library : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this library. + /// public string Name { get; set; } + + /// + /// The list of paths that this library is responsible for. This is mainly used by the Scan task. + /// public string[] Paths { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection Providers { get; set; } + /// + /// The list of used for items in this library. + /// + [EditableRelation] [LoadableRelation] public ICollection Providers { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } - [LoadableRelation] public virtual ICollection Collections { get; set; } + /// + /// The list of shows in this library. + /// + [LoadableRelation] public ICollection Shows { get; set; } + + /// + /// The list of collections in this library. + /// + [LoadableRelation] public ICollection Collections { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ProviderLinks { get; set; } - [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } - [SerializeIgnore] public virtual ICollection> CollectionLinks { get; set; } + /// + /// The internal link between this library and provider in the list. + /// + [Link] public ICollection> ProviderLinks { get; set; } + + /// + /// The internal link between this library and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } + + /// + /// The internal link between this library and collection in the list. + /// + [Link] public ICollection> CollectionLinks { get; set; } #endif } } diff --git a/Kyoo.Common/Models/Resources/People.cs b/Kyoo.Common/Models/Resources/People.cs index 46b86143..9fea0112 100644 --- a/Kyoo.Common/Models/Resources/People.cs +++ b/Kyoo.Common/Models/Resources/People.cs @@ -3,14 +3,37 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// An actor, voice actor, writer, animator, somebody who worked on a . + /// public class People : IResource { + /// public int ID { get; set; } - public string Slug { get; set; } - public string Name { get; set; } - [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection Roles { get; set; } + /// + public string Slug { get; set; } + + /// + /// The name of this person. + /// + public string Name { get; set; } + + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// + [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The link to metadata providers that this person has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } + + /// + /// The list of roles this person has played in. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection Roles { get; set; } } } diff --git a/Kyoo.Common/Models/Resources/Provider.cs b/Kyoo.Common/Models/Resources/Provider.cs index 6a19f27c..ac7d9ffc 100644 --- a/Kyoo.Common/Models/Resources/Provider.cs +++ b/Kyoo.Common/Models/Resources/Provider.cs @@ -1,37 +1,72 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// This class contains metadata about . + /// You can have providers even if you don't have the corresponding . + /// public class Provider : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this provider. + /// public string Name { get; set; } + + /// + /// The path of this provider's logo. + /// By default, the http path for this logo is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; } + + /// + /// The extension of the logo. This is used for http responses. + /// [SerializeIgnore] public string LogoExtension { get; set; } - [LoadableRelation] public virtual ICollection Libraries { get; set; } + + /// + /// The list of libraries that uses this provider. + /// + [LoadableRelation] public ICollection Libraries { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } - [SerializeIgnore] public virtual ICollection MetadataLinks { get; set; } + /// + /// The internal link between this provider and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } + + /// + /// The internal link between this provider and related . + /// + [Link] public ICollection MetadataLinks { get; set; } #endif + /// + /// Create a new, default, + /// public Provider() { } + /// + /// Create a new and specify it's . + /// The is automatically calculated from it's name. + /// + /// The name of this provider. + /// The logo of this provider. public Provider(string name, string logo) { Slug = Utility.ToSlug(name); Name = name; Logo = logo; } - - public Provider(int id, string name, string logo) - { - ID = id; - Slug = Utility.ToSlug(name); - Name = name; - Logo = logo; - } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index b3f7ab27..369763ea 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -1,25 +1,75 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A season of a . + /// public class Season : IResource { + /// public int ID { get; set; } + + /// public string Slug => $"{ShowSlug}-s{SeasonNumber}"; - [SerializeIgnore] public int ShowID { get; set; } + + /// + /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. + /// [SerializeIgnore] public string ShowSlug { private get; set; } - [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } + + /// + /// The ID of the Show containing this season. This value is only set when the has been loaded. + /// + [SerializeIgnore] public int ShowID { get; set; } + /// + /// The show that contains this season. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } + /// + /// The number of this season. This can be set to 0 to indicate specials. This defaults to -1 for unset. + /// public int SeasonNumber { get; set; } = -1; + /// + /// The title of this season. + /// public string Title { get; set; } + + /// + /// A quick overview of this season. + /// public string Overview { get; set; } - public int? Year { get; set; } + + /// + /// The starting air date of this season. + /// + public DateTime? StartDate { get; set; } + + /// + /// The ending date of this season. + /// + public DateTime? EndDate { get; set; } + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } + + /// + /// The link to metadata providers that this episode has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } - [LoadableRelation] public virtual ICollection Episodes { get; set; } + /// + /// The list of episodes that this season contains. + /// + [LoadableRelation] public ICollection Episodes { get; set; } } } diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs index 94afa240..1f497541 100644 --- a/Kyoo.Common/Models/Resources/User.cs +++ b/Kyoo.Common/Models/Resources/User.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; namespace Kyoo.Models { @@ -52,7 +53,7 @@ namespace Kyoo.Models /// /// Links between Users and Shows. /// - public ICollection> ShowLinks { get; set; } + [Link] public ICollection> ShowLinks { get; set; } #endif } diff --git a/Kyoo.Common/Utility/TaskUtils.cs b/Kyoo.Common/Utility/TaskUtils.cs new file mode 100644 index 00000000..78332cc9 --- /dev/null +++ b/Kyoo.Common/Utility/TaskUtils.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A class containing helper method for tasks. + /// + public static class TaskUtils + { + /// + /// Run a method after the execution of the task. + /// + /// The task to wait. + /// + /// The method to run after the task finish. This will only be run if the task finished successfully. + /// + /// The type of the item in the task. + /// A continuation task wrapping the initial task and adding a continuation method. + /// + /// The source task has been canceled. + public static Task Then(this Task task, Action then) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + then(x.Result); + return x.Result; + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// Map the result of a task to another result. + /// + /// The task to map. + /// The mapper method, it take the task's result as a parameter and should return the new result. + /// The type of returns of the given task + /// The resulting task after the mapping method + /// A task wrapping the initial task and mapping the initial result. + /// The source task has been canceled. + public static Task Map(this Task task, Func map) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + return map(x.Result); + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// A method to return the a default value from a task if the initial task is null. + /// + /// The initial task + /// The type that the task will return + /// A non-null task. + [NotNull] + public static Task DefaultIfNull([CanBeNull] Task value) + { + return value ?? Task.FromResult(default); + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs index e25b595b..e7d9600a 100644 --- a/Kyoo.Common/Utility/Utility.cs +++ b/Kyoo.Common/Utility/Utility.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -270,43 +269,6 @@ namespace Kyoo throw new ArgumentNullException(nameof(ex)); ExceptionDispatchInfo.Capture(ex).Throw(); } - - public static Task Then(this Task task, Action map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - map(x.Result); - return x.Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Map(this Task task, Func map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return map(x.Result); - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Cast(this Task task) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return (T)((dynamic)x).Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } /// /// Get a friendly type name (supporting generics) diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index 43c7fb39..cfae6a9e 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; @@ -15,20 +16,61 @@ namespace Kyoo.Tests.SpecificTests { _repository = Repositories.LibraryManager.ShowRepository; } - // + + [Fact] + public async Task EditTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Path = "/super"; + value.Title = "New Title"; + Show edited = await _repository.Edit(value, false); + KAssert.DeepEqual(value, edited); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows.FirstAsync(); + + KAssert.DeepEqual(show, value); + } + + [Fact] + public async Task EditGenreTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Genres = new[] {new Genre("test")}; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name})); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.Genres) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name})); + } + // [Fact] - // public async Task EditTest() + // public async Task EditPeopleTest() // { // Show value = await _repository.Get(TestSample.Get().Slug); - // value.Path = "/super"; - // value.Title = "New Title"; + // value.People = new[] {new People + // { + // Name = "test" + // }}; // Show edited = await _repository.Edit(value, false); - // KAssert.DeepEqual(value, edited); + // + // Assert.Equal(value.Slug, edited.Slug); + // Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name})); // // await using DatabaseContext database = Repositories.Context.New(); - // Show show = await database.Shows.FirstAsync(); + // Show show = await database.Shows + // .Include(x => x.Genres) + // .FirstAsync(); // - // KAssert.DeepEqual(show, value); + // Assert.Equal(value.Slug, show.Slug); + // Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name})); // } } } \ No newline at end of file diff --git a/Kyoo.Tests/Utility/TaskTests.cs b/Kyoo.Tests/Utility/TaskTests.cs new file mode 100644 index 00000000..3a7baa48 --- /dev/null +++ b/Kyoo.Tests/Utility/TaskTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Kyoo.Tests +{ + public class TaskTests + { + [Fact] + public async Task DefaultIfNullTest() + { + Assert.Equal(0, await TaskUtils.DefaultIfNull(null)); + Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1))); + } + + [Fact] + public async Task ThenTest() + { + await Assert.ThrowsAsync(() => Task.FromResult(1) + .Then(_ => throw new ArgumentException())); + Assert.Equal(1, await Task.FromResult(1) + .Then(_ => {})); + + static async Task Faulted() + { + await Task.Delay(1); + throw new ArgumentException(); + } + await Assert.ThrowsAsync(() => Faulted().Then(_ => KAssert.Fail())); + + static async Task Infinite() + { + await Task.Delay(100000); + return 1; + } + + CancellationTokenSource token = new(); + token.Cancel(); + await Assert.ThrowsAsync(() => Task.Run(Infinite, token.Token) + .Then(_ => {})); + } + + [Fact] + public async Task MapTest() + { + await Assert.ThrowsAsync(() => Task.FromResult(1) + .Map(_ => throw new ArgumentException())); + Assert.Equal(2, await Task.FromResult(1) + .Map(x => x + 1)); + + static async Task Faulted() + { + await Task.Delay(1); + throw new ArgumentException(); + } + await Assert.ThrowsAsync(() => Faulted() + .Map(x => + { + KAssert.Fail(); + return x; + })); + + static async Task Infinite() + { + await Task.Delay(100000); + return 1; + } + + CancellationTokenSource token = new(); + token.Cancel(); + await Assert.ThrowsAsync(() => Task.Run(Infinite, token.Token) + .Map(x => x)); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index eb3f36e4..595f2156 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -103,9 +103,9 @@ namespace Kyoo.Controllers await base.Validate(resource); if (resource.Studio != null) resource.Studio = await _studios.CreateIfNotExists(resource.Studio); - resource.Genres = await resource.Genres - .SelectAsync(x => _genres.CreateIfNotExists(x)) - .ToListAsync(); + resource.Genres = await TaskUtils.DefaultIfNull(resource.Genres + ?.SelectAsync(x => _genres.CreateIfNotExists(x)) + .ToListAsync()); resource.GenreLinks = resource.Genres? .Select(x => Link.UCreate(resource, x)) .ToList(); From c7569691e228fe620d7e121b35060c70d4492641 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 2 Jun 2021 00:35:10 +0200 Subject: [PATCH 08/57] Adding more documentation --- .../Models/Attributes/InjectedAttribute.cs | 3 +- .../Models/Attributes/SerializeAttribute.cs | 24 +++ Kyoo.Common/Models/LibraryItem.cs | 65 +++++-- Kyoo.Common/Models/Link.cs | 6 +- Kyoo.Common/Models/MetadataID.cs | 10 +- Kyoo.Common/Models/Resources/Show.cs | 154 +++++++++++++-- Kyoo.Common/Models/Resources/Studio.cs | 33 ++-- Kyoo.Common/Models/Resources/Track.cs | 183 +++++++++--------- Kyoo.Common/Utility/EnumerableExtensions.cs | 9 +- Kyoo.Tests/KAssert.cs | 4 + Kyoo.Tests/Library/TestSample.cs | 4 +- Kyoo/Controllers/Transcoder.cs | 2 +- Kyoo/Models/Stream.cs | 67 +++++++ Kyoo/Tasks/Crawler.cs | 12 +- 14 files changed, 417 insertions(+), 159 deletions(-) create mode 100644 Kyoo/Models/Stream.cs diff --git a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs index 1e9a8ece..b036acdf 100644 --- a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs +++ b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs @@ -8,7 +8,8 @@ namespace Kyoo.Models.Attributes /// An attribute to inform that the service will be injected automatically by a service provider. /// /// - /// It should only be used on and will be injected before calling + /// It should only be used on and will be injected before calling . + /// It can also be used on and it will be injected before calling . /// [AttributeUsage(AttributeTargets.Property)] [MeansImplicitUse(ImplicitUseKindFlags.Assign)] diff --git a/Kyoo.Common/Models/Attributes/SerializeAttribute.cs b/Kyoo.Common/Models/Attributes/SerializeAttribute.cs index 3eafb90c..a7958c91 100644 --- a/Kyoo.Common/Models/Attributes/SerializeAttribute.cs +++ b/Kyoo.Common/Models/Attributes/SerializeAttribute.cs @@ -2,17 +2,41 @@ using System; namespace Kyoo.Models.Attributes { + /// + /// Remove an property from the serialization pipeline. It will simply be skipped. + /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class SerializeIgnoreAttribute : Attribute {} + /// + /// Remove a property from the deserialization pipeline. The user can't input value for this property. + /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class DeserializeIgnoreAttribute : Attribute {} + /// + /// Change the way the field is serialized. It allow one to use a string format like formatting instead of the default value. + /// This can be disabled for a request by setting the "internal" query string parameter to true. + /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class SerializeAsAttribute : Attribute { + /// + /// The format string to use. + /// public string Format { get; } + /// + /// Create a new with the selected format. + /// + /// + /// The format string can contains any property within {}. It will be replaced by the actual value of the property. + /// You can also use the special value {HOST} that will put the webhost address. + /// + /// + /// The show's poster serialized uses this format string: {HOST}/api/shows/{Slug}/poster + /// + /// The format to use public SerializeAsAttribute(string format) { Format = format; diff --git a/Kyoo.Common/Models/LibraryItem.cs b/Kyoo.Common/Models/LibraryItem.cs index 78f604f2..7d48dd75 100644 --- a/Kyoo.Common/Models/LibraryItem.cs +++ b/Kyoo.Common/Models/LibraryItem.cs @@ -5,6 +5,9 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// The type of item, ether a show, a movie or a collection. + /// public enum ItemType { Show, @@ -12,16 +15,50 @@ namespace Kyoo.Models Collection } + /// + /// A type union between and . + /// This is used to list content put inside a library. + /// public class LibraryItem : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The title of the show or collection. + /// public string Title { get; set; } + + /// + /// The summary of the show or collection. + /// public string Overview { get; set; } + + /// + /// Is this show airing, not aired yet or finished? This is only applicable for shows. + /// public Status? Status { get; set; } - public string TrailerUrl { get; set; } - public int? StartYear { get; set; } - public int? EndYear { get; set; } + + /// + /// The date this show or collection started airing. It can be null if this is unknown. + /// + public DateTime? StartAir { get; set; } + + /// + /// The date this show or collection finished airing. + /// It must be after the but can be the same (example: for movies). + /// It can also be null if this is unknown. + /// + public DateTime? EndAir { get; set; } + + /// + /// The path of this item's poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; } [UsedImplicitly] private string _type => Type == ItemType.Collection ? "collection" : "show"; public ItemType Type { get; set; } @@ -35,9 +72,8 @@ namespace Kyoo.Models Title = show.Title; Overview = show.Overview; Status = show.Status; - TrailerUrl = show.TrailerUrl; - StartYear = show.StartYear; - EndYear = show.EndYear; + StartAir = show.StartAir; + EndAir = show.EndAir; Poster = show.Poster; Type = show.IsMovie ? ItemType.Movie : ItemType.Show; } @@ -49,9 +85,8 @@ namespace Kyoo.Models Title = collection.Name; Overview = collection.Overview; Status = Models.Status.Unknown; - TrailerUrl = null; - StartYear = null; - EndYear = null; + StartAir = null; + EndAir = null; Poster = collection.Poster; Type = ItemType.Collection; } @@ -63,9 +98,8 @@ namespace Kyoo.Models Title = x.Title, Overview = x.Overview, Status = x.Status, - TrailerUrl = x.TrailerUrl, - StartYear = x.StartYear, - EndYear = x.EndYear, + StartAir = x.StartAir, + EndAir = x.EndAir, Poster= x.Poster, Type = x.IsMovie ? ItemType.Movie : ItemType.Show }; @@ -77,10 +111,9 @@ namespace Kyoo.Models Title = x.Name, Overview = x.Overview, Status = Models.Status.Unknown, - TrailerUrl = null, - StartYear = null, - EndYear = null, - Poster= x.Poster, + StartAir = null, + EndAir = null, + Poster = x.Poster, Type = ItemType.Collection }; } diff --git a/Kyoo.Common/Models/Link.cs b/Kyoo.Common/Models/Link.cs index 2df85f1f..41758b52 100644 --- a/Kyoo.Common/Models/Link.cs +++ b/Kyoo.Common/Models/Link.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; namespace Kyoo.Models @@ -55,13 +54,12 @@ namespace Kyoo.Models where T1 : class, IResource where T2 : class, IResource { - public virtual T1 First { get; set; } - public virtual T2 Second { get; set; } + public T1 First { get; set; } + public T2 Second { get; set; } public Link() {} - [SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")] public Link(T1 first, T2 second, bool privateItems = false) : base(first, second) { diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index d1752d50..400d65da 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -6,19 +6,19 @@ namespace Kyoo.Models { [SerializeIgnore] public int ID { get; set; } [SerializeIgnore] public int ProviderID { get; set; } - public virtual Provider Provider {get; set; } + public Provider Provider {get; set; } [SerializeIgnore] public int? ShowID { get; set; } - [SerializeIgnore] public virtual Show Show { get; set; } + [SerializeIgnore] public Show Show { get; set; } [SerializeIgnore] public int? EpisodeID { get; set; } - [SerializeIgnore] public virtual Episode Episode { get; set; } + [SerializeIgnore] public Episode Episode { get; set; } [SerializeIgnore] public int? SeasonID { get; set; } - [SerializeIgnore] public virtual Season Season { get; set; } + [SerializeIgnore] public Season Season { get; set; } [SerializeIgnore] public int? PeopleID { get; set; } - [SerializeIgnore] public virtual People People { get; set; } + [SerializeIgnore] public People People { get; set; } public string DataID { get; set; } public string Link { get; set; } diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index e7d14d79..7656401f 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -1,53 +1,168 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Kyoo.Common.Models.Attributes; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A series or a movie. + /// public class Show : IResource, IOnMerge { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The title of this show. + /// public string Title { get; set; } + + /// + /// The list of alternative titles of this show. + /// [EditableRelation] public string[] Aliases { get; set; } + + /// + /// The path of the root directory of this show. + /// This can be any kind of path supported by + /// [SerializeIgnore] public string Path { get; set; } + + /// + /// The summary of this show. + /// public string Overview { get; set; } + + /// + /// Is this show airing, not aired yet or finished? + /// public Status? Status { get; set; } + + /// + /// An URL to a trailer. This could be any path supported by the . + /// + /// TODO for now, this is set to a youtube url. It should be cached and converted to a local file. public string TrailerUrl { get; set; } + + /// + /// The date this show started airing. It can be null if this is unknown. + /// + public DateTime? StartAir { get; set; } + + /// + /// The date this show finished airing. + /// It must be after the but can be the same (example: for movies). + /// It can also be null if this is unknown. + /// + public DateTime? EndAir { get; set; } - public int? StartYear { get; set; } - public int? EndYear { get; set; } - + /// + /// The path of this show's poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/shows/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The path of this show's logo. + /// By default, the http path for this logo is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/shows/{Slug}/logo")] public string Logo { get; set; } + + /// + /// The path of this show's backdrop. + /// By default, the http path for this backdrop is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] public string Backdrop { get; set; } + /// + /// True if this show represent a movie, false otherwise. + /// public bool IsMovie { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - + /// + /// The link to metadata providers that this show has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } + /// + /// The ID of the Studio that made this show. This value is only set when the has been loaded. + /// [SerializeIgnore] public int? StudioID { get; set; } - [LoadableRelation(nameof(StudioID))] [EditableRelation] public virtual Studio Studio { get; set; } - [LoadableRelation] [EditableRelation] public virtual ICollection Genres { get; set; } - [LoadableRelation] [EditableRelation] public virtual ICollection People { get; set; } - [LoadableRelation] public virtual ICollection Seasons { get; set; } - [LoadableRelation] public virtual ICollection Episodes { get; set; } - [LoadableRelation] public virtual ICollection Libraries { get; set; } - [LoadableRelation] public virtual ICollection Collections { get; set; } + /// + /// The Studio that made this show. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; } + + /// + /// The list of genres (themes) this show has. + /// + [LoadableRelation] [EditableRelation] public ICollection Genres { get; set; } + + /// + /// The list of people that made this show. + /// + [LoadableRelation] [EditableRelation] public ICollection People { get; set; } + + /// + /// The different seasons in this show. If this is a movie, this list is always null or empty. + /// + [LoadableRelation] public ICollection Seasons { get; set; } + + /// + /// The list of episodes in this show. If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to -1). + /// Having an episode is necessary to store metadata and tracks. + /// + [LoadableRelation] public ICollection Episodes { get; set; } + + /// + /// The list of libraries that contains this show. + /// + [LoadableRelation] public ICollection Libraries { get; set; } + + /// + /// The list of collections that contains this show. + /// + [LoadableRelation] public ICollection Collections { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } - [SerializeIgnore] public virtual ICollection> CollectionLinks { get; set; } - [SerializeIgnore] public virtual ICollection> GenreLinks { get; set; } + /// + /// The internal link between this show and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } + + /// + /// The internal link between this show and collections in the list. + /// + [Link] public ICollection> CollectionLinks { get; set; } + + /// + /// The internal link between this show and genres in the list. + /// + [Link] public ICollection> GenreLinks { get; set; } #endif + /// + /// Retrieve the internal provider's ID of a show using it's provider slug. + /// + /// This method will never return anything if the are not loaded. + /// The slug of the provider + /// The field of the asked provider. public string GetID(string provider) { - return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID; + return ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID; } - public virtual void OnMerge(object merged) + /// + public void OnMerge(object merged) { if (ExternalIDs != null) foreach (MetadataID id in ExternalIDs) @@ -64,5 +179,8 @@ namespace Kyoo.Models } } + /// + /// The enum containing show's status. + /// public enum Status { Finished, Airing, Planned, Unknown } } diff --git a/Kyoo.Common/Models/Resources/Studio.cs b/Kyoo.Common/Models/Resources/Studio.cs index 9eea3a7b..ebc3c4c1 100644 --- a/Kyoo.Common/Models/Resources/Studio.cs +++ b/Kyoo.Common/Models/Resources/Studio.cs @@ -3,31 +3,40 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A studio that make shows. + /// public class Studio : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this studio. + /// public string Name { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } + /// + /// The list of shows that are made by this studio. + /// + [LoadableRelation] public ICollection Shows { get; set; } + /// + /// Create a new, empty, . + /// public Studio() { } + /// + /// Create a new with a specific name, the slug is calculated automatically. + /// + /// The name of the studio. public Studio(string name) { Slug = Utility.ToSlug(name); Name = name; } - - public Studio(string slug, string name) - { - Slug = slug; - Name = name; - } - - public static Studio Default() - { - return new Studio("unknown", "Unknown Studio"); - } } } diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 92438e0a..290ecd3f 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -1,11 +1,13 @@ -using Kyoo.Models.Watch; -using System.Globalization; +using System.Globalization; using System.Linq; -using System.Runtime.InteropServices; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// The list of available stream types. + /// Attachments are only used temporarily by the transcoder but are not stored in a database. + /// public enum StreamType { Unknown = 0, @@ -15,82 +17,15 @@ namespace Kyoo.Models Attachment = 4 } - namespace Watch - { - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] - public class Stream - { - public string Title { get; set; } - public string Language { get; set; } - public string Codec { get; set; } - [MarshalAs(UnmanagedType.I1)] public bool isDefault; - [MarshalAs(UnmanagedType.I1)] public bool isForced; - [SerializeIgnore] public string Path { get; set; } - [SerializeIgnore] public StreamType Type { get; set; } - - public Stream() {} - - public Stream(string title, string language, string codec, bool isDefault, bool isForced, string path, StreamType type) - { - Title = title; - Language = language; - Codec = codec; - this.isDefault = isDefault; - this.isForced = isForced; - Path = path; - Type = type; - } - - public Stream(Stream stream) - { - Title = stream.Title; - Language = stream.Language; - isDefault = stream.isDefault; - isForced = stream.isForced; - Codec = stream.Codec; - Path = stream.Path; - Type = stream.Type; - } - } - } - - public class Track : Stream, IResource + /// + /// A video, audio or subtitle track for an episode. + /// + public class Track : IResource { + /// public int ID { get; set; } - [SerializeIgnore] public int EpisodeID { get; set; } - public int TrackIndex { get; set; } - public bool IsDefault - { - get => isDefault; - set => isDefault = value; - } - public bool IsForced - { - get => isForced; - set => isForced = value; - } - - public string DisplayName - { - get - { - string language = GetLanguage(Language); - - if (language == null) - return $"Unknown (index: {TrackIndex})"; - CultureInfo info = CultureInfo.GetCultures(CultureTypes.NeutralCultures) - .FirstOrDefault(x => x.ThreeLetterISOLanguageName == language); - string name = info?.EnglishName ?? language; - if (IsForced) - name += " Forced"; - if (IsExternal) - name += " (External)"; - if (Title != null && Title.Length > 1) - name += " - " + Title; - return name; - } - } - + + /// public string Slug { get @@ -112,34 +47,90 @@ namespace Kyoo.Models return $"{Episode.Slug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}"; } } - - public bool IsExternal { get; set; } - [LoadableRelation(nameof(EpisodeID))] public virtual Episode Episode { get; set; } - public Track() { } + /// + /// The title of the stream. + /// + public string Title { get; set; } + + /// + /// The language of this stream (as a ISO-639-2 language code) + /// + public string Language { get; set; } + + /// + /// The codec of this stream. + /// + public string Codec { get; set; } + + + /// + /// Is this stream the default one of it's type? + /// + public bool IsDefault { get; set; } + + /// + /// Is this stream tagged as forced? + /// + public bool IsForced { get; set; } + + /// + /// Is this track extern to the episode's file? + /// + public bool IsExternal { get; set; } + + /// + /// The path of this track. + /// + [SerializeIgnore] public string Path { get; set; } + + /// + /// The type of this stream. + /// + [SerializeIgnore] public StreamType Type { get; set; } + + /// + /// The ID of the episode that uses this track. This value is only set when the has been loaded. + /// + [SerializeIgnore] public int EpisodeID { get; set; } + /// + /// The episode that uses this track. + /// + [LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; } - public Track(StreamType type, - string title, - string language, - bool isDefault, - bool isForced, - string codec, - bool isExternal, - string path) - : base(title, language, codec, isDefault, isForced, path, type) - { - IsExternal = isExternal; - } + /// + /// The index of this track on the episode. + /// + public int TrackIndex { get; set; } - public Track(Stream stream) - : base(stream) + /// + /// A user-friendly name for this track. It does not include the track type. + /// + public string DisplayName { - IsExternal = false; + get + { + string language = GetLanguage(Language); + + if (language == null) + return $"Unknown (index: {TrackIndex})"; + CultureInfo info = CultureInfo.GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(x => x.ThreeLetterISOLanguageName == language); + string name = info?.EnglishName ?? language; + if (IsForced) + name += " Forced"; + if (IsExternal) + name += " (External)"; + if (Title is {Length: > 1}) + name += " - " + Title; + return name; + } } //Converting mkv track language to c# system language tag. private static string GetLanguage(string mkvLanguage) { + // TODO delete this and have a real way to get the language string from the ISO-639-2. return mkvLanguage switch { "fre" => "fra", diff --git a/Kyoo.Common/Utility/EnumerableExtensions.cs b/Kyoo.Common/Utility/EnumerableExtensions.cs index 459d5f1c..93f1b52e 100644 --- a/Kyoo.Common/Utility/EnumerableExtensions.cs +++ b/Kyoo.Common/Utility/EnumerableExtensions.cs @@ -20,7 +20,8 @@ namespace Kyoo /// The type of items in the returned list /// The list mapped. /// The list or the mapper can't be null - public static IEnumerable Map([NotNull] this IEnumerable self, + [LinqTunnel] + public static IEnumerable Map([NotNull] this IEnumerable self, [NotNull] Func mapper) { if (self == null) @@ -52,6 +53,7 @@ namespace Kyoo /// The type of items in the returned list /// The list mapped as an AsyncEnumerable /// The list or the mapper can't be null + [LinqTunnel] public static IAsyncEnumerable MapAsync([NotNull] this IEnumerable self, [NotNull] Func> mapper) { @@ -84,6 +86,7 @@ namespace Kyoo /// The type of items in the returned list /// The list mapped as an AsyncEnumerable /// The list or the mapper can't be null + [LinqTunnel] public static IAsyncEnumerable SelectAsync([NotNull] this IEnumerable self, [NotNull] Func> mapper) { @@ -110,6 +113,7 @@ namespace Kyoo /// The type of items in the async list and in the returned list. /// A task that will return a simple list /// The list can't be null + [LinqTunnel] public static Task> ToListAsync([NotNull] this IAsyncEnumerable self) { if (self == null) @@ -134,6 +138,7 @@ namespace Kyoo /// The type of items inside the list /// The iterable and the action can't be null. /// The iterator proxied, there is no dual iterations. + [LinqTunnel] public static IEnumerable IfEmpty([NotNull] this IEnumerable self, [NotNull] Action action) { if (self == null) @@ -236,6 +241,7 @@ namespace Kyoo /// The number of items in each chunk /// The type of data in the initial list. /// A list of chunks + [LinqTunnel] public static IEnumerable> BatchBy(this List list, int countPerList) { for (int i = 0; i < list.Count; i += countPerList) @@ -249,6 +255,7 @@ namespace Kyoo /// The number of items in each chunk /// The type of data in the initial list. /// A list of chunks + [LinqTunnel] public static IEnumerable BatchBy(this IEnumerable list, int countPerList) { T[] ret = new T[countPerList]; diff --git a/Kyoo.Tests/KAssert.cs b/Kyoo.Tests/KAssert.cs index 2c97ddce..7ac753dd 100644 --- a/Kyoo.Tests/KAssert.cs +++ b/Kyoo.Tests/KAssert.cs @@ -1,4 +1,5 @@ using System.Reflection; +using JetBrains.Annotations; using Xunit; using Xunit.Sdk; @@ -15,6 +16,7 @@ namespace Kyoo.Tests /// The value to check against /// The value to check /// The type to check + [AssertionMethod] public static void DeepEqual(T expected, T value) { foreach (PropertyInfo property in typeof(T).GetProperties()) @@ -24,6 +26,7 @@ namespace Kyoo.Tests /// /// Explicitly fail a test. /// + [AssertionMethod] public static void Fail() { throw new XunitException(); @@ -33,6 +36,7 @@ namespace Kyoo.Tests /// Explicitly fail a test. /// /// The message that will be seen in the test report + [AssertionMethod] public static void Fail(string message) { throw new XunitException(message); diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 5642054b..16eed5d8 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -26,8 +26,8 @@ namespace Kyoo.Tests "school students, they had long ceased to think of each other as friends.", Status = Status.Finished, TrailerUrl = null, - StartYear = 2011, - EndYear = 2011, + StartAir = new DateTime(2011), + EndAir = new DateTime(2011), Poster = "poster", Logo = "logo", Backdrop = "backdrop", diff --git a/Kyoo/Controllers/Transcoder.cs b/Kyoo/Controllers/Transcoder.cs index 823aa9d2..8f6e006e 100644 --- a/Kyoo/Controllers/Transcoder.cs +++ b/Kyoo/Controllers/Transcoder.cs @@ -57,7 +57,7 @@ namespace Kyoo.Controllers Stream stream = Marshal.PtrToStructure(streamsPtr); if (stream!.Type != StreamType.Unknown) { - tracks[j] = new Track(stream); + tracks[j] = stream.ToTrack(); j++; } streamsPtr += size; diff --git a/Kyoo/Models/Stream.cs b/Kyoo/Models/Stream.cs new file mode 100644 index 00000000..3639a4a6 --- /dev/null +++ b/Kyoo/Models/Stream.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; +using Kyoo.Models.Attributes; + +namespace Kyoo.Models.Watch +{ + /// + /// The unmanaged stream that the transcoder will return. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public class Stream + { + /// + /// The title of the stream. + /// + public string Title { get; set; } + + /// + /// The language of this stream (as a ISO-639-2 language code) + /// + public string Language { get; set; } + + /// + /// The codec of this stream. + /// + public string Codec { get; set; } + + /// + /// Is this stream the default one of it's type? + /// + [MarshalAs(UnmanagedType.I1)] public bool IsDefault; + + /// + /// Is this stream tagged as forced? + /// + [MarshalAs(UnmanagedType.I1)] public bool IsForced; + + /// + /// The path of this track. + /// + [SerializeIgnore] public string Path { get; set; } + + /// + /// The type of this stream. + /// + [SerializeIgnore] public StreamType Type { get; set; } + + + /// + /// Create a track from this stream. + /// + /// A new track that represent this stream. + public Track ToTrack() + { + return new() + { + Title = Title, + Language = Language, + Codec = Codec, + IsDefault = IsDefault, + IsForced = IsForced, + Path = Path, + Type = Type, + IsExternal = false + }; + } + } +} \ No newline at end of file diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index e6a6cebf..ebe3eaed 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -292,13 +292,19 @@ namespace Kyoo.Tasks catch (DuplicatedItemException) { old = await libraryManager.GetOrDefault(show.Slug); - if (old.Path == showPath) + if (old != null && old.Path == showPath) { await libraryManager.Load(old, x => x.ExternalIDs); return old; } - show.Slug += $"-{show.StartYear}"; - await libraryManager.Create(show); + + if (show.StartAir != null) + { + show.Slug += $"-{show.StartAir.Value.Year}"; + await libraryManager.Create(show); + } + else + throw; } await ThumbnailsManager.Validate(show); return show; From 988135ec122abad8b51173d28c1278f34e0284d8 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 6 Jun 2021 21:10:00 +0200 Subject: [PATCH 09/57] Adding more documentation --- Kyoo.Common/Models/LibraryItem.cs | 26 +++++++-- Kyoo.Common/Models/Link.cs | 88 +++++++++++++++++++++++++++++-- Kyoo.Common/Models/MetadataID.cs | 7 +++ Kyoo/Views/ShowApi.cs | 2 + 4 files changed, 115 insertions(+), 8 deletions(-) diff --git a/Kyoo.Common/Models/LibraryItem.cs b/Kyoo.Common/Models/LibraryItem.cs index 7d48dd75..4df07770 100644 --- a/Kyoo.Common/Models/LibraryItem.cs +++ b/Kyoo.Common/Models/LibraryItem.cs @@ -1,6 +1,5 @@ using System; using System.Linq.Expressions; -using JetBrains.Annotations; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -59,12 +58,23 @@ namespace Kyoo.Models /// By default, the http path for this poster is returned from the public API. /// This can be disabled using the internal query flag. /// - [SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; } - [UsedImplicitly] private string _type => Type == ItemType.Collection ? "collection" : "show"; + [SerializeAs("{HOST}/api/{Type}/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The type of this item (ether a collection, a show or a movie). + /// public ItemType Type { get; set; } + + /// + /// Create a new, empty . + /// public LibraryItem() {} + /// + /// Create a from a show. + /// + /// The show that this library item should represent. public LibraryItem(Show show) { ID = show.ID; @@ -78,6 +88,10 @@ namespace Kyoo.Models Type = show.IsMovie ? ItemType.Movie : ItemType.Show; } + /// + /// Create a from a collection + /// + /// The collection that this library item should represent. public LibraryItem(Collection collection) { ID = -collection.ID; @@ -91,6 +105,9 @@ namespace Kyoo.Models Type = ItemType.Collection; } + /// + /// An expression to create a representing a show. + /// public static Expression> FromShow => x => new LibraryItem { ID = x.ID, @@ -104,6 +121,9 @@ namespace Kyoo.Models Type = x.IsMovie ? ItemType.Movie : ItemType.Show }; + /// + /// An expression to create a representing a collection. + /// public static Expression> FromCollection => x => new LibraryItem { ID = -x.ID, diff --git a/Kyoo.Common/Models/Link.cs b/Kyoo.Common/Models/Link.cs index 41758b52..6d815af9 100644 --- a/Kyoo.Common/Models/Link.cs +++ b/Kyoo.Common/Models/Link.cs @@ -3,30 +3,62 @@ using System.Linq.Expressions; namespace Kyoo.Models { + /// + /// A class representing a link between two resources. + /// + /// + /// Links should only be used on the data layer and not on other application code. + /// public class Link { + /// + /// The ID of the first item of the link. + /// The first item of the link should be the one to own the link. + /// public int FirstID { get; set; } + + /// + /// The ID of the second item of this link + /// The second item of the link should be the owned resource. + /// public int SecondID { get; set; } + /// + /// Create a new typeless . + /// public Link() {} + /// + /// Create a new typeless with two IDs. + /// + /// The ID of the first resource + /// The ID of the second resource public Link(int firstID, int secondID) { FirstID = firstID; SecondID = secondID; } + /// + /// Create a new typeless between two resources. + /// + /// The first resource + /// The second resource public Link(IResource first, IResource second) { FirstID = first.ID; SecondID = second.ID; } - public static Link Create(IResource first, IResource second) - { - return new(first, second); - } - + /// + /// Create a new typed link between two resources. + /// This method can be used instead of the constructor to make use of generic parameters deduction. + /// + /// The first resource + /// The second resource + /// The type of the first resource + /// The type of the second resource + /// A newly created typed link with both resources public static Link Create(T first, T2 second) where T : class, IResource where T2 : class, IResource @@ -34,6 +66,16 @@ namespace Kyoo.Models return new(first, second); } + /// + /// Create a new typed link between two resources without storing references to resources. + /// This is the same as but this method does not set + /// and fields. Only IDs are stored and not references. + /// + /// The first resource + /// The second resource + /// The type of the first resource + /// The type of the second resource + /// A newly created typed link with both resources public static Link UCreate(T first, T2 second) where T : class, IResource where T2 : class, IResource @@ -41,6 +83,9 @@ namespace Kyoo.Models return new(first, second, true); } + /// + /// The expression to retrieve the unique ID of a Link. This is an aggregate of the two resources IDs. + /// public static Expression> PrimaryKey { get @@ -50,16 +95,41 @@ namespace Kyoo.Models } } + /// + /// A strongly typed link between two resources. + /// + /// The type of the first resource + /// The type of the second resource public class Link : Link where T1 : class, IResource where T2 : class, IResource { + /// + /// A reference of the first resource. + /// public T1 First { get; set; } + + /// + /// A reference to the second resource. + /// public T2 Second { get; set; } + /// + /// Create a new, empty, typed . + /// public Link() {} + + /// + /// Create a new typed link with two resources. + /// + /// The first resource + /// The second resource + /// + /// True if no reference to resources should be kept, false otherwise. + /// The default is false (references are kept). + /// public Link(T1 first, T2 second, bool privateItems = false) : base(first, second) { @@ -69,10 +139,18 @@ namespace Kyoo.Models Second = second; } + /// + /// Create a new typed link with IDs only. + /// + /// The ID of the first resource + /// The ID of the second resource public Link(int firstID, int secondID) : base(firstID, secondID) { } + /// + /// The expression to retrieve the unique ID of a typed Link. This is an aggregate of the two resources IDs. + /// public new static Expression, object>> PrimaryKey { get diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index 400d65da..7621830c 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -2,9 +2,16 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// ID and link of an item on an external provider. + /// public class MetadataID { + /// + /// The unique ID of this metadata. This is the equivalent of . + /// [SerializeIgnore] public int ID { get; set; } + [SerializeIgnore] public int ProviderID { get; set; } public Provider Provider {get; set; } diff --git a/Kyoo/Views/ShowApi.cs b/Kyoo/Views/ShowApi.cs index 4e121d75..966aad05 100644 --- a/Kyoo/Views/ShowApi.cs +++ b/Kyoo/Views/ShowApi.cs @@ -16,6 +16,8 @@ namespace Kyoo.Api { [Route("api/show")] [Route("api/shows")] + [Route("api/movie")] + [Route("api/movies")] [ApiController] [PartialPermission(nameof(ShowApi))] public class ShowApi : CrudApi From 4ffd526875326fcb4cbb902dcb09a30a647e8739 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 7 Jun 2021 00:12:39 +0200 Subject: [PATCH 10/57] Reworking external ids and adding documentation --- Kyoo.Common/Controllers/IRepository.cs | 15 +++-- .../Implementations/LibraryManager.cs | 16 ++--- Kyoo.Common/Models/MetadataID.cs | 29 +++------- Kyoo.Common/Models/Page.cs | 56 +++++++++++++----- Kyoo.Common/Models/PeopleRole.cs | 58 +++++++++++++++---- Kyoo.Common/Models/Resources/Episode.cs | 4 +- Kyoo.Common/Models/Resources/People.cs | 4 +- Kyoo.Common/Models/Resources/Provider.cs | 5 -- Kyoo.Common/Models/Resources/Season.cs | 4 +- Kyoo.Common/Models/Resources/Show.cs | 12 ++-- Kyoo.Common/Models/SearchResult.cs | 44 +++++++++++--- Kyoo.Common/Utility/Utility.cs | 1 - Kyoo.CommonAPI/DatabaseContext.cs | 45 +++++++++----- .../Repositories/EpisodeRepository.cs | 6 +- .../Repositories/PeopleRepository.cs | 6 +- .../Repositories/ProviderRepository.cs | 12 ++-- .../Repositories/SeasonRepository.cs | 6 +- .../Repositories/ShowRepository.cs | 8 +-- Kyoo/Startup.cs | 1 - 19 files changed, 214 insertions(+), 118 deletions(-) diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index dad2d5e3..3db8d475 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -631,10 +631,12 @@ namespace Kyoo.Controllers /// A predicate to add arbitrary filter /// Sort information (sort order & sort by) /// Pagination information (where to start and how many to get) + /// The type of metadata to retrieve /// A filtered list of external ids. - Task> GetMetadataID(Expression> where = null, - Sort sort = default, - Pagination limit = default); + Task>> GetMetadataID(Expression, bool>> where = null, + Sort> sort = default, + Pagination limit = default) + where T : class, IResource; /// /// Get a list of external ids that match all filters @@ -643,10 +645,11 @@ namespace Kyoo.Controllers /// A sort by expression /// Pagination information (where to start and how many to get) /// A filtered list of external ids. - Task> GetMetadataID([Optional] Expression> where, - Expression> sort, + Task>> GetMetadataID([Optional] Expression, bool>> where, + Expression, object>> sort, Pagination limit = default - ) => GetMetadataID(where, new Sort(sort), limit); + ) where T : class, IResource + => GetMetadataID(where, new Sort>(sort), limit); } /// diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index ce34f267..66581157 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -250,9 +250,9 @@ namespace Kyoo.Controllers (Show s, nameof(Show.ExternalIDs)) => SetRelation(s, - ProviderRepository.GetMetadataID(x => x.ShowID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.Show = y; x.ShowID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (Show s, nameof(Show.Genres)) => GenreRepository .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) @@ -290,9 +290,9 @@ namespace Kyoo.Controllers (Season s, nameof(Season.ExternalIDs)) => SetRelation(s, - ProviderRepository.GetMetadataID(x => x.SeasonID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.Season = y; x.SeasonID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (Season s, nameof(Season.Episodes)) => SetRelation(s, EpisodeRepository.GetAll(x => x.Season.ID == obj.ID), @@ -309,9 +309,9 @@ namespace Kyoo.Controllers (Episode e, nameof(Episode.ExternalIDs)) => SetRelation(e, - ProviderRepository.GetMetadataID(x => x.EpisodeID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.Episode = y; x.EpisodeID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (Episode e, nameof(Episode.Tracks)) => SetRelation(e, TrackRepository.GetAll(x => x.Episode.ID == obj.ID), @@ -355,9 +355,9 @@ namespace Kyoo.Controllers (People p, nameof(People.ExternalIDs)) => SetRelation(p, - ProviderRepository.GetMetadataID(x => x.PeopleID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.People = y; x.PeopleID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (People p, nameof(People.Roles)) => PeopleRepository .GetFromPeople(obj.ID) diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index 7621830c..d9ebd933 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -1,33 +1,20 @@ -using Kyoo.Models.Attributes; - namespace Kyoo.Models { /// /// ID and link of an item on an external provider. /// - public class MetadataID + /// + public class MetadataID : Link + where T : class, IResource { /// - /// The unique ID of this metadata. This is the equivalent of . + /// The ID of the resource on the external provider. /// - [SerializeIgnore] public int ID { get; set; } - - [SerializeIgnore] public int ProviderID { get; set; } - public Provider Provider {get; set; } - - [SerializeIgnore] public int? ShowID { get; set; } - [SerializeIgnore] public Show Show { get; set; } - - [SerializeIgnore] public int? EpisodeID { get; set; } - [SerializeIgnore] public Episode Episode { get; set; } - - [SerializeIgnore] public int? SeasonID { get; set; } - [SerializeIgnore] public Season Season { get; set; } - - [SerializeIgnore] public int? PeopleID { get; set; } - [SerializeIgnore] public People People { get; set; } - public string DataID { get; set; } + + /// + /// The URL of the resource on the external provider. + /// public string Link { get; set; } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Page.cs b/Kyoo.Common/Models/Page.cs index 71f9c7cd..023aac3e 100644 --- a/Kyoo.Common/Models/Page.cs +++ b/Kyoo.Common/Models/Page.cs @@ -3,22 +3,45 @@ using System.Linq; namespace Kyoo.Models { + /// + /// A page of resource that contains information about the pagination of resources. + /// + /// The type of resource contained in this page. public class Page where T : IResource { - public string This { get; set; } - public string First { get; set; } - public string Next { get; set; } + /// + /// The link of the current page. + /// + public string This { get; } + + /// + /// The link of the first page. + /// + public string First { get; } + + /// + /// The link of the next page. + /// + public string Next { get; } + /// + /// The number of items in the current page. + /// public int Count => Items.Count; - public ICollection Items { get; set; } - - public Page() { } - - public Page(ICollection items) - { - Items = items; - } - + + /// + /// The list of items in the page. + /// + public ICollection Items { get; } + + + /// + /// Create a new . + /// + /// The list of items in the page. + /// The link of the current page. + /// The link of the next page. + /// The link of the first page. public Page(ICollection items, string @this, string next, string first) { Items = items; @@ -27,7 +50,14 @@ namespace Kyoo.Models First = first; } - public Page(ICollection items, + /// + /// Create a new and compute the urls. + /// + /// The list of items in the page. + /// The base url of the resources available from this page. + /// The list of query strings of the current page + /// The number of items requested for the current page. + public Page(ICollection items, string url, Dictionary query, int limit) diff --git a/Kyoo.Common/Models/PeopleRole.cs b/Kyoo.Common/Models/PeopleRole.cs index be48abd1..062bb3e8 100644 --- a/Kyoo.Common/Models/PeopleRole.cs +++ b/Kyoo.Common/Models/PeopleRole.cs @@ -1,17 +1,55 @@ -using Kyoo.Models.Attributes; - namespace Kyoo.Models { + /// + /// A role a person played for a show. It can be an actor, musician, voice actor, director, writer... + /// + /// + /// This class is not serialized like other classes. + /// Based on the field, it is serialized like + /// a show with two extra fields ( and ). + /// public class PeopleRole : IResource { - [SerializeIgnore] public int ID { get; set; } - [SerializeIgnore] public string Slug => ForPeople ? Show.Slug : People.Slug; - [SerializeIgnore] public bool ForPeople; - [SerializeIgnore] public int PeopleID { get; set; } - [SerializeIgnore] public virtual People People { get; set; } - [SerializeIgnore] public int ShowID { get; set; } - [SerializeIgnore] public virtual Show Show { get; set; } - public string Role { get; set; } + /// + public int ID { get; set; } + + /// + public string Slug => ForPeople ? Show.Slug : People.Slug; + + /// + /// Should this role be used as a Show substitute (the value is false) or + /// as a People substitute (the value is true). + /// + public bool ForPeople { get; set; } + + /// + /// The ID of the People playing the role. + /// + public int PeopleID { get; set; } + /// + /// The people that played this role. + /// + public People People { get; set; } + + /// + /// The ID of the Show where the People playing in. + /// + public int ShowID { get; set; } + /// + /// The show where the People played in. + /// + public Show Show { get; set; } + + /// + /// The type of work the person has done for the show. + /// That can be something like "Actor", "Writer", "Music", "Voice Actor"... + /// public string Type { get; set; } + + /// + /// The role the People played. + /// This is mostly used to inform witch character was played for actor and voice actors. + /// + public string Role { get; set; } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index e5d0d7f1..3a8e3b60 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -86,9 +86,9 @@ namespace Kyoo.Models public DateTime? ReleaseDate { get; set; } /// - /// The link to metadata providers that this episode has. See for more information. + /// The link to metadata providers that this episode has. See for more information. /// - [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } /// /// The list of tracks this episode has. This lists video, audio and subtitles available. diff --git a/Kyoo.Common/Models/Resources/People.cs b/Kyoo.Common/Models/Resources/People.cs index 9fea0112..7ae04613 100644 --- a/Kyoo.Common/Models/Resources/People.cs +++ b/Kyoo.Common/Models/Resources/People.cs @@ -27,9 +27,9 @@ namespace Kyoo.Models [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } /// - /// The link to metadata providers that this person has. See for more information. + /// The link to metadata providers that this person has. See for more information. /// - [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } /// /// The list of roles this person has played in. See for more information. diff --git a/Kyoo.Common/Models/Resources/Provider.cs b/Kyoo.Common/Models/Resources/Provider.cs index ac7d9ffc..e13a9be4 100644 --- a/Kyoo.Common/Models/Resources/Provider.cs +++ b/Kyoo.Common/Models/Resources/Provider.cs @@ -44,11 +44,6 @@ namespace Kyoo.Models /// The internal link between this provider and libraries in the list. /// [Link] public ICollection> LibraryLinks { get; set; } - - /// - /// The internal link between this provider and related . - /// - [Link] public ICollection MetadataLinks { get; set; } #endif /// diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 369763ea..3c8f21a9 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -63,9 +63,9 @@ namespace Kyoo.Models [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; } /// - /// The link to metadata providers that this episode has. See for more information. + /// The link to metadata providers that this episode has. See for more information. /// - [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } /// /// The list of episodes that this season contains. diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index 7656401f..788ffff3 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -89,9 +89,9 @@ namespace Kyoo.Models public bool IsMovie { get; set; } /// - /// The link to metadata providers that this show has. See for more information. + /// The link to metadata providers that this show has. See for more information. /// - [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } /// /// The ID of the Studio that made this show. This value is only set when the has been loaded. @@ -155,18 +155,18 @@ namespace Kyoo.Models /// /// This method will never return anything if the are not loaded. /// The slug of the provider - /// The field of the asked provider. + /// The field of the asked provider. public string GetID(string provider) { - return ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID; + return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID; } /// public void OnMerge(object merged) { if (ExternalIDs != null) - foreach (MetadataID id in ExternalIDs) - id.Show = this; + foreach (MetadataID id in ExternalIDs) + id.First = this; if (People != null) foreach (PeopleRole link in People) link.Show = this; diff --git a/Kyoo.Common/Models/SearchResult.cs b/Kyoo.Common/Models/SearchResult.cs index 42a25bcf..9ec2bc37 100644 --- a/Kyoo.Common/Models/SearchResult.cs +++ b/Kyoo.Common/Models/SearchResult.cs @@ -2,14 +2,44 @@ namespace Kyoo.Models { + /// + /// Results of a search request. + /// public class SearchResult { - public string Query; - public IEnumerable Collections; - public IEnumerable Shows; - public IEnumerable Episodes; - public IEnumerable People; - public IEnumerable Genres; - public IEnumerable Studios; + /// + /// The query of the search request. + /// + public string Query { get; init; } + + /// + /// The collections that matched the search. + /// + public ICollection Collections { get; init; } + + /// + /// The shows that matched the search. + /// + public ICollection Shows { get; init; } + + /// + /// The episodes that matched the search. + /// + public ICollection Episodes { get; init; } + + /// + /// The people that matched the search. + /// + public ICollection People { get; init; } + + /// + /// The genres that matched the search. + /// + public ICollection Genres { get; init; } + + /// + /// The studios that matched the search. + /// + public ICollection Studios { get; init; } } } diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs index e7d9600a..28699035 100644 --- a/Kyoo.Common/Utility/Utility.cs +++ b/Kyoo.Common/Utility/Utility.cs @@ -7,7 +7,6 @@ using System.Reflection; using System.Runtime.ExceptionServices; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; using JetBrains.Annotations; namespace Kyoo diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 584230b9..97e0421c 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -61,10 +61,6 @@ namespace Kyoo /// 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; } @@ -79,6 +75,17 @@ namespace Kyoo /// public DbSet WatchedEpisodes { 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. /// @@ -205,25 +212,33 @@ namespace Kyoo .WithMany(x => x.ShowLinks), y => y.HasKey(Link.PrimaryKey)); - modelBuilder.Entity() - .HasOne(x => x.Show) + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Season) + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Episode) + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.People) + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Provider) - .WithMany(x => x.MetadataLinks) + + + 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() diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 6487e6b1..46c8d62c 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -234,9 +234,9 @@ namespace Kyoo.Controllers await base.Validate(resource); resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x => { - x.Provider = await _providers.CreateIfNotExists(x.Provider); - x.ProviderID = x.Provider.ID; - _database.Entry(x.Provider).State = EntityState.Detached; + x.Second = await _providers.CreateIfNotExists(x.Second); + x.SecondID = x.Second.ID; + _database.Entry(x.Second).State = EntityState.Detached; return x; }).ToListAsync(); } diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index 452f59eb..9adb37ee 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -73,9 +73,9 @@ namespace Kyoo.Controllers await base.Validate(resource); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - _database.Entry(id.Provider).State = EntityState.Detached; + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; }); await resource.Roles.ForEachAsync(async role => { diff --git a/Kyoo/Controllers/Repositories/ProviderRepository.cs b/Kyoo/Controllers/Repositories/ProviderRepository.cs index 135e8148..e48d4f50 100644 --- a/Kyoo/Controllers/Repositories/ProviderRepository.cs +++ b/Kyoo/Controllers/Repositories/ProviderRepository.cs @@ -58,18 +58,18 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - obj.MetadataLinks.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); } /// - public Task> GetMetadataID(Expression> where = null, - Sort sort = default, + public Task>> GetMetadataID(Expression, bool>> where = null, + Sort> sort = default, Pagination limit = default) + where T : class, IResource { - return ApplyFilters(_database.MetadataIds.Include(y => y.Provider), - x => _database.MetadataIds.FirstOrDefaultAsync(y => y.ID == x), - x => x.ID, + return ApplyFilters(_database.MetadataIds().Include(y => y.Second), + x => _database.MetadataIds().FirstOrDefaultAsync(y => y.FirstID == x), + x => x.FirstID, where, sort, limit); diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index 289ca08f..1f31ace7 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -160,9 +160,9 @@ namespace Kyoo.Controllers await base.Validate(resource); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - _database.Entry(id.Provider).State = EntityState.Detached; + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; }); } diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 595f2156..ba88a639 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -111,9 +111,9 @@ namespace Kyoo.Controllers .ToList(); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - _database.Entry(id.Provider).State = EntityState.Detached; + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; }); await resource.People.ForEachAsync(async role => { @@ -196,7 +196,7 @@ namespace Kyoo.Controllers _database.Entry(entry).State = EntityState.Deleted; if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) + foreach (MetadataID entry in obj.ExternalIDs) _database.Entry(entry).State = EntityState.Deleted; await _database.SaveChangesAsync(); diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 27c05c56..e52aaee1 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -4,7 +4,6 @@ using Kyoo.Authentication; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Options; -using Kyoo.Postgresql; using Kyoo.SqLite; using Kyoo.Tasks; using Microsoft.AspNetCore.Builder; From ab8d5c79e43de1098d46aa9f7d4223c7946230f2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 7 Jun 2021 20:58:02 +0200 Subject: [PATCH 11/57] Adding more documentation --- Kyoo.Common/Models/Chapter.cs | 38 ++++++ Kyoo.Common/Models/WatchItem.cs | 205 +++++++++++++++++++------------- 2 files changed, 163 insertions(+), 80 deletions(-) create mode 100644 Kyoo.Common/Models/Chapter.cs diff --git a/Kyoo.Common/Models/Chapter.cs b/Kyoo.Common/Models/Chapter.cs new file mode 100644 index 00000000..51ccc231 --- /dev/null +++ b/Kyoo.Common/Models/Chapter.cs @@ -0,0 +1,38 @@ +namespace Kyoo.Models +{ + /// + /// A chapter to split an episode in multiple parts. + /// + public class Chapter + { + /// + /// The start time of the chapter (in second from the start of the episode). + /// + public float StartTime { get; set; } + + /// + /// The end time of the chapter (in second from the start of the episode)&. + /// + public float EndTime { get; set; } + + /// + /// The name of this chapter. This should be a human-readable name that could be presented to the user. + /// There should be well-known chapters name for commonly used chapters. + /// For example, use "Opening" for the introduction-song and "Credits" for the end chapter with credits. + /// + public string Name { get; set; } + + /// + /// Create a new . + /// + /// The start time of the chapter (in second) + /// The end time of the chapter (in second) + /// The name of this chapter + public Chapter(float startTime, float endTime, string name) + { + StartTime = startTime; + EndTime = endTime; + Name = name; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index 71ec1993..a7e15fd1 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -9,95 +9,137 @@ using PathIO = System.IO.Path; namespace Kyoo.Models { - public class Chapter - { - public float StartTime; - public float EndTime; - public string Name; - - public Chapter(float startTime, float endTime, string name) - { - StartTime = startTime; - EndTime = endTime; - Name = name; - } - } - + /// + /// A watch item give information useful for playback. + /// Information about tracks and display information that could be used by the player. + /// This contains mostly data from an with another form. + /// public class WatchItem { + /// + /// The ID of the episode associated with this item. + /// public int EpisodeID { get; set; } - - public string ShowTitle { get; set; } - public string ShowSlug { get; set; } - public int SeasonNumber { get; set; } - public int EpisodeNumber { get; set; } - public int AbsoluteNumber { get; set; } - public string Title { get; set; } + + /// + /// The slug of this episode. + /// public string Slug { get; set; } + + /// + /// The title of the show containing this episode. + /// + public string ShowTitle { get; set; } + + /// + /// The slug of the show containing this episode + /// + public string ShowSlug { get; set; } + + /// + /// The season in witch this episode is in. This defaults to -1 if not specified. + /// + public int SeasonNumber { get; set; } + + /// + /// The number of this episode is it's season. This defaults to -1 if not specified. + /// + public int EpisodeNumber { get; set; } + + /// + /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. + /// This defaults to -1 if not specified. + /// + public int AbsoluteNumber { get; set; } + + /// + /// The title of this episode. + /// + public string Title { get; set; } + + /// + /// The release date of this episode. It can be null if unknown. + /// public DateTime? ReleaseDate { get; set; } + + /// + /// The path of the video file for this episode. Any format supported by a is allowed. + /// [SerializeIgnore] public string Path { get; set; } + + /// + /// The episode that come before this one if you follow usual watch orders. + /// If this is the first episode or this is a movie, it will be null. + /// public Episode PreviousEpisode { get; set; } + + /// + /// The episode that come after this one if you follow usual watch orders. + /// If this is the last aired episode or this is a movie, it will be null. + /// public Episode NextEpisode { get; set; } + + /// + /// true if this is a movie, false otherwise. + /// public bool IsMovie { get; set; } + /// + /// The path of this item's poster. + /// By default, the http path for the poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; } + + /// + /// The path of this item's logo. + /// By default, the http path for the logo is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; } + + /// + /// The path of this item's backdrop. + /// By default, the http path for the backdrop is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; } + /// + /// The container of the video file of this episode. + /// Common containers are mp4, mkv, avi and so on. + /// public string Container { get; set; } + + /// + /// The video track. See for more information. + /// public Track Video { get; set; } + + /// + /// The list of audio tracks. See for more information. + /// public ICollection Audios { get; set; } + + /// + /// The list of subtitles tracks. See for more information. + /// public ICollection Subtitles { get; set; } + + /// + /// The list of chapters. See for more information. + /// public ICollection Chapters { get; set; } + - public WatchItem() { } - - private WatchItem(int episodeID, - Show show, - int seasonNumber, - int episodeNumber, - int absoluteNumber, - string title, - DateTime? releaseDate, - string path) - { - EpisodeID = episodeID; - ShowTitle = show.Title; - ShowSlug = show.Slug; - SeasonNumber = seasonNumber; - EpisodeNumber = episodeNumber; - AbsoluteNumber = absoluteNumber; - Title = title; - ReleaseDate = releaseDate; - Path = path; - IsMovie = show.IsMovie; - - Poster = show.Poster; - Logo = show.Logo; - Backdrop = show.Backdrop; - - Container = Path.Substring(Path.LastIndexOf('.') + 1); - Slug = Episode.GetSlug(ShowSlug, seasonNumber, episodeNumber, absoluteNumber); - } - - private WatchItem(int episodeID, - Show show, - int seasonNumber, - int episodeNumber, - int absoluteNumber, - string title, - DateTime? releaseDate, - string path, - Track video, - ICollection audios, - ICollection subtitles) - : this(episodeID, show, seasonNumber, episodeNumber, absoluteNumber, title, releaseDate, path) - { - Video = video; - Audios = audios; - Subtitles = subtitles; - } - + /// + /// Create a from an . + /// + /// The episode to transform. + /// + /// A library manager to retrieve the next and previous episode and load the show & tracks of the episode. + /// + /// A new WatchItem representing the given episode. public static async Task FromEpisode(Episode ep, ILibraryManager library) { Episode previous = null; @@ -123,24 +165,27 @@ namespace Kyoo.Models next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1); } - return new WatchItem(ep.ID, - ep.Show, - ep.SeasonNumber, - ep.EpisodeNumber, - ep.AbsoluteNumber, - ep.Title, - ep.ReleaseDate, - ep.Path, - ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), - ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), - ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray()) + return new WatchItem { + EpisodeID = ep.ID, + ShowSlug = ep.Show.Slug, + SeasonNumber = ep.SeasonNumber, + EpisodeNumber = ep.EpisodeNumber, + AbsoluteNumber = ep.AbsoluteNumber, + Title = ep.Title, + ReleaseDate = ep.ReleaseDate, + Path = ep.Path, + Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), + Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), + Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(), PreviousEpisode = previous, NextEpisode = next, Chapters = await GetChapters(ep.Path) }; } + // TODO move this method in a controller to support abstraction. + // TODO use a IFileManager to retrieve and read files. private static async Task> GetChapters(string episodePath) { string path = PathIO.Combine( From cc672876aee7470f36525c175903536aed3f52da Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 7 Jun 2021 22:30:40 +0200 Subject: [PATCH 12/57] Fixing metadata ids --- Kyoo.Common/Models/MetadataID.cs | 14 ++ Kyoo.CommonAPI/DatabaseContext.cs | 8 + Kyoo.CommonAPI/Kyoo.CommonAPI.csproj | 4 +- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 4 +- ....cs => 20210607202403_Initial.Designer.cs} | 193 ++++++++++++------ ...9_Initial.cs => 20210607202403_Initial.cs} | 178 ++++++++++------ .../PostgresContextModelSnapshot.cs | 191 ++++++++++++----- Kyoo.SqLite/Kyoo.SqLite.csproj | 2 +- ....cs => 20210607202259_Initial.Designer.cs} | 190 ++++++++++++----- ...8_Initial.cs => 20210607202259_Initial.cs} | 180 ++++++++++------ .../Migrations/SqLiteContextModelSnapshot.cs | 188 ++++++++++++----- Kyoo.Tests/Kyoo.Tests.csproj | 2 +- Kyoo/Kyoo.csproj | 6 +- 13 files changed, 810 insertions(+), 350 deletions(-) rename Kyoo.Postgresql/Migrations/{20210507203809_Initial.Designer.cs => 20210607202403_Initial.Designer.cs} (84%) rename Kyoo.Postgresql/Migrations/{20210507203809_Initial.cs => 20210607202403_Initial.cs} (84%) rename Kyoo.SqLite/Migrations/{20210529124408_Initial.Designer.cs => 20210607202259_Initial.Designer.cs} (83%) rename Kyoo.SqLite/Migrations/{20210529124408_Initial.cs => 20210607202259_Initial.cs} (84%) diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index d9ebd933..34872314 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -1,3 +1,6 @@ +using System; +using System.Linq.Expressions; + namespace Kyoo.Models { /// @@ -16,5 +19,16 @@ namespace Kyoo.Models /// The URL of the resource on the external provider. /// public string Link { get; set; } + + /// + /// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs. + /// + public new static Expression, object>> PrimaryKey + { + get + { + return x => new {First = x.FirstID, Second = x.SecondID}; + } + } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 97e0421c..b106a2fb 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -212,18 +212,26 @@ namespace Kyoo .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) diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index 5288b814..3181be71 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 3ef6e4c2..05b767ab 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -18,11 +18,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210607202403_Initial.Designer.cs similarity index 84% rename from Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210607202403_Initial.Designer.cs index 834321b2..fe975800 100644 --- a/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210607202403_Initial.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210507203809_Initial")] + [Migration("20210607202403_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -23,7 +23,7 @@ namespace Kyoo.Postgresql.Migrations .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.5") + .HasAnnotation("ProductVersion", "5.0.6") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("Kyoo.Models.Collection", b => @@ -76,9 +76,6 @@ namespace Kyoo.Postgresql.Migrations b.Property("ReleaseDate") .HasColumnType("timestamp without time zone"); - b.Property("Runtime") - .HasColumnType("integer"); - b.Property("SeasonID") .HasColumnType("integer"); @@ -241,47 +238,88 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Link"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); b.Property("DataID") .HasColumnType("text"); - b.Property("EpisodeID") + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("integer"); + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("DataID") + .HasColumnType("text"); + b.Property("Link") .HasColumnType("text"); - b.Property("PeopleID") + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("integer"); - b.Property("ProviderID") + b.Property("SecondID") .HasColumnType("integer"); - b.Property("SeasonID") + b.Property("DataID") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("integer"); - b.Property("ShowID") + b.Property("SecondID") .HasColumnType("integer"); - b.HasKey("ID"); + b.Property("DataID") + .HasColumnType("text"); - b.HasIndex("EpisodeID"); + b.Property("Link") + .HasColumnType("text"); - b.HasIndex("PeopleID"); + b.HasKey("FirstID", "SecondID"); - b.HasIndex("ProviderID"); + b.HasIndex("SecondID"); - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID"); - - b.ToTable("MetadataIds"); + b.ToTable("MetadataID"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -316,6 +354,9 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("ForPeople") + .HasColumnType("boolean"); + b.Property("PeopleID") .HasColumnType("integer"); @@ -372,6 +413,9 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + b.Property("Overview") .HasColumnType("text"); @@ -384,12 +428,12 @@ namespace Kyoo.Postgresql.Migrations b.Property("ShowID") .HasColumnType("integer"); + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + b.Property("Title") .HasColumnType("text"); - b.Property("Year") - .HasColumnType("integer"); - b.HasKey("ID"); b.HasIndex("ShowID", "SeasonNumber") @@ -411,8 +455,8 @@ namespace Kyoo.Postgresql.Migrations b.Property("Backdrop") .HasColumnType("text"); - b.Property("EndYear") - .HasColumnType("integer"); + b.Property("EndAir") + .HasColumnType("timestamp without time zone"); b.Property("IsMovie") .HasColumnType("boolean"); @@ -433,8 +477,8 @@ namespace Kyoo.Postgresql.Migrations .IsRequired() .HasColumnType("text"); - b.Property("StartYear") - .HasColumnType("integer"); + b.Property("StartAir") + .HasColumnType("timestamp without time zone"); b.Property("Status") .HasColumnType("status"); @@ -708,43 +752,80 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.HasOne("Kyoo.Models.Episode", "Episode") + b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.Season", "Season") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Kyoo.Models.Show", "Show") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Episode"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("People"); + b.Navigation("First"); - b.Navigation("Provider"); + b.Navigation("Second"); + }); - b.Navigation("Season"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Show"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -854,8 +935,6 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Provider", b => { b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); }); modelBuilder.Entity("Kyoo.Models.Season", b => diff --git a/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs b/Kyoo.Postgresql/Migrations/20210607202403_Initial.cs similarity index 84% rename from Kyoo.Postgresql/Migrations/20210507203809_Initial.cs rename to Kyoo.Postgresql/Migrations/20210607202403_Initial.cs index 678e90ee..18df2ec7 100644 --- a/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210607202403_Initial.cs @@ -171,6 +171,32 @@ namespace Kyoo.Postgresql.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false), + DataID = table.Column(type: "text", nullable: true), + Link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_MetadataID_People_FirstID", + column: x => x.FirstID, + principalTable: "People", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Shows", columns: table => new @@ -184,8 +210,8 @@ namespace Kyoo.Postgresql.Migrations Overview = table.Column(type: "text", nullable: true), Status = table.Column(type: "status", nullable: true), TrailerUrl = table.Column(type: "text", nullable: true), - StartYear = table.Column(type: "integer", nullable: true), - EndYear = table.Column(type: "integer", nullable: true), + StartAir = table.Column(type: "timestamp without time zone", nullable: true), + EndAir = table.Column(type: "timestamp without time zone", nullable: true), Poster = table.Column(type: "text", nullable: true), Logo = table.Column(type: "text", nullable: true), Backdrop = table.Column(type: "text", nullable: true), @@ -299,16 +325,43 @@ namespace Kyoo.Postgresql.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false), + DataID = table.Column(type: "text", nullable: true), + Link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Shows_FirstID", + column: x => x.FirstID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "PeopleRoles", columns: table => new { ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ForPeople = table.Column(type: "boolean", nullable: false), PeopleID = table.Column(type: "integer", nullable: false), ShowID = table.Column(type: "integer", nullable: false), - Role = table.Column(type: "text", nullable: true), - Type = table.Column(type: "text", nullable: true) + Type = table.Column(type: "text", nullable: true), + Role = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -337,7 +390,8 @@ namespace Kyoo.Postgresql.Migrations SeasonNumber = table.Column(type: "integer", nullable: false), Title = table.Column(type: "text", nullable: true), Overview = table.Column(type: "text", nullable: true), - Year = table.Column(type: "integer", nullable: true), + StartDate = table.Column(type: "timestamp without time zone", nullable: true), + EndDate = table.Column(type: "timestamp without time zone", nullable: true), Poster = table.Column(type: "text", nullable: true) }, constraints: table => @@ -366,8 +420,7 @@ namespace Kyoo.Postgresql.Migrations Thumb = table.Column(type: "text", nullable: true), Title = table.Column(type: "text", nullable: true), Overview = table.Column(type: "text", nullable: true), - ReleaseDate = table.Column(type: "timestamp without time zone", nullable: true), - Runtime = table.Column(type: "integer", nullable: false) + ReleaseDate = table.Column(type: "timestamp without time zone", nullable: true) }, constraints: table => { @@ -387,50 +440,53 @@ namespace Kyoo.Postgresql.Migrations }); migrationBuilder.CreateTable( - name: "MetadataIds", + name: "MetadataID", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProviderID = table.Column(type: "integer", nullable: false), - ShowID = table.Column(type: "integer", nullable: true), - EpisodeID = table.Column(type: "integer", nullable: true), - SeasonID = table.Column(type: "integer", nullable: true), - PeopleID = table.Column(type: "integer", nullable: true), + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false), DataID = table.Column(type: "text", nullable: true), Link = table.Column(type: "text", nullable: true) }, constraints: table => { - table.PrimaryKey("PK_MetadataIds", x => x.ID); + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_MetadataIds_Episodes_EpisodeID", - column: x => x.EpisodeID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_People_PeopleID", - column: x => x.PeopleID, - principalTable: "People", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_Providers_ProviderID", - column: x => x.ProviderID, + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, principalTable: "Providers", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_MetadataIds_Seasons_SeasonID", - column: x => x.SeasonID, + name: "FK_MetadataID_Seasons_FirstID", + column: x => x.FirstID, principalTable: "Seasons", principalColumn: "ID", onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false), + DataID = table.Column(type: "text", nullable: true), + Link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_MetadataIds_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", + name: "FK_MetadataID_Episodes_FirstID", + column: x => x.FirstID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", principalColumn: "ID", onDelete: ReferentialAction.Cascade); }); @@ -441,16 +497,16 @@ namespace Kyoo.Postgresql.Migrations { ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - EpisodeID = table.Column(type: "integer", nullable: false), - TrackIndex = table.Column(type: "integer", nullable: false), - IsDefault = table.Column(type: "boolean", nullable: false), - IsForced = table.Column(type: "boolean", nullable: false), - IsExternal = table.Column(type: "boolean", nullable: false), Title = table.Column(type: "text", nullable: true), Language = table.Column(type: "text", nullable: true), Codec = table.Column(type: "text", nullable: true), + IsDefault = table.Column(type: "boolean", nullable: false), + IsForced = table.Column(type: "boolean", nullable: false), + IsExternal = table.Column(type: "boolean", nullable: false), Path = table.Column(type: "text", nullable: true), - Type = table.Column(type: "stream_type", nullable: false) + Type = table.Column(type: "stream_type", nullable: false), + EpisodeID = table.Column(type: "integer", nullable: false), + TrackIndex = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -548,29 +604,24 @@ namespace Kyoo.Postgresql.Migrations column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_EpisodeID", - table: "MetadataIds", - column: "EpisodeID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_PeopleID", - table: "MetadataIds", - column: "PeopleID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ProviderID", - table: "MetadataIds", - column: "ProviderID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_SeasonID", - table: "MetadataIds", - column: "SeasonID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ShowID", - table: "MetadataIds", - column: "ShowID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( name: "IX_People_Slug", @@ -656,7 +707,16 @@ namespace Kyoo.Postgresql.Migrations name: "Link"); migrationBuilder.DropTable( - name: "MetadataIds"); + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); migrationBuilder.DropTable( name: "PeopleRoles"); diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 4c6ceac7..0e8a675c 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -21,7 +21,7 @@ namespace Kyoo.Postgresql.Migrations .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.5") + .HasAnnotation("ProductVersion", "5.0.6") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("Kyoo.Models.Collection", b => @@ -74,9 +74,6 @@ namespace Kyoo.Postgresql.Migrations b.Property("ReleaseDate") .HasColumnType("timestamp without time zone"); - b.Property("Runtime") - .HasColumnType("integer"); - b.Property("SeasonID") .HasColumnType("integer"); @@ -239,47 +236,88 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Link"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); b.Property("DataID") .HasColumnType("text"); - b.Property("EpisodeID") + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("integer"); + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("DataID") + .HasColumnType("text"); + b.Property("Link") .HasColumnType("text"); - b.Property("PeopleID") + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("integer"); - b.Property("ProviderID") + b.Property("SecondID") .HasColumnType("integer"); - b.Property("SeasonID") + b.Property("DataID") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("integer"); - b.Property("ShowID") + b.Property("SecondID") .HasColumnType("integer"); - b.HasKey("ID"); + b.Property("DataID") + .HasColumnType("text"); - b.HasIndex("EpisodeID"); + b.Property("Link") + .HasColumnType("text"); - b.HasIndex("PeopleID"); + b.HasKey("FirstID", "SecondID"); - b.HasIndex("ProviderID"); + b.HasIndex("SecondID"); - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID"); - - b.ToTable("MetadataIds"); + b.ToTable("MetadataID"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -314,6 +352,9 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("ForPeople") + .HasColumnType("boolean"); + b.Property("PeopleID") .HasColumnType("integer"); @@ -370,6 +411,9 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + b.Property("Overview") .HasColumnType("text"); @@ -382,12 +426,12 @@ namespace Kyoo.Postgresql.Migrations b.Property("ShowID") .HasColumnType("integer"); + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + b.Property("Title") .HasColumnType("text"); - b.Property("Year") - .HasColumnType("integer"); - b.HasKey("ID"); b.HasIndex("ShowID", "SeasonNumber") @@ -409,8 +453,8 @@ namespace Kyoo.Postgresql.Migrations b.Property("Backdrop") .HasColumnType("text"); - b.Property("EndYear") - .HasColumnType("integer"); + b.Property("EndAir") + .HasColumnType("timestamp without time zone"); b.Property("IsMovie") .HasColumnType("boolean"); @@ -431,8 +475,8 @@ namespace Kyoo.Postgresql.Migrations .IsRequired() .HasColumnType("text"); - b.Property("StartYear") - .HasColumnType("integer"); + b.Property("StartAir") + .HasColumnType("timestamp without time zone"); b.Property("Status") .HasColumnType("status"); @@ -706,43 +750,80 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.HasOne("Kyoo.Models.Episode", "Episode") + b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.Season", "Season") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Kyoo.Models.Show", "Show") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Episode"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("People"); + b.Navigation("First"); - b.Navigation("Provider"); + b.Navigation("Second"); + }); - b.Navigation("Season"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Show"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -852,8 +933,6 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Provider", b => { b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); }); modelBuilder.Entity("Kyoo.Models.Season", b => diff --git a/Kyoo.SqLite/Kyoo.SqLite.csproj b/Kyoo.SqLite/Kyoo.SqLite.csproj index 43c2634f..0d5ab21c 100644 --- a/Kyoo.SqLite/Kyoo.SqLite.csproj +++ b/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Kyoo.SqLite/Migrations/20210529124408_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210607202259_Initial.Designer.cs similarity index 83% rename from Kyoo.SqLite/Migrations/20210529124408_Initial.Designer.cs rename to Kyoo.SqLite/Migrations/20210607202259_Initial.Designer.cs index ffdeefad..c0f57c7f 100644 --- a/Kyoo.SqLite/Migrations/20210529124408_Initial.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210607202259_Initial.Designer.cs @@ -6,10 +6,10 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Kyoo.SQLite.Migrations +namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210529124408_Initial")] + [Migration("20210607202259_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -66,9 +66,6 @@ namespace Kyoo.SQLite.Migrations b.Property("ReleaseDate") .HasColumnType("TEXT"); - b.Property("Runtime") - .HasColumnType("INTEGER"); - b.Property("SeasonID") .HasColumnType("INTEGER"); @@ -229,46 +226,88 @@ namespace Kyoo.SQLite.Migrations b.ToTable("Link"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.Property("ID") - .ValueGeneratedOnAdd() + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") .HasColumnType("INTEGER"); b.Property("DataID") .HasColumnType("TEXT"); - b.Property("EpisodeID") + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("INTEGER"); + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + b.Property("Link") .HasColumnType("TEXT"); - b.Property("PeopleID") + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("INTEGER"); - b.Property("ProviderID") + b.Property("SecondID") .HasColumnType("INTEGER"); - b.Property("SeasonID") + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("INTEGER"); - b.Property("ShowID") + b.Property("SecondID") .HasColumnType("INTEGER"); - b.HasKey("ID"); + b.Property("DataID") + .HasColumnType("TEXT"); - b.HasIndex("EpisodeID"); + b.Property("Link") + .HasColumnType("TEXT"); - b.HasIndex("PeopleID"); + b.HasKey("FirstID", "SecondID"); - b.HasIndex("ProviderID"); + b.HasIndex("SecondID"); - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID"); - - b.ToTable("MetadataIds"); + b.ToTable("MetadataID"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -301,6 +340,9 @@ namespace Kyoo.SQLite.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("ForPeople") + .HasColumnType("INTEGER"); + b.Property("PeopleID") .HasColumnType("INTEGER"); @@ -355,6 +397,9 @@ namespace Kyoo.SQLite.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("EndDate") + .HasColumnType("TEXT"); + b.Property("Overview") .HasColumnType("TEXT"); @@ -367,11 +412,11 @@ namespace Kyoo.SQLite.Migrations b.Property("ShowID") .HasColumnType("INTEGER"); - b.Property("Title") + b.Property("StartDate") .HasColumnType("TEXT"); - b.Property("Year") - .HasColumnType("INTEGER"); + b.Property("Title") + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -393,8 +438,8 @@ namespace Kyoo.SQLite.Migrations b.Property("Backdrop") .HasColumnType("TEXT"); - b.Property("EndYear") - .HasColumnType("INTEGER"); + b.Property("EndAir") + .HasColumnType("TEXT"); b.Property("IsMovie") .HasColumnType("INTEGER"); @@ -415,8 +460,8 @@ namespace Kyoo.SQLite.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("StartYear") - .HasColumnType("INTEGER"); + b.Property("StartAir") + .HasColumnType("TEXT"); b.Property("Status") .HasColumnType("INTEGER"); @@ -687,43 +732,80 @@ namespace Kyoo.SQLite.Migrations b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.HasOne("Kyoo.Models.Episode", "Episode") + b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.Season", "Season") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Kyoo.Models.Show", "Show") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Episode"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("People"); + b.Navigation("First"); - b.Navigation("Provider"); + b.Navigation("Second"); + }); - b.Navigation("Season"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Show"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -833,8 +915,6 @@ namespace Kyoo.SQLite.Migrations modelBuilder.Entity("Kyoo.Models.Provider", b => { b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); }); modelBuilder.Entity("Kyoo.Models.Season", b => diff --git a/Kyoo.SqLite/Migrations/20210529124408_Initial.cs b/Kyoo.SqLite/Migrations/20210607202259_Initial.cs similarity index 84% rename from Kyoo.SqLite/Migrations/20210529124408_Initial.cs rename to Kyoo.SqLite/Migrations/20210607202259_Initial.cs index 68a28ec5..a75d0b03 100644 --- a/Kyoo.SqLite/Migrations/20210529124408_Initial.cs +++ b/Kyoo.SqLite/Migrations/20210607202259_Initial.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace Kyoo.SQLite.Migrations +namespace Kyoo.SqLite.Migrations { public partial class Initial : Migration { @@ -163,6 +163,32 @@ namespace Kyoo.SQLite.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_MetadataID_People_FirstID", + column: x => x.FirstID, + principalTable: "People", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Shows", columns: table => new @@ -176,8 +202,8 @@ namespace Kyoo.SQLite.Migrations Overview = table.Column(type: "TEXT", nullable: true), Status = table.Column(type: "INTEGER", nullable: true), TrailerUrl = table.Column(type: "TEXT", nullable: true), - StartYear = table.Column(type: "INTEGER", nullable: true), - EndYear = table.Column(type: "INTEGER", nullable: true), + StartAir = table.Column(type: "TEXT", nullable: true), + EndAir = table.Column(type: "TEXT", nullable: true), Poster = table.Column(type: "TEXT", nullable: true), Logo = table.Column(type: "TEXT", nullable: true), Backdrop = table.Column(type: "TEXT", nullable: true), @@ -291,16 +317,43 @@ namespace Kyoo.SQLite.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Shows_FirstID", + column: x => x.FirstID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "PeopleRoles", columns: table => new { ID = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), + ForPeople = table.Column(type: "INTEGER", nullable: false), PeopleID = table.Column(type: "INTEGER", nullable: false), ShowID = table.Column(type: "INTEGER", nullable: false), - Role = table.Column(type: "TEXT", nullable: true), - Type = table.Column(type: "TEXT", nullable: true) + Type = table.Column(type: "TEXT", nullable: true), + Role = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -329,7 +382,8 @@ namespace Kyoo.SQLite.Migrations SeasonNumber = table.Column(type: "INTEGER", nullable: false), Title = table.Column(type: "TEXT", nullable: true), Overview = table.Column(type: "TEXT", nullable: true), - Year = table.Column(type: "INTEGER", nullable: true), + StartDate = table.Column(type: "TEXT", nullable: true), + EndDate = table.Column(type: "TEXT", nullable: true), Poster = table.Column(type: "TEXT", nullable: true) }, constraints: table => @@ -358,8 +412,7 @@ namespace Kyoo.SQLite.Migrations Thumb = table.Column(type: "TEXT", nullable: true), Title = table.Column(type: "TEXT", nullable: true), Overview = table.Column(type: "TEXT", nullable: true), - ReleaseDate = table.Column(type: "TEXT", nullable: true), - Runtime = table.Column(type: "INTEGER", nullable: false) + ReleaseDate = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -379,50 +432,53 @@ namespace Kyoo.SQLite.Migrations }); migrationBuilder.CreateTable( - name: "MetadataIds", + name: "MetadataID", columns: table => new { - ID = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ProviderID = table.Column(type: "INTEGER", nullable: false), - ShowID = table.Column(type: "INTEGER", nullable: true), - EpisodeID = table.Column(type: "INTEGER", nullable: true), - SeasonID = table.Column(type: "INTEGER", nullable: true), - PeopleID = table.Column(type: "INTEGER", nullable: true), + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), DataID = table.Column(type: "TEXT", nullable: true), Link = table.Column(type: "TEXT", nullable: true) }, constraints: table => { - table.PrimaryKey("PK_MetadataIds", x => x.ID); + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_MetadataIds_Episodes_EpisodeID", - column: x => x.EpisodeID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_People_PeopleID", - column: x => x.PeopleID, - principalTable: "People", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_Providers_ProviderID", - column: x => x.ProviderID, + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, principalTable: "Providers", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_MetadataIds_Seasons_SeasonID", - column: x => x.SeasonID, + name: "FK_MetadataID_Seasons_FirstID", + column: x => x.FirstID, principalTable: "Seasons", principalColumn: "ID", onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_MetadataIds_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", + name: "FK_MetadataID_Episodes_FirstID", + column: x => x.FirstID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", principalColumn: "ID", onDelete: ReferentialAction.Cascade); }); @@ -433,16 +489,16 @@ namespace Kyoo.SQLite.Migrations { ID = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - EpisodeID = table.Column(type: "INTEGER", nullable: false), - TrackIndex = table.Column(type: "INTEGER", nullable: false), - IsDefault = table.Column(type: "INTEGER", nullable: false), - IsForced = table.Column(type: "INTEGER", nullable: false), - IsExternal = table.Column(type: "INTEGER", nullable: false), Title = table.Column(type: "TEXT", nullable: true), Language = table.Column(type: "TEXT", nullable: true), Codec = table.Column(type: "TEXT", nullable: true), + IsDefault = table.Column(type: "INTEGER", nullable: false), + IsForced = table.Column(type: "INTEGER", nullable: false), + IsExternal = table.Column(type: "INTEGER", nullable: false), Path = table.Column(type: "TEXT", nullable: true), - Type = table.Column(type: "INTEGER", nullable: false) + Type = table.Column(type: "INTEGER", nullable: false), + EpisodeID = table.Column(type: "INTEGER", nullable: false), + TrackIndex = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -540,29 +596,24 @@ namespace Kyoo.SQLite.Migrations column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_EpisodeID", - table: "MetadataIds", - column: "EpisodeID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_PeopleID", - table: "MetadataIds", - column: "PeopleID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ProviderID", - table: "MetadataIds", - column: "ProviderID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_SeasonID", - table: "MetadataIds", - column: "SeasonID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ShowID", - table: "MetadataIds", - column: "ShowID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( name: "IX_People_Slug", @@ -648,7 +699,16 @@ namespace Kyoo.SQLite.Migrations name: "Link"); migrationBuilder.DropTable( - name: "MetadataIds"); + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); migrationBuilder.DropTable( name: "PeopleRoles"); diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs index 60379986..086c55a5 100644 --- a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs +++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Kyoo.SQLite.Migrations +namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] partial class SqLiteContextModelSnapshot : ModelSnapshot @@ -64,9 +64,6 @@ namespace Kyoo.SQLite.Migrations b.Property("ReleaseDate") .HasColumnType("TEXT"); - b.Property("Runtime") - .HasColumnType("INTEGER"); - b.Property("SeasonID") .HasColumnType("INTEGER"); @@ -227,46 +224,88 @@ namespace Kyoo.SQLite.Migrations b.ToTable("Link"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.Property("ID") - .ValueGeneratedOnAdd() + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") .HasColumnType("INTEGER"); b.Property("DataID") .HasColumnType("TEXT"); - b.Property("EpisodeID") + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("INTEGER"); + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + b.Property("Link") .HasColumnType("TEXT"); - b.Property("PeopleID") + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("INTEGER"); - b.Property("ProviderID") + b.Property("SecondID") .HasColumnType("INTEGER"); - b.Property("SeasonID") + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") .HasColumnType("INTEGER"); - b.Property("ShowID") + b.Property("SecondID") .HasColumnType("INTEGER"); - b.HasKey("ID"); + b.Property("DataID") + .HasColumnType("TEXT"); - b.HasIndex("EpisodeID"); + b.Property("Link") + .HasColumnType("TEXT"); - b.HasIndex("PeopleID"); + b.HasKey("FirstID", "SecondID"); - b.HasIndex("ProviderID"); + b.HasIndex("SecondID"); - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID"); - - b.ToTable("MetadataIds"); + b.ToTable("MetadataID"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -299,6 +338,9 @@ namespace Kyoo.SQLite.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("ForPeople") + .HasColumnType("INTEGER"); + b.Property("PeopleID") .HasColumnType("INTEGER"); @@ -353,6 +395,9 @@ namespace Kyoo.SQLite.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("EndDate") + .HasColumnType("TEXT"); + b.Property("Overview") .HasColumnType("TEXT"); @@ -365,11 +410,11 @@ namespace Kyoo.SQLite.Migrations b.Property("ShowID") .HasColumnType("INTEGER"); - b.Property("Title") + b.Property("StartDate") .HasColumnType("TEXT"); - b.Property("Year") - .HasColumnType("INTEGER"); + b.Property("Title") + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -391,8 +436,8 @@ namespace Kyoo.SQLite.Migrations b.Property("Backdrop") .HasColumnType("TEXT"); - b.Property("EndYear") - .HasColumnType("INTEGER"); + b.Property("EndAir") + .HasColumnType("TEXT"); b.Property("IsMovie") .HasColumnType("INTEGER"); @@ -413,8 +458,8 @@ namespace Kyoo.SQLite.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("StartYear") - .HasColumnType("INTEGER"); + b.Property("StartAir") + .HasColumnType("TEXT"); b.Property("Status") .HasColumnType("INTEGER"); @@ -685,43 +730,80 @@ namespace Kyoo.SQLite.Migrations b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.HasOne("Kyoo.Models.Episode", "Episode") + b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.Season", "Season") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Kyoo.Models.Show", "Show") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Episode"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("People"); + b.Navigation("First"); - b.Navigation("Provider"); + b.Navigation("Second"); + }); - b.Navigation("Season"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Show"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -831,8 +913,6 @@ namespace Kyoo.SQLite.Migrations modelBuilder.Entity("Kyoo.Models.Provider", b => { b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); }); modelBuilder.Entity("Kyoo.Models.Season", b => diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index dfb1d2c4..265ccb98 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -14,7 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 583341f6..5a369ea9 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -35,9 +35,9 @@ - - - + + + From d61f3538fe013fe2b3199e0348bdd6b379ed7132 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 8 Jun 2021 23:27:32 +0200 Subject: [PATCH 13/57] Adding tests --- Kyoo.CommonAPI/LocalRepository.cs | 2 +- Kyoo.Tests/Library/RepositoryActivator.cs | 7 +- Kyoo.Tests/Library/RepositoryTests.cs | 12 +- .../Library/SpecificTests/GlobalTests.cs | 32 ++++ Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 150 +++++++++++++++--- Kyoo.Tests/Library/TestContext.cs | 28 +++- Kyoo.Tests/Library/TestSample.cs | 43 +++++ Kyoo.Tests/Utility/MergerTests.cs | 21 +++ .../Repositories/ShowRepository.cs | 17 +- 9 files changed, 269 insertions(+), 43 deletions(-) create mode 100644 Kyoo.Tests/Library/SpecificTests/GlobalTests.cs create mode 100644 Kyoo.Tests/Utility/MergerTests.cs diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index d63c1b06..42653843 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -225,7 +225,7 @@ namespace Kyoo.Controllers T old = await GetWithTracking(edited.ID); if (resetOld) - Merger.Nullify(old); + old = Merger.Nullify(old); Merger.Complete(old, edited, x => x.GetCustomAttribute() == null); await EditRelations(old, edited, resetOld); await Database.SaveChangesAsync(); diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index 09016c60..0d2dda01 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -1,14 +1,13 @@ using System; using System.Threading.Tasks; using Kyoo.Controllers; -using Kyoo.Models; namespace Kyoo.Tests { public class RepositoryActivator : IDisposable, IAsyncDisposable { - public TestContext Context { get; init; } - public ILibraryManager LibraryManager { get; init; } + public TestContext Context { get; } + public ILibraryManager LibraryManager { get; } private readonly DatabaseContext _database; @@ -50,8 +49,6 @@ namespace Kyoo.Tests studio, genre }); - - Context.AddTest(); } public void Dispose() diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index 9c350933..16bc4fa9 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -13,10 +13,11 @@ namespace Kyoo.Tests protected readonly RepositoryActivator Repositories; private readonly IRepository _repository; - protected RepositoryTests(RepositoryActivator repositories) + protected RepositoryTests() { - Repositories = repositories; + Repositories = new RepositoryActivator(); _repository = Repositories.LibraryManager.GetRepository(); + Repositories.Context.AddTest(); } [Fact] @@ -66,5 +67,12 @@ namespace Kyoo.Tests await _repository.Delete(TestSample.Get().Slug); Assert.Equal(0, await _repository.GetCount()); } + + [Fact] + public async Task DeleteByValueTest() + { + await _repository.Delete(TestSample.Get()); + Assert.Equal(0, await _repository.GetCount()); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs b/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs new file mode 100644 index 00000000..284c2ad7 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Models; +using Xunit; + +namespace Kyoo.Tests.SpecificTests +{ + public class GlobalTests + { + [Fact] + public async Task DeleteShowWithEpisodeAndSeason() + { + RepositoryActivator repositories = new(); + Show show = TestSample.Get(); + show.Seasons = new[] + { + TestSample.Get() + }; + show.Seasons.First().Episodes = new[] + { + TestSample.Get() + }; + await repositories.Context.AddAsync(show); + + Assert.Equal(1, await repositories.LibraryManager.ShowRepository.GetCount()); + await repositories.LibraryManager.ShowRepository.Delete(show); + Assert.Equal(0, await repositories.LibraryManager.ShowRepository.GetCount()); + Assert.Equal(0, await repositories.LibraryManager.SeasonRepository.GetCount()); + Assert.Equal(0, await repositories.LibraryManager.EpisodeRepository.GetCount()); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index cfae6a9e..5cfcfea2 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; @@ -12,7 +13,6 @@ namespace Kyoo.Tests.SpecificTests private readonly IShowRepository _repository; public ShowTests() - : base(new RepositoryActivator()) { _repository = Repositories.LibraryManager.ShowRepository; } @@ -51,26 +51,132 @@ namespace Kyoo.Tests.SpecificTests Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name})); } - // [Fact] - // public async Task EditPeopleTest() - // { - // Show value = await _repository.Get(TestSample.Get().Slug); - // value.People = new[] {new People - // { - // Name = "test" - // }}; - // Show edited = await _repository.Edit(value, false); - // - // Assert.Equal(value.Slug, edited.Slug); - // Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name})); - // - // await using DatabaseContext database = Repositories.Context.New(); - // Show show = await database.Shows - // .Include(x => x.Genres) - // .FirstAsync(); - // - // Assert.Equal(value.Slug, show.Slug); - // Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name})); - // } + [Fact] + public async Task EditStudioTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Studio = new Studio("studio"); + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal("studio", edited.Studio.Slug); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.Genres) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal("studio", edited.Studio.Slug); + } + + [Fact] + public async Task EditAliasesTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Aliases = new[] {"NiceNewAlias", "SecondAlias"}; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal(value.Aliases, edited.Aliases); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows.FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal(value.Aliases, edited.Aliases); + } + + [Fact] + public async Task EditPeopleTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.People = new[] + { + new PeopleRole + { + Show = value, + People = TestSample.Get(), + ForPeople = false, + Type = "Actor", + Role = "NiceCharacter" + } + }; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal(edited.People.First().ShowID, value.ID); + Assert.Equal( + value.People.Select(x => new{x.Role, x.Slug, x.People.Name}), + edited.People.Select(x => new{x.Role, x.Slug, x.People.Name})); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.People) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal( + value.People.Select(x => new{x.Role, x.Slug, x.People.Name}), + edited.People.Select(x => new{x.Role, x.Slug, x.People.Name})); + } + + [Fact] + public async Task EditExternalIDsTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.ExternalIDs = new[] + { + new MetadataID() + { + First = value, + Second = new Provider("test", "test.png"), + DataID = "1234" + } + }; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal( + value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}), + edited.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug})); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.ExternalIDs) + .ThenInclude(x => x.Second) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal( + value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}), + show.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug})); + } + + [Fact] + public async Task EditResetOldTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + Show newValue = new() + { + ID = value.ID, + Title = "Reset" + }; + + await Assert.ThrowsAsync(() => _repository.Edit(newValue, true)); + + newValue.Slug = "reset"; + Show edited = await _repository.Edit(newValue, true); + + Assert.Equal(value.ID, edited.ID); + Assert.Null(edited.Overview); + Assert.Equal("reset", edited.Slug); + Assert.Equal("Reset", edited.Title); + Assert.Null(edited.Aliases); + Assert.Null(edited.ExternalIDs); + Assert.Null(edited.People); + Assert.Null(edited.Genres); + Assert.Null(edited.Studio); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 50db0b6b..a3d2a51e 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -37,6 +37,17 @@ namespace Kyoo.Tests context.Database.Migrate(); } + /// + /// Fill the database with pre defined values using a clean context. + /// + public void AddTest() + where T : class + { + using DatabaseContext context = New(); + context.Set().Add(TestSample.Get()); + context.SaveChanges(); + } + /// /// Fill the database with pre defined values using a clean context. /// @@ -49,15 +60,26 @@ namespace Kyoo.Tests } /// - /// Fill the database with pre defined values using a clean context. + /// Add an arbitrary data to the test context. /// - public void AddTest() + public void Add(T obj) where T : class { using DatabaseContext context = New(); - context.Set().Add(TestSample.Get()); + context.Set().Add(obj); context.SaveChanges(); } + + /// + /// Add an arbitrary data to the test context. + /// + public async Task AddAsync(T obj) + where T : class + { + await using DatabaseContext context = New(); + await context.Set().AddAsync(obj); + await context.SaveChangesAsync(); + } /// /// Get a new database context connected to a in memory Sqlite database. diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 16eed5d8..3ad39969 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -34,6 +34,49 @@ namespace Kyoo.Tests IsMovie = false, Studio = null } + }, + { + typeof(Season), + new Season + { + ID = 1, + ShowSlug = "anohana", + ShowID = 1, + SeasonNumber = 1, + Title = "Season 1", + Overview = "The first season", + StartDate = new DateTime(2020, 06, 05), + EndDate = new DateTime(2020, 07, 05), + Poster = "poster" + } + }, + { + typeof(Episode), + new Episode + { + ID = 1, + ShowSlug = "anohana", + ShowID = 1, + SeasonID = 1, + SeasonNumber = 1, + EpisodeNumber = 1, + AbsoluteNumber = 1, + Path = "/home/kyoo/anohana-s1e1", + Thumb = "thumbnail", + Title = "Episode 1", + Overview = "Summary of the first episode", + ReleaseDate = new DateTime(2020, 06, 05) + } + }, + { + typeof(People), + new People + { + ID = 1, + Slug = "the-actor", + Name = "The Actor", + Poster = "NicePoster" + } } }; diff --git a/Kyoo.Tests/Utility/MergerTests.cs b/Kyoo.Tests/Utility/MergerTests.cs new file mode 100644 index 00000000..614d328f --- /dev/null +++ b/Kyoo.Tests/Utility/MergerTests.cs @@ -0,0 +1,21 @@ +using Kyoo.Models; +using Xunit; + +namespace Kyoo.Tests +{ + public class MergerTests + { + [Fact] + public void NullifyTest() + { + Genre genre = new("test") + { + ID = 5 + }; + Merger.Nullify(genre); + Assert.Equal(0, genre.ID); + Assert.Null(genre.Name); + Assert.Null(genre.Slug); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index ba88a639..deeea623 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -131,6 +131,12 @@ namespace Kyoo.Controllers if (changed.Aliases != null || resetOld) resource.Aliases = changed.Aliases; + if (changed.Studio != null || resetOld) + { + await Database.Entry(resource).Reference(x => x.Studio).LoadAsync(); + resource.Studio = changed.Studio; + } + if (changed.Genres != null || resetOld) { await Database.Entry(resource).Collection(x => x.GenreLinks).LoadAsync(); @@ -189,18 +195,9 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - - - if (obj.People != null) - foreach (PeopleRole entry in obj.People) - _database.Entry(entry).State = EntityState.Deleted; - - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Deleted; - await _database.SaveChangesAsync(); + // TODO handle that with events maybe. (for now, seasons & episodes might not be loaded) if (obj.Seasons != null) await _seasons.Value.DeleteRange(obj.Seasons); From 6cac9916539d70d2fc294f4df7a1773edf7d517e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 9 Jun 2021 23:11:20 +0200 Subject: [PATCH 14/57] Fixing show creation --- Kyoo.SqLite/SqLiteContext.cs | 3 +- Kyoo.Tests/Library/RepositoryActivator.cs | 4 +- Kyoo.Tests/Library/RepositoryTests.cs | 12 ++++++ .../Library/SpecificTests/GlobalTests.cs | 42 +++++++++++++++---- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 38 +++++++++++++++++ Kyoo.Tests/Library/TestContext.cs | 1 + Kyoo.Tests/Library/TestSample.cs | 12 +++--- .../Repositories/ShowRepository.cs | 42 +++++-------------- 8 files changed, 105 insertions(+), 49 deletions(-) diff --git a/Kyoo.SqLite/SqLiteContext.cs b/Kyoo.SqLite/SqLiteContext.cs index ca1a651e..1a2a392a 100644 --- a/Kyoo.SqLite/SqLiteContext.cs +++ b/Kyoo.SqLite/SqLiteContext.cs @@ -119,7 +119,8 @@ namespace Kyoo.SqLite /// protected override bool IsDuplicateException(Exception ex) { - return ex.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 /*SQLITE_CONSTRAINT_UNIQUE*/}; + return ex.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 /*SQLITE_CONSTRAINT_UNIQUE*/} + or SqliteException { SqliteExtendedErrorCode: 1555 /*SQLITE_CONSTRAINT_PRIMARYKEY*/}; } /// diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index 0d2dda01..42468a49 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -24,9 +24,7 @@ namespace Kyoo.Tests StudioRepository studio = new(_database); PeopleRepository people = new(_database, provider, new Lazy(() => LibraryManager.ShowRepository)); - ShowRepository show = new(_database, studio, people, genre, provider, - new Lazy(() => LibraryManager.SeasonRepository), - new Lazy(() => LibraryManager.EpisodeRepository)); + ShowRepository show = new(_database, studio, people, genre, provider); SeasonRepository season = new(_database, provider, show, new Lazy(() => LibraryManager.EpisodeRepository)); LibraryItemRepository libraryItem = new(_database, diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index 16bc4fa9..e8504a16 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -74,5 +74,17 @@ namespace Kyoo.Tests await _repository.Delete(TestSample.Get()); Assert.Equal(0, await _repository.GetCount()); } + + [Fact] + public async Task CreateTest() + { + await Assert.ThrowsAsync(() => _repository.Create(TestSample.Get())); + await _repository.Delete(TestSample.Get()); + + T expected = TestSample.Get(); + expected.ID = 0; + await _repository.Create(expected); + KAssert.DeepEqual(expected, await _repository.Get(expected.Slug)); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs b/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs index 284c2ad7..57b8c4c3 100644 --- a/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Kyoo.Models; @@ -5,12 +7,25 @@ using Xunit; namespace Kyoo.Tests.SpecificTests { - public class GlobalTests + public class GlobalTests : IDisposable, IAsyncDisposable { + private readonly RepositoryActivator _repositories; + + public GlobalTests() + { + _repositories = new RepositoryActivator(); + } + + [Fact] + [SuppressMessage("ReSharper", "EqualExpressionComparison")] + public void SampleTest() + { + Assert.False(ReferenceEquals(TestSample.Get(), TestSample.Get())); + } + [Fact] public async Task DeleteShowWithEpisodeAndSeason() { - RepositoryActivator repositories = new(); Show show = TestSample.Get(); show.Seasons = new[] { @@ -20,13 +35,24 @@ namespace Kyoo.Tests.SpecificTests { TestSample.Get() }; - await repositories.Context.AddAsync(show); + await _repositories.Context.AddAsync(show); - Assert.Equal(1, await repositories.LibraryManager.ShowRepository.GetCount()); - await repositories.LibraryManager.ShowRepository.Delete(show); - Assert.Equal(0, await repositories.LibraryManager.ShowRepository.GetCount()); - Assert.Equal(0, await repositories.LibraryManager.SeasonRepository.GetCount()); - Assert.Equal(0, await repositories.LibraryManager.EpisodeRepository.GetCount()); + Assert.Equal(1, await _repositories.LibraryManager.ShowRepository.GetCount()); + await _repositories.LibraryManager.ShowRepository.Delete(show); + Assert.Equal(0, await _repositories.LibraryManager.ShowRepository.GetCount()); + Assert.Equal(0, await _repositories.LibraryManager.SeasonRepository.GetCount()); + Assert.Equal(0, await _repositories.LibraryManager.EpisodeRepository.GetCount()); + } + + public void Dispose() + { + _repositories.Dispose(); + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + return _repositories.DisposeAsync(); } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index 5cfcfea2..c49ca824 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -178,5 +178,43 @@ namespace Kyoo.Tests.SpecificTests Assert.Null(edited.Genres); Assert.Null(edited.Studio); } + + [Fact] + public async Task CreateWithRelationsTest() + { + Show expected = TestSample.Get(); + expected.ID = 0; + expected.Slug = "created-relation-test"; + expected.ExternalIDs = new[] + { + new MetadataID + { + First = expected, + Second = new Provider("provider", "provider.png"), + DataID = "ID" + } + }; + expected.Genres = new[] + { + new Genre + { + Name = "Genre", + Slug = "genre" + } + }; + expected.People = new[] + { + new PeopleRole + { + People = TestSample.Get(), + Show = expected, + ForPeople = false, + Role = "actor" + } + }; + expected.Studio = new Studio("studio"); + Show created = await _repository.Create(expected); + KAssert.DeepEqual(expected, created); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index a3d2a51e..30f8beb5 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -93,6 +93,7 @@ namespace Kyoo.Tests public void Dispose() { _connection.Close(); + GC.SuppressFinalize(this); } public async ValueTask DisposeAsync() diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 3ad39969..7abf1ed1 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -6,11 +6,11 @@ namespace Kyoo.Tests { public static class TestSample { - private static readonly Dictionary Samples = new() + private static readonly Dictionary> Samples = new() { { typeof(Show), - new Show + () => new Show { ID = 1, Slug = "anohana", @@ -37,7 +37,7 @@ namespace Kyoo.Tests }, { typeof(Season), - new Season + () => new Season { ID = 1, ShowSlug = "anohana", @@ -52,7 +52,7 @@ namespace Kyoo.Tests }, { typeof(Episode), - new Episode + () => new Episode { ID = 1, ShowSlug = "anohana", @@ -70,7 +70,7 @@ namespace Kyoo.Tests }, { typeof(People), - new People + () => new People { ID = 1, Slug = "the-actor", @@ -82,7 +82,7 @@ namespace Kyoo.Tests public static T Get() { - return (T)Samples[typeof(T)]; + return (T)Samples[typeof(T)](); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index deeea623..360d5d28 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -33,14 +33,6 @@ namespace Kyoo.Controllers /// A provider repository to handle externalID creation and deletion /// private readonly IProviderRepository _providers; - /// - /// A lazy loaded season repository to handle cascade deletion (seasons deletion whith it's show) - /// - private readonly Lazy _seasons; - /// - /// A lazy loaded episode repository to handle cascade deletion (episode deletion whith it's show) - /// - private readonly Lazy _episodes; /// protected override Expression> DefaultSort => x => x.Title; @@ -53,15 +45,11 @@ namespace Kyoo.Controllers /// A people repository /// A genres repository /// A provider repository - /// A lazy loaded season repository - /// A lazy loaded episode repository public ShowRepository(DatabaseContext database, IStudioRepository studios, IPeopleRepository people, IGenreRepository genres, - IProviderRepository providers, - Lazy seasons, - Lazy episodes) + IProviderRepository providers) : base(database) { _database = database; @@ -69,8 +57,6 @@ namespace Kyoo.Controllers _people = people; _genres = genres; _providers = providers; - _seasons = seasons; - _episodes = episodes; } @@ -103,12 +89,16 @@ namespace Kyoo.Controllers await base.Validate(resource); if (resource.Studio != null) resource.Studio = await _studios.CreateIfNotExists(resource.Studio); - resource.Genres = await TaskUtils.DefaultIfNull(resource.Genres - ?.SelectAsync(x => _genres.CreateIfNotExists(x)) - .ToListAsync()); + resource.GenreLinks = resource.Genres? - .Select(x => Link.UCreate(resource, x)) + .Select(x => Link.Create(resource, x)) .ToList(); + await resource.GenreLinks.ForEachAsync(async id => + { + id.Second = await _genres.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; + }); await resource.ExternalIDs.ForEachAsync(async id => { id.Second = await _providers.CreateIfNotExists(id.Second); @@ -139,8 +129,8 @@ namespace Kyoo.Controllers if (changed.Genres != null || resetOld) { - await Database.Entry(resource).Collection(x => x.GenreLinks).LoadAsync(); - resource.GenreLinks = changed.Genres?.Select(x => Link.UCreate(resource, x)).ToList(); + await Database.Entry(resource).Collection(x => x.Genres).LoadAsync(); + resource.Genres = changed.Genres; } if (changed.People != null || resetOld) @@ -191,18 +181,8 @@ namespace Kyoo.Controllers /// public override async Task Delete(Show obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - _database.Entry(obj).State = EntityState.Deleted; await _database.SaveChangesAsync(); - - // TODO handle that with events maybe. (for now, seasons & episodes might not be loaded) - if (obj.Seasons != null) - await _seasons.Value.DeleteRange(obj.Seasons); - - if (obj.Episodes != null) - await _episodes.Value.DeleteRange(obj.Episodes); } } } \ No newline at end of file From af39793b7c68b67eb5b543298c9a03a2f12626a6 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 10 Jun 2021 21:01:50 +0200 Subject: [PATCH 15/57] Testing more the base repository class --- Kyoo.Common/Controllers/IRepository.cs | 43 +-------- Kyoo.CommonAPI/CrudApi.cs | 2 +- Kyoo.CommonAPI/LocalRepository.cs | 27 +----- Kyoo.Tests/Library/RepositoryActivator.cs | 3 +- Kyoo.Tests/Library/RepositoryTests.cs | 94 ++++++++++++++++++- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 17 ++++ .../Repositories/EpisodeRepository.cs | 2 +- .../Repositories/SeasonRepository.cs | 17 +--- 8 files changed, 124 insertions(+), 81 deletions(-) diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 3db8d475..352ebe04 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -242,49 +241,13 @@ namespace Kyoo.Controllers /// The resource to delete /// If the item is not found Task Delete([NotNull] T obj); - + /// - /// Delete a list of resources. - /// - /// One or multiple resources to delete - /// If the item is not found - Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable()); - /// - /// Delete a list of resources. - /// - /// An enumerable of resources to delete - /// If the item is not found - Task DeleteRange(IEnumerable objs); - /// - /// Delete a list of resources. - /// - /// One or multiple resource's id - /// If the item is not found - Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); - /// - /// Delete a list of resources. - /// - /// An enumerable of resource's id - /// If the item is not found - Task DeleteRange(IEnumerable ids); - /// - /// Delete a list of resources. - /// - /// One or multiple resource's slug - /// If the item is not found - Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); - /// - /// Delete a list of resources. - /// - /// An enumerable of resource's slug - /// If the item is not found - Task DeleteRange(IEnumerable slugs); - /// - /// Delete a list of resources. + /// Delete all resources that match the predicate. /// /// A predicate to filter resources to delete. Every resource that match this will be deleted. /// If the item is not found - Task DeleteRange([NotNull] Expression> where); + Task DeleteAll([NotNull] Expression> where); } /// diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index b6d03580..336d3226 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -194,7 +194,7 @@ namespace Kyoo.CommonApi { try { - await _repository.DeleteRange(ApiHelper.ParseWhere(where)); + await _repository.DeleteAll(ApiHelper.ParseWhere(where)); } catch (ItemNotFoundException) { diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 42653843..9c23c774 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -295,31 +295,10 @@ namespace Kyoo.Controllers public abstract Task Delete(T obj); /// - public virtual async Task DeleteRange(IEnumerable objs) + public async Task DeleteAll(Expression> where) { - foreach (T obj in objs) - await Delete(obj); - } - - /// - public virtual async Task DeleteRange(IEnumerable ids) - { - foreach (int id in ids) - await Delete(id); - } - - /// - public virtual async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); - } - - /// - public async Task DeleteRange(Expression> where) - { - ICollection resources = await GetAll(where); - await DeleteRange(resources); + foreach (T resource in await GetAll(where)) + await Delete(resource); } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index 42468a49..7352a781 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -25,8 +25,7 @@ namespace Kyoo.Tests PeopleRepository people = new(_database, provider, new Lazy(() => LibraryManager.ShowRepository)); ShowRepository show = new(_database, studio, people, genre, provider); - SeasonRepository season = new(_database, provider, show, - new Lazy(() => LibraryManager.EpisodeRepository)); + SeasonRepository season = new(_database, provider, show); LibraryItemRepository libraryItem = new(_database, new Lazy(() => LibraryManager.LibraryRepository), new Lazy(() => LibraryManager.ShowRepository), diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index e8504a16..82482bc7 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -1,4 +1,7 @@ +using System; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; @@ -8,7 +11,7 @@ using Xunit; namespace Kyoo.Tests { public abstract class RepositoryTests - where T : class, IResource + where T : class, IResource, new() { protected readonly RepositoryActivator Repositories; private readonly IRepository _repository; @@ -86,5 +89,94 @@ namespace Kyoo.Tests await _repository.Create(expected); KAssert.DeepEqual(expected, await _repository.Get(expected.Slug)); } + + [Fact] + public async Task CreateNullTest() + { + await Assert.ThrowsAsync(() => _repository.Create(null!)); + } + + [Fact] + public async Task CreateIfNotExistNullTest() + { + await Assert.ThrowsAsync(() => _repository.CreateIfNotExists(null!)); + } + + [Fact] + public async Task CreateIfNotExistTest() + { + T expected = TestSample.Get(); + KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get())); + await _repository.Delete(TestSample.Get()); + KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get())); + } + + [Fact] + public async Task EditNullTest() + { + await Assert.ThrowsAsync(() => _repository.Edit(null!, false)); + } + + [Fact] + public async Task EditNonExistingTest() + { + await Assert.ThrowsAsync(() => _repository.Edit(new T {ID = 56}, false)); + } + + [Fact] + public async Task GetExpressionIDTest() + { + KAssert.DeepEqual(TestSample.Get(), await _repository.Get(x => x.ID == TestSample.Get().ID)); + } + + [Fact] + public async Task GetExpressionSlugTest() + { + KAssert.DeepEqual(TestSample.Get(), await _repository.Get(x => x.Slug == TestSample.Get().Slug)); + } + + [Fact] + public async Task GetExpressionNotFoundTest() + { + await Assert.ThrowsAsync(() => _repository.Get(x => x.Slug == "non-existing")); + } + + [Fact] + public async Task GetExpressionNullTest() + { + await Assert.ThrowsAsync(() => _repository.Get((Expression>)null!)); + } + + [Fact] + public async Task GetOrDefaultTest() + { + Assert.Null(await _repository.GetOrDefault(56)); + Assert.Null(await _repository.GetOrDefault("non-existing")); + Assert.Null(await _repository.GetOrDefault(x => x.Slug == "non-existing")); + } + + [Fact] + public async Task GetCountWithFilterTest() + { + string slug = TestSample.Get().Slug[2..4]; + Assert.Equal(1, await _repository.GetCount(x => x.Slug.Contains(slug))); + } + + [Fact] + public async Task GetAllTest() + { + string slug = TestSample.Get().Slug[2..4]; + ICollection ret = await _repository.GetAll(x => x.Slug.Contains(slug)); + Assert.Equal(1, ret.Count); + KAssert.DeepEqual(TestSample.Get(), ret.First()); + } + + [Fact] + public async Task DeleteAllTest() + { + string slug = TestSample.Get().Slug[2..4]; + await _repository.DeleteAll(x => x.Slug.Contains(slug)); + Assert.Equal(0, await _repository.GetCount()); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index c49ca824..183780aa 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -216,5 +216,22 @@ namespace Kyoo.Tests.SpecificTests Show created = await _repository.Create(expected); KAssert.DeepEqual(expected, created); } + + [Fact] + public async Task SlugDuplicationTest() + { + Show test = TestSample.Get(); + test.ID = 0; + test.Slug = "300"; + Show created = await _repository.Create(test); + Assert.Equal("300!", created.Slug); + } + + [Fact] + public async Task GetSlugTest() + { + Show reference = TestSample.Get(); + Assert.Equal(reference.Slug, await _repository.GetSlug(reference.ID)); + } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 46c8d62c..e6765c13 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -195,7 +195,7 @@ namespace Kyoo.Controllers if (changed.Tracks != null || resetOld) { - await _tracks.DeleteRange(x => x.EpisodeID == resource.ID); + await _tracks.DeleteAll(x => x.EpisodeID == resource.ID); resource.Tracks = changed.Tracks; await ValidateTracks(resource); } diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index 1f31ace7..ad3d5064 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -27,11 +27,7 @@ namespace Kyoo.Controllers /// A show repository to get show's slug from their ID and keep the slug in each episode. /// private readonly IShowRepository _shows; - /// - /// A lazilly loaded episode repository to handle deletion of episodes with the season. - /// - private readonly Lazy _episodes; - + /// protected override Expression> DefaultSort => x => x.SeasonNumber; @@ -43,17 +39,14 @@ namespace Kyoo.Controllers /// The database handle that will be used /// A provider repository /// A show repository - /// A lazy loaded episode repository. public SeasonRepository(DatabaseContext database, IProviderRepository providers, - IShowRepository shows, - Lazy episodes) + IShowRepository shows) : base(database) { _database = database; _providers = providers; _shows = shows; - _episodes = episodes; } @@ -186,9 +179,9 @@ namespace Kyoo.Controllers _database.Entry(obj).State = EntityState.Deleted; obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); - - if (obj.Episodes != null) - await _episodes.Value.DeleteRange(obj.Episodes); + // + // if (obj.Episodes != null) + // await _episodes.Value.DeleteRange(obj.Episodes); } } } \ No newline at end of file From 32f12ae8337ff77bf6d48af885905c387d30d91c Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 13 Jun 2021 15:50:27 +0200 Subject: [PATCH 16/57] Adding slugs setters --- Kyoo.Common/Models/Resources/Episode.cs | 32 +++++- Kyoo.Common/Models/Resources/Season.cs | 17 ++- Kyoo.Common/Models/Resources/Track.cs | 32 +++++- Kyoo.CommonAPI/DatabaseContext.cs | 2 +- Kyoo.CommonAPI/Kyoo.CommonAPI.csproj | 4 +- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 2 +- Kyoo.SqLite/Kyoo.SqLite.csproj | 4 +- Kyoo.SqLite/SqLiteContext.cs | 7 +- Kyoo.Tests/Library/RepositoryActivator.cs | 4 +- .../Library/SpecificTests/SeasonTests.cs | 15 +++ Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 19 ++++ Kyoo.Tests/Library/TestSample.cs | 1 + .../Repositories/EpisodeRepository.cs | 103 +++--------------- .../Repositories/SeasonRepository.cs | 60 +--------- .../Repositories/TrackRepository.cs | 4 +- Kyoo/Kyoo.csproj | 6 +- 16 files changed, 144 insertions(+), 168 deletions(-) create mode 100644 Kyoo.Tests/Library/SpecificTests/SeasonTests.cs diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 3a8e3b60..692e951c 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using JetBrains.Annotations; using Kyoo.Controllers; using Kyoo.Models.Attributes; @@ -14,9 +15,36 @@ namespace Kyoo.Models { /// public int ID { get; set; } - + /// - public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + public string Slug + { + get => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + set + { + Match match = Regex.Match(value, @"(?.*)-s(?\d*)e(?\d*)"); + + if (match.Success) + { + ShowSlug = match.Groups["show"].Value; + SeasonNumber = int.Parse(match.Groups["season"].Value); + EpisodeNumber = int.Parse(match.Groups["episode"].Value); + } + else + { + match = Regex.Match(value, @"(?.*)-(?\d*)"); + if (match.Success) + { + ShowSlug = match.Groups["Show"].Value; + AbsoluteNumber = int.Parse(match.Groups["absolute"].Value); + } + else + ShowSlug = value; + SeasonNumber = -1; + EpisodeNumber = -1; + } + } + } /// /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 3c8f21a9..35bb6ffe 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using Kyoo.Controllers; using Kyoo.Models.Attributes; @@ -12,9 +13,21 @@ namespace Kyoo.Models { /// public int ID { get; set; } - + /// - public string Slug => $"{ShowSlug}-s{SeasonNumber}"; + public string Slug + { + get => $"{ShowSlug}-s{SeasonNumber}"; + set + { + Match match = Regex.Match(value, @"(?.*)-s(?\d*)"); + + if (!match.Success) + throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); + ShowSlug = match.Groups["show"].Value; + SeasonNumber = int.Parse(match.Groups["season"].Value); + } + } /// /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 290ecd3f..17736e07 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -1,5 +1,7 @@ -using System.Globalization; +using System; +using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -44,10 +46,36 @@ namespace Kyoo.Models "subrip" => ".srt", {} x => $".{x}" }; - return $"{Episode.Slug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}"; + return $"{EpisodeSlug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}"; + } + set + { + Match match = Regex.Match(value, @"(?.*)-s(?\d+)e(?\d+)" + + @"(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..\w)?"); + + if (!match.Success) + { + match = Regex.Match(value, @"(?.*)\.(?.{0,3})(?-forced)?(\..\w)?"); + if (!match.Success) + throw new ArgumentException("Invalid track slug. " + + "Format: {episodeSlug}.{language}[-forced][.{extension}]"); + } + + EpisodeSlug = Episode.GetSlug(match.Groups["show"].Value, + match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : -1, + match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1); + Language = match.Groups["language"].Value; + IsForced = match.Groups["forced"].Success; + if (match.Groups["type"].Success) + Type = Enum.Parse(match.Groups["type"].Value, true); } } + /// + /// The slug of the episode that contain this track. If this is not set, this track is ill-formed. + /// + [SerializeIgnore] public string EpisodeSlug { private get; set; } + /// /// The title of the stream. /// diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index b106a2fb..f3d7f867 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -260,7 +260,7 @@ namespace Kyoo 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(); diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index 3181be71..f6add6b4 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 05b767ab..1add3667 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Kyoo.SqLite/Kyoo.SqLite.csproj b/Kyoo.SqLite/Kyoo.SqLite.csproj index 0d5ab21c..ba87fa11 100644 --- a/Kyoo.SqLite/Kyoo.SqLite.csproj +++ b/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -19,11 +19,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Kyoo.SqLite/SqLiteContext.cs b/Kyoo.SqLite/SqLiteContext.cs index 1a2a392a..81b1626e 100644 --- a/Kyoo.SqLite/SqLiteContext.cs +++ b/Kyoo.SqLite/SqLiteContext.cs @@ -82,10 +82,6 @@ namespace Kyoo.SqLite /// The database's model builder. protected override void OnModelCreating(ModelBuilder modelBuilder) { - // modelBuilder.HasPostgresEnum(); - // modelBuilder.HasPostgresEnum(); - // modelBuilder.HasPostgresEnum(); - ValueConverter arrayConvertor = new( x => string.Join(";", x), x => x.Split(';', StringSplitOptions.None)); @@ -112,7 +108,6 @@ namespace Kyoo.SqLite modelBuilder.Entity() .Property(x => x.ExtraData) .HasConversion(jsonConvertor); - base.OnModelCreating(modelBuilder); } @@ -127,7 +122,7 @@ namespace Kyoo.SqLite public override Expression> Like(Expression> query, string format) { MethodInfo iLike = MethodOfUtils.MethodOf(EF.Functions.Like); - MethodCallExpression call = Expression.Call(iLike, query.Body, Expression.Constant(format)); + MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format)); return Expression.Lambda>(call, query.Parameters); } diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index 7352a781..df0be918 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -25,13 +25,13 @@ namespace Kyoo.Tests PeopleRepository people = new(_database, provider, new Lazy(() => LibraryManager.ShowRepository)); ShowRepository show = new(_database, studio, people, genre, provider); - SeasonRepository season = new(_database, provider, show); + SeasonRepository season = new(_database, provider); LibraryItemRepository libraryItem = new(_database, new Lazy(() => LibraryManager.LibraryRepository), new Lazy(() => LibraryManager.ShowRepository), new Lazy(() => LibraryManager.CollectionRepository)); TrackRepository track = new(_database); - EpisodeRepository episode = new(_database, provider, show, track); + EpisodeRepository episode = new(_database, provider, track); LibraryManager = new LibraryManager(new IBaseRepository[] { provider, diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs new file mode 100644 index 00000000..a61436e9 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -0,0 +1,15 @@ +using Kyoo.Controllers; +using Kyoo.Models; + +namespace Kyoo.Tests.SpecificTests +{ + public class SeasonTests : RepositoryTests + { + private readonly ISeasonRepository _repository; + + public SeasonTests() + { + _repository = Repositories.LibraryManager.SeasonRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index 183780aa..9c0b5138 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; @@ -233,5 +234,23 @@ namespace Kyoo.Tests.SpecificTests Show reference = TestSample.Get(); Assert.Equal(reference.Slug, await _repository.GetSlug(reference.ID)); } + + [Theory] + [InlineData("test")] + [InlineData("super")] + [InlineData("title")] + [InlineData("TiTlE")] + [InlineData("SuPeR")] + public async Task SearchTest(string query) + { + Show value = new() + { + Slug = "super-test", + Title = "This is a test title²" + }; + await _repository.Create(value); + ICollection ret = await _repository.Search(query); + KAssert.DeepEqual(value, ret.First()); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 7abf1ed1..6f122c89 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -45,6 +45,7 @@ namespace Kyoo.Tests SeasonNumber = 1, Title = "Season 1", Overview = "The first season", + Show = Get(), StartDate = new DateTime(2020, 06, 05), EndDate = new DateTime(2020, 07, 05), Poster = "poster" diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index e6765c13..5a01bd44 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; @@ -16,7 +15,7 @@ namespace Kyoo.Controllers public class EpisodeRepository : LocalRepository, IEpisodeRepository { /// - /// The databse handle + /// The database handle /// private readonly DatabaseContext _database; /// @@ -24,10 +23,6 @@ namespace Kyoo.Controllers /// private readonly IProviderRepository _providers; /// - /// A show repository to get show's slug from their ID and keep the slug in each episode. - /// - private readonly IShowRepository _shows; - /// /// A track repository to handle creation and deletion of tracks related to the current episode. /// private readonly ITrackRepository _tracks; @@ -41,66 +36,31 @@ namespace Kyoo.Controllers /// /// The database handle to use. /// A provider repository - /// A show repository /// A track repository public EpisodeRepository(DatabaseContext database, IProviderRepository providers, - IShowRepository shows, ITrackRepository tracks) : base(database) { _database = database; _providers = providers; - _shows = shows; _tracks = tracks; } - /// - public override async Task GetOrDefault(int id) + public Task GetOrDefault(int showID, int seasonNumber, int episodeNumber) { - Episode ret = await base.GetOrDefault(id); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); } /// - public override async Task GetOrDefault(string slug) + public Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber) { - Match match = Regex.Match(slug, @"(?.*)-s(?\d*)e(?\d*)"); - - if (match.Success) - { - return await GetOrDefault(match.Groups["show"].Value, - int.Parse(match.Groups["season"].Value), - int.Parse(match.Groups["episode"].Value)); - } - - Episode episode = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug); - if (episode != null) - episode.ShowSlug = slug; - return episode; - } - - /// - public override async Task GetOrDefault(Expression> where) - { - Episode ret = await base.GetOrDefault(where); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; - } - - /// - public async Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber) - { - Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); - if (ret != null) - ret.ShowSlug = showSlug; - return ret; + return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); } /// @@ -122,61 +82,30 @@ namespace Kyoo.Controllers } /// - public async Task GetOrDefault(int showID, int seasonNumber, int episodeNumber) + public Task GetAbsolute(int showID, int absoluteNumber) { - Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(showID); - return ret; + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.AbsoluteNumber == absoluteNumber); } /// - public async Task GetAbsolute(int showID, int absoluteNumber) + public Task GetAbsolute(string showSlug, int absoluteNumber) { - Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID - && x.AbsoluteNumber == absoluteNumber); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(showID); - return ret; - } - - /// - public async Task GetAbsolute(string showSlug, int absoluteNumber) - { - Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.AbsoluteNumber == absoluteNumber); - if (ret != null) - ret.ShowSlug = showSlug; - return ret; + return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.AbsoluteNumber == absoluteNumber); } /// public override async Task> Search(string query) { - List episodes = await _database.Episodes + return await _database.Episodes .Where(x => x.EpisodeNumber != -1) .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); - foreach (Episode episode in episodes) - episode.ShowSlug = await _shows.GetSlug(episode.ShowID); - return episodes; } - /// - public override async Task> GetAll(Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - ICollection episodes = await base.GetAll(where, sort, limit); - foreach (Episode episode in episodes) - episode.ShowSlug = await _shows.GetSlug(episode.ShowID); - return episodes; - } - /// public override async Task Create(Episode obj) { diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index ad3d5064..d4529606 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; @@ -23,66 +22,30 @@ namespace Kyoo.Controllers /// A provider repository to handle externalID creation and deletion /// private readonly IProviderRepository _providers; - /// - /// A show repository to get show's slug from their ID and keep the slug in each episode. - /// - private readonly IShowRepository _shows; /// protected override Expression> DefaultSort => x => x.SeasonNumber; /// - /// Create a new using the provided handle, a provider & a show repository and - /// a service provider to lazilly request an episode repository. + /// Create a new . /// /// The database handle that will be used /// A provider repository - /// A show repository public SeasonRepository(DatabaseContext database, - IProviderRepository providers, - IShowRepository shows) + IProviderRepository providers) : base(database) { _database = database; _providers = providers; - _shows = shows; - } - - - /// - public override async Task Get(int id) - { - Season ret = await base.Get(id); - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; } - /// - public override async Task Get(Expression> where) - { - Season ret = await base.Get(where); - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; - } - - /// - public override Task Get(string slug) - { - Match match = Regex.Match(slug, @"(?.*)-s(?\d*)"); - - if (!match.Success) - throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); - return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); - } - /// public async Task Get(int showID, int seasonNumber) { Season ret = await GetOrDefault(showID, seasonNumber); if (ret == null) throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}"); - ret.ShowSlug = await _shows.GetSlug(showID); return ret; } @@ -92,7 +55,6 @@ namespace Kyoo.Controllers Season ret = await GetOrDefault(showSlug, seasonNumber); if (ret == null) throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}"); - ret.ShowSlug = showSlug; return ret; } @@ -113,27 +75,13 @@ namespace Kyoo.Controllers /// public override async Task> Search(string query) { - List seasons = await _database.Seasons + return await _database.Seasons .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); - foreach (Season season in seasons) - season.ShowSlug = await _shows.GetSlug(season.ShowID); - return seasons; } - - /// - public override async Task> GetAll(Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - ICollection seasons = await base.GetAll(where, sort, limit); - foreach (Season season in seasons) - season.ShowSlug = await _shows.GetSlug(season.ShowID); - return seasons; - } - + /// public override async Task Create(Season obj) { diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 4b27a5e4..b33f48a4 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -16,7 +16,7 @@ namespace Kyoo.Controllers public class TrackRepository : LocalRepository, ITrackRepository { /// - /// The databse handle + /// The database handle /// private readonly DatabaseContext _database; @@ -27,7 +27,7 @@ namespace Kyoo.Controllers /// /// Create a new . /// - /// The datatabse handle + /// The database handle public TrackRepository(DatabaseContext database) : base(database) { diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 5a369ea9..1fc38b98 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -35,9 +35,9 @@ - - - + + + From 538ba9a67399d43c3e9f6289c6ec6c8776c55de4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 13 Jun 2021 18:21:05 +0200 Subject: [PATCH 17/57] Adding sqlite seasons triggers --- Kyoo.SqLite/Kyoo.SqLite.csproj | 12 +- ....cs => 20210613135157_Initial.Designer.cs} | 13 +- ...9_Initial.cs => 20210613135157_Initial.cs} | 3 + .../Migrations/SqLiteContextModelSnapshot.cs | 11 +- .../Triggers/TriggersMigrations.Designer.cs | 967 ++++++++++++++++++ Kyoo.SqLite/Triggers/TriggersMigrations.cs | 35 + 6 files changed, 1032 insertions(+), 9 deletions(-) rename Kyoo.SqLite/Migrations/{20210607202259_Initial.Designer.cs => 20210613135157_Initial.Designer.cs} (98%) rename Kyoo.SqLite/Migrations/{20210607202259_Initial.cs => 20210613135157_Initial.cs} (99%) create mode 100644 Kyoo.SqLite/Triggers/TriggersMigrations.Designer.cs create mode 100644 Kyoo.SqLite/Triggers/TriggersMigrations.cs diff --git a/Kyoo.SqLite/Kyoo.SqLite.csproj b/Kyoo.SqLite/Kyoo.SqLite.csproj index ba87fa11..74665497 100644 --- a/Kyoo.SqLite/Kyoo.SqLite.csproj +++ b/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -28,14 +28,14 @@ - all - false - runtime + + + - all - false - runtime + + + diff --git a/Kyoo.SqLite/Migrations/20210607202259_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210613135157_Initial.Designer.cs similarity index 98% rename from Kyoo.SqLite/Migrations/20210607202259_Initial.Designer.cs rename to Kyoo.SqLite/Migrations/20210613135157_Initial.Designer.cs index c0f57c7f..188c7b25 100644 --- a/Kyoo.SqLite/Migrations/20210607202259_Initial.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210613135157_Initial.Designer.cs @@ -9,14 +9,14 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210607202259_Initial")] + [Migration("20210613135157_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.6"); + .HasAnnotation("ProductVersion", "5.0.7"); modelBuilder.Entity("Kyoo.Models.Collection", b => { @@ -75,6 +75,9 @@ namespace Kyoo.SqLite.Migrations b.Property("ShowID") .HasColumnType("INTEGER"); + b.Property("Slug") + .HasColumnType("TEXT"); + b.Property("Thumb") .HasColumnType("TEXT"); @@ -412,6 +415,9 @@ namespace Kyoo.SqLite.Migrations b.Property("ShowID") .HasColumnType("INTEGER"); + b.Property("Slug") + .HasColumnType("TEXT"); + b.Property("StartDate") .HasColumnType("TEXT"); @@ -533,6 +539,9 @@ namespace Kyoo.SqLite.Migrations b.Property("Path") .HasColumnType("TEXT"); + b.Property("Slug") + .HasColumnType("TEXT"); + b.Property("Title") .HasColumnType("TEXT"); diff --git a/Kyoo.SqLite/Migrations/20210607202259_Initial.cs b/Kyoo.SqLite/Migrations/20210613135157_Initial.cs similarity index 99% rename from Kyoo.SqLite/Migrations/20210607202259_Initial.cs rename to Kyoo.SqLite/Migrations/20210613135157_Initial.cs index a75d0b03..bece453b 100644 --- a/Kyoo.SqLite/Migrations/20210607202259_Initial.cs +++ b/Kyoo.SqLite/Migrations/20210613135157_Initial.cs @@ -378,6 +378,7 @@ namespace Kyoo.SqLite.Migrations { ID = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: true), ShowID = table.Column(type: "INTEGER", nullable: false), SeasonNumber = table.Column(type: "INTEGER", nullable: false), Title = table.Column(type: "TEXT", nullable: true), @@ -403,6 +404,7 @@ namespace Kyoo.SqLite.Migrations { ID = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: true), ShowID = table.Column(type: "INTEGER", nullable: false), SeasonID = table.Column(type: "INTEGER", nullable: true), SeasonNumber = table.Column(type: "INTEGER", nullable: false), @@ -489,6 +491,7 @@ namespace Kyoo.SqLite.Migrations { ID = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: true), Title = table.Column(type: "TEXT", nullable: true), Language = table.Column(type: "TEXT", nullable: true), Codec = table.Column(type: "TEXT", nullable: true), diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs index 086c55a5..16c6105a 100644 --- a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs +++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace Kyoo.SqLite.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.6"); + .HasAnnotation("ProductVersion", "5.0.7"); modelBuilder.Entity("Kyoo.Models.Collection", b => { @@ -73,6 +73,9 @@ namespace Kyoo.SqLite.Migrations b.Property("ShowID") .HasColumnType("INTEGER"); + b.Property("Slug") + .HasColumnType("TEXT"); + b.Property("Thumb") .HasColumnType("TEXT"); @@ -410,6 +413,9 @@ namespace Kyoo.SqLite.Migrations b.Property("ShowID") .HasColumnType("INTEGER"); + b.Property("Slug") + .HasColumnType("TEXT"); + b.Property("StartDate") .HasColumnType("TEXT"); @@ -531,6 +537,9 @@ namespace Kyoo.SqLite.Migrations b.Property("Path") .HasColumnType("TEXT"); + b.Property("Slug") + .HasColumnType("TEXT"); + b.Property("Title") .HasColumnType("TEXT"); diff --git a/Kyoo.SqLite/Triggers/TriggersMigrations.Designer.cs b/Kyoo.SqLite/Triggers/TriggersMigrations.Designer.cs new file mode 100644 index 00000000..83bbdcd1 --- /dev/null +++ b/Kyoo.SqLite/Triggers/TriggersMigrations.Designer.cs @@ -0,0 +1,967 @@ +// +using System; +using Kyoo.SqLite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Kyoo.SqLite.Migrations +{ + [DbContext(typeof(SqLiteContext))] + [Migration("20210613135215_Triggers")] + partial class Triggers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.7"); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteNumber") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonID") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Slug") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Paths") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ForPeople") + .HasColumnType("INTEGER"); + + b.Property("PeopleID") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("LogoExtension") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Slug") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Aliases") + .HasColumnType("TEXT"); + + b.Property("Backdrop") + .HasColumnType("TEXT"); + + b.Property("EndAir") + .HasColumnType("TEXT"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartAir") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StudioID") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrailerUrl") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("EpisodeID") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Slug") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("ExtraData") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("WatchedPercentage") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID"); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("GenreLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Navigation("LibraryLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.SqLite/Triggers/TriggersMigrations.cs b/Kyoo.SqLite/Triggers/TriggersMigrations.cs new file mode 100644 index 00000000..242cb4a1 --- /dev/null +++ b/Kyoo.SqLite/Triggers/TriggersMigrations.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Kyoo.SqLite.Migrations +{ + public partial class Triggers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW + BEGIN + UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber + WHERE ID == new.ID; + END"); + migrationBuilder.Sql(@" + CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW + BEGIN + UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber + WHERE ID == new.ID; + END"); + migrationBuilder.Sql(@" + CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW + BEGIN + UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; + END;"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;"); + migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;"); + migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;"); + } + } +} From 3ad5a0d127eb5a3fb7b77feda90ba8b92340b1f4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 13 Jun 2021 19:00:48 +0200 Subject: [PATCH 18/57] Fixing tests --- Kyoo.Tests/Library/RepositoryTests.cs | 4 +++- Kyoo.Tests/Library/TestSample.cs | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index 82482bc7..013eb6f2 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -20,7 +20,9 @@ namespace Kyoo.Tests { Repositories = new RepositoryActivator(); _repository = Repositories.LibraryManager.GetRepository(); - Repositories.Context.AddTest(); + Repositories.Context.AddTest(); + if (new T() is not Show) + Repositories.Context.AddTest(); } [Fact] diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 6f122c89..7abf1ed1 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -45,7 +45,6 @@ namespace Kyoo.Tests SeasonNumber = 1, Title = "Season 1", Overview = "The first season", - Show = Get(), StartDate = new DateTime(2020, 06, 05), EndDate = new DateTime(2020, 07, 05), Poster = "poster" From 1bb29be1343142a3478cb6aabf36d690b774be94 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 13 Jun 2021 19:13:05 +0200 Subject: [PATCH 19/57] Adding slugs tests for seasons --- .../Library/SpecificTests/SeasonTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index a61436e9..07db9d97 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -1,5 +1,7 @@ +using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; +using Xunit; namespace Kyoo.Tests.SpecificTests { @@ -11,5 +13,34 @@ namespace Kyoo.Tests.SpecificTests { _repository = Repositories.LibraryManager.SeasonRepository; } + + [Fact] + public async Task SlugEditTest() + { + Season season = await _repository.Get(1); + Assert.Equal("anohana-s1", season.Slug); + Show show = new() + { + ID = season.ShowID, + Slug = "new-slug" + }; + await Repositories.LibraryManager.ShowRepository.Edit(show, false); + season = await _repository.Get(1); + Assert.Equal("new-slug-s1", season.Slug); + } + + [Fact] + public async Task SeasonNumberEditTest() + { + Season season = await _repository.Get(1); + Assert.Equal("anohana-s1", season.Slug); + season = await _repository.Edit(new Season + { + ID = 1, + SeasonNumber = 2 + }, false); + season = await _repository.Get(1); + Assert.Equal("anohana-s2", season.Slug); + } } } \ No newline at end of file From bf9c64b9d137c322459a0cb78998a7daa1e3912b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 15 Jun 2021 23:16:05 +0200 Subject: [PATCH 20/57] Adding a postgresql test context --- Kyoo.Tests/Library/RepositoryActivator.cs | 8 +- Kyoo.Tests/Library/RepositoryTests.cs | 4 +- .../Library/SpecificTests/SeasonTests.cs | 14 ++- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 20 +++- Kyoo.Tests/Library/TestContext.cs | 99 +++++++++++++------ 5 files changed, 106 insertions(+), 39 deletions(-) diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index df0be918..9f7f78fb 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -10,11 +10,13 @@ namespace Kyoo.Tests public ILibraryManager LibraryManager { get; } - private readonly DatabaseContext _database; + private readonly DatabaseContext _database; - public RepositoryActivator() + public RepositoryActivator(PostgresFixture postgres = null) { - Context = new TestContext(); + Context = postgres == null + ? new SqLiteTestContext() + : new PostgresTestContext(postgres); _database = Context.New(); ProviderRepository provider = new(_database); diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index 013eb6f2..78f52cb5 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -16,9 +16,9 @@ namespace Kyoo.Tests protected readonly RepositoryActivator Repositories; private readonly IRepository _repository; - protected RepositoryTests() + protected RepositoryTests(RepositoryActivator repositories) { - Repositories = new RepositoryActivator(); + Repositories = repositories; _repository = Repositories.LibraryManager.GetRepository(); Repositories.Context.AddTest(); if (new T() is not Show) diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index 07db9d97..04a493a9 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -5,11 +5,19 @@ using Xunit; namespace Kyoo.Tests.SpecificTests { - public class SeasonTests : RepositoryTests + public class SqLiteSeasonTests : SeasonTests + { + public SqLiteSeasonTests() + : base(new RepositoryActivator(true)) + { } + } + + public abstract class SeasonTests : RepositoryTests { private readonly ISeasonRepository _repository; - public SeasonTests() + protected SeasonTests(RepositoryActivator repositories) + : base(repositories) { _repository = Repositories.LibraryManager.SeasonRepository; } @@ -34,7 +42,7 @@ namespace Kyoo.Tests.SpecificTests { Season season = await _repository.Get(1); Assert.Equal("anohana-s1", season.Slug); - season = await _repository.Edit(new Season + await _repository.Edit(new Season { ID = 1, SeasonNumber = 2 diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index 9c0b5138..ea2146fc 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -9,11 +9,27 @@ using Xunit; namespace Kyoo.Tests.SpecificTests { - public class ShowTests : RepositoryTests + public class SqLiteShowTests : ShowTests + { + public SqLiteShowTests() + : base(new RepositoryActivator(null)) + { } + } + + [Collection(nameof(Postgresql))] + public class PostgresShowTests : ShowTests + { + public PostgresShowTests(PostgresFixture postgres) + : base(new RepositoryActivator(postgres)) + { } + } + + public abstract class ShowTests : RepositoryTests { private readonly IShowRepository _repository; - public ShowTests() + protected ShowTests(RepositoryActivator repositories) + : base(repositories) { _repository = Repositories.LibraryManager.ShowRepository; } diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 30f8beb5..8a1d1a64 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -3,39 +3,90 @@ using System.Threading.Tasks; using Kyoo.SqLite; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Xunit; namespace Kyoo.Tests { - /// - /// Class responsible to fill and create in memory databases for unit tests. - /// - public class TestContext : IDisposable, IAsyncDisposable + public sealed class SqLiteTestContext : TestContext { - /// - /// The context's options that specify to use an in memory Sqlite database. - /// - private readonly DbContextOptions _context; - /// /// The internal sqlite connection used by all context returned by this class. /// private readonly SqliteConnection _connection; - - /// - /// Create a new database and fill it with information. - /// - public TestContext() + + public SqLiteTestContext() { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); - - _context = new DbContextOptionsBuilder() + + Context = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; using DatabaseContext context = New(); context.Database.Migrate(); } + + public override void Dispose() + { + _connection.Close(); + } + + public override async ValueTask DisposeAsync() + { + await _connection.CloseAsync(); + } + + public override DatabaseContext New() + { + return new SqLiteContext(Context); + } + } + + [CollectionDefinition(nameof(Postgresql))] + public class PostgresCollection : ICollectionFixture + {} + + public class PostgresFixture + { + + } + + public sealed class PostgresTestContext : TestContext + { + private readonly PostgresFixture _template; + + public PostgresTestContext(PostgresFixture template) + { + _template = template; + } + + public override void Dispose() + { + throw new NotImplementedException(); + } + + public override ValueTask DisposeAsync() + { + throw new NotImplementedException(); + } + + public override DatabaseContext New() + { + throw new NotImplementedException(); + } + } + + + /// + /// Class responsible to fill and create in memory databases for unit tests. + /// + public abstract class TestContext : IDisposable, IAsyncDisposable + { + /// + /// The context's options that specify to use an in memory Sqlite database. + /// + protected DbContextOptions Context; /// /// Fill the database with pre defined values using a clean context. @@ -85,20 +136,10 @@ namespace Kyoo.Tests /// Get a new database context connected to a in memory Sqlite database. /// /// A valid DatabaseContext - public DatabaseContext New() - { - return new SqLiteContext(_context); - } + public abstract DatabaseContext New(); - public void Dispose() - { - _connection.Close(); - GC.SuppressFinalize(this); - } + public abstract void Dispose(); - public async ValueTask DisposeAsync() - { - await _connection.CloseAsync(); - } + public abstract ValueTask DisposeAsync(); } } From 6252d4f82c799f6e35bebf68a3d0f88a6a101dc3 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 15 Jun 2021 23:17:10 +0200 Subject: [PATCH 21/57] Fixing basics issues --- Kyoo.Tests/Library/SpecificTests/SeasonTests.cs | 2 +- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index 04a493a9..7690416a 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -8,7 +8,7 @@ namespace Kyoo.Tests.SpecificTests public class SqLiteSeasonTests : SeasonTests { public SqLiteSeasonTests() - : base(new RepositoryActivator(true)) + : base(new RepositoryActivator()) { } } diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index ea2146fc..33977be2 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -12,7 +12,7 @@ namespace Kyoo.Tests.SpecificTests public class SqLiteShowTests : ShowTests { public SqLiteShowTests() - : base(new RepositoryActivator(null)) + : base(new RepositoryActivator()) { } } From 9ed51d11ccc84b46de14b35ec5d157387086e2b7 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 16 Jun 2021 21:27:35 +0200 Subject: [PATCH 22/57] Adding postgres context for tests --- Kyoo.Tests/Library/RepositoryTests.cs | 3 - Kyoo.Tests/Library/TestContext.cs | 105 +++++++++++++++++--------- Kyoo.Tests/Library/TestSample.cs | 9 +++ 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index 78f52cb5..f8b5b7a4 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -20,9 +20,6 @@ namespace Kyoo.Tests { Repositories = repositories; _repository = Repositories.LibraryManager.GetRepository(); - Repositories.Context.AddTest(); - if (new T() is not Show) - Repositories.Context.AddTest(); } [Fact] diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 8a1d1a64..ed3e46c4 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -1,8 +1,10 @@ using System; using System.Threading.Tasks; +using Kyoo.Postgresql; using Kyoo.SqLite; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Npgsql; using Xunit; namespace Kyoo.Tests @@ -13,18 +15,24 @@ namespace Kyoo.Tests /// The internal sqlite connection used by all context returned by this class. /// private readonly SqliteConnection _connection; + + /// + /// The context's options that specify to use an in memory Sqlite database. + /// + private readonly DbContextOptions _context; public SqLiteTestContext() { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); - Context = new DbContextOptionsBuilder() + _context = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; using DatabaseContext context = New(); - context.Database.Migrate(); + context.Database.EnsureCreated(); + TestSample.FillDatabase(context); } public override void Dispose() @@ -39,7 +47,7 @@ namespace Kyoo.Tests public override DatabaseContext New() { - return new SqLiteContext(Context); + return new SqLiteContext(_context); } } @@ -47,33 +55,83 @@ namespace Kyoo.Tests public class PostgresCollection : ICollectionFixture {} - public class PostgresFixture + public sealed class PostgresFixture : IDisposable { + private readonly PostgresContext _context; + public string Template { get; } + + public string Connection => PostgresTestContext.GetConnectionString(Template); + + public PostgresFixture() + { + string id = Guid.NewGuid().ToString().Replace('-', '_'); + Template = $"kyoo_template_{id}"; + + DbContextOptions options = new DbContextOptionsBuilder() + .UseNpgsql(Connection) + .Options; + + _context = new PostgresContext(options); + _context.Database.EnsureCreated(); + TestSample.FillDatabase(_context); + _context.Database.CloseConnection(); + } + + public void Dispose() + { + _context.Database.EnsureDeleted(); + _context.Dispose(); + } } public sealed class PostgresTestContext : TestContext { - private readonly PostgresFixture _template; + private readonly NpgsqlConnection _connection; + private readonly DbContextOptions _context; public PostgresTestContext(PostgresFixture template) { - _template = template; + string id = Guid.NewGuid().ToString().Replace('-', '_'); + string database = $"kyoo_test_{id}"; + + using (NpgsqlConnection connection = new(template.Connection)) + { + connection.Open(); + using NpgsqlCommand cmd = new($"CREATE DATABASE {database} WITH TEMPLATE {template.Template}", connection); + cmd.ExecuteNonQuery(); + } + + _connection = new NpgsqlConnection(GetConnectionString(database)); + _connection.Open(); + + _context = new DbContextOptionsBuilder() + .UseNpgsql(_connection) + .Options; + } + + public static string GetConnectionString(string database) + { + return $"Server=127.0.0.1;Port=5432;Database={database};User ID=kyoo;Password=kyooPassword"; } public override void Dispose() { - throw new NotImplementedException(); + using DatabaseContext db = New(); + db.Database.EnsureDeleted(); + _connection.Close(); } - public override ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { - throw new NotImplementedException(); + await using DatabaseContext db = New(); + await db.Database.EnsureDeletedAsync(); + await _connection.CloseAsync(); } public override DatabaseContext New() { - throw new NotImplementedException(); + return new PostgresContext(_context); } } @@ -83,33 +141,6 @@ namespace Kyoo.Tests /// public abstract class TestContext : IDisposable, IAsyncDisposable { - /// - /// The context's options that specify to use an in memory Sqlite database. - /// - protected DbContextOptions Context; - - /// - /// Fill the database with pre defined values using a clean context. - /// - public void AddTest() - where T : class - { - using DatabaseContext context = New(); - context.Set().Add(TestSample.Get()); - context.SaveChanges(); - } - - /// - /// Fill the database with pre defined values using a clean context. - /// - public async Task AddTestAsync() - where T : class - { - await using DatabaseContext context = New(); - await context.Set().AddAsync(TestSample.Get()); - await context.SaveChangesAsync(); - } - /// /// Add an arbitrary data to the test context. /// diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 7abf1ed1..6a729286 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -84,5 +84,14 @@ namespace Kyoo.Tests { return (T)Samples[typeof(T)](); } + + public static void FillDatabase(DatabaseContext context) + { + context.Shows.Add(Get()); + context.Seasons.Add(Get()); + // context.Episodes.Add(Get()); + // context.People.Add(Get()); + context.SaveChanges(); + } } } \ No newline at end of file From 7bd78bfaac9f9457fb5f24da4bea75504f010de2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 16 Jun 2021 23:16:33 +0200 Subject: [PATCH 23/57] Fixing some postgres tests --- ....cs => 20210616203804_Initial.Designer.cs} | 13 +++++++++-- ...3_Initial.cs => 20210616203804_Initial.cs} | 3 +++ .../PostgresContextModelSnapshot.cs | 11 +++++++++- Kyoo.Postgresql/PostgresContext.cs | 16 +++++++------- Kyoo.Tests/Library/TestContext.cs | 22 +++++++++++-------- 5 files changed, 45 insertions(+), 20 deletions(-) rename Kyoo.Postgresql/Migrations/{20210607202403_Initial.Designer.cs => 20210616203804_Initial.Designer.cs} (98%) rename Kyoo.Postgresql/Migrations/{20210607202403_Initial.cs => 20210616203804_Initial.cs} (99%) diff --git a/Kyoo.Postgresql/Migrations/20210607202403_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210616203804_Initial.Designer.cs similarity index 98% rename from Kyoo.Postgresql/Migrations/20210607202403_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210616203804_Initial.Designer.cs index fe975800..83f90e5c 100644 --- a/Kyoo.Postgresql/Migrations/20210607202403_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210616203804_Initial.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210607202403_Initial")] + [Migration("20210616203804_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -23,7 +23,7 @@ namespace Kyoo.Postgresql.Migrations .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.6") + .HasAnnotation("ProductVersion", "5.0.7") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("Kyoo.Models.Collection", b => @@ -85,6 +85,9 @@ namespace Kyoo.Postgresql.Migrations b.Property("ShowID") .HasColumnType("integer"); + b.Property("Slug") + .HasColumnType("text"); + b.Property("Thumb") .HasColumnType("text"); @@ -428,6 +431,9 @@ namespace Kyoo.Postgresql.Migrations b.Property("ShowID") .HasColumnType("integer"); + b.Property("Slug") + .HasColumnType("text"); + b.Property("StartDate") .HasColumnType("timestamp without time zone"); @@ -552,6 +558,9 @@ namespace Kyoo.Postgresql.Migrations b.Property("Path") .HasColumnType("text"); + b.Property("Slug") + .HasColumnType("text"); + b.Property("Title") .HasColumnType("text"); diff --git a/Kyoo.Postgresql/Migrations/20210607202403_Initial.cs b/Kyoo.Postgresql/Migrations/20210616203804_Initial.cs similarity index 99% rename from Kyoo.Postgresql/Migrations/20210607202403_Initial.cs rename to Kyoo.Postgresql/Migrations/20210616203804_Initial.cs index 18df2ec7..94367a9d 100644 --- a/Kyoo.Postgresql/Migrations/20210607202403_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210616203804_Initial.cs @@ -386,6 +386,7 @@ namespace Kyoo.Postgresql.Migrations { ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: true), ShowID = table.Column(type: "integer", nullable: false), SeasonNumber = table.Column(type: "integer", nullable: false), Title = table.Column(type: "text", nullable: true), @@ -411,6 +412,7 @@ namespace Kyoo.Postgresql.Migrations { ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: true), ShowID = table.Column(type: "integer", nullable: false), SeasonID = table.Column(type: "integer", nullable: true), SeasonNumber = table.Column(type: "integer", nullable: false), @@ -497,6 +499,7 @@ namespace Kyoo.Postgresql.Migrations { ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: true), Title = table.Column(type: "text", nullable: true), Language = table.Column(type: "text", nullable: true), Codec = table.Column(type: "text", nullable: true), diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 0e8a675c..a88a7ab2 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -21,7 +21,7 @@ namespace Kyoo.Postgresql.Migrations .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.6") + .HasAnnotation("ProductVersion", "5.0.7") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("Kyoo.Models.Collection", b => @@ -83,6 +83,9 @@ namespace Kyoo.Postgresql.Migrations b.Property("ShowID") .HasColumnType("integer"); + b.Property("Slug") + .HasColumnType("text"); + b.Property("Thumb") .HasColumnType("text"); @@ -426,6 +429,9 @@ namespace Kyoo.Postgresql.Migrations b.Property("ShowID") .HasColumnType("integer"); + b.Property("Slug") + .HasColumnType("text"); + b.Property("StartDate") .HasColumnType("timestamp without time zone"); @@ -550,6 +556,9 @@ namespace Kyoo.Postgresql.Migrations b.Property("Path") .HasColumnType("text"); + b.Property("Slug") + .HasColumnType("text"); + b.Property("Title") .HasColumnType("text"); diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index 4836601c..c6efa395 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -26,16 +26,19 @@ namespace Kyoo.Postgresql /// Should the configure step be skipped? This is used when the database is created via DbContextOptions. /// private readonly bool _skipConfigure; - - /// - /// A basic constructor that set default values (query tracker behaviors, mapping enums...) - /// - public PostgresContext() + + + static PostgresContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); } + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + public PostgresContext() { } /// /// Create a new using specific options @@ -44,9 +47,6 @@ namespace Kyoo.Postgresql public PostgresContext(DbContextOptions options) : base(options) { - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); _skipConfigure = true; } diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index ed3e46c4..0ecb4637 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -31,7 +31,7 @@ namespace Kyoo.Tests .Options; using DatabaseContext context = New(); - context.Database.EnsureCreated(); + context.Database.Migrate(); TestSample.FillDatabase(context); } @@ -57,7 +57,7 @@ namespace Kyoo.Tests public sealed class PostgresFixture : IDisposable { - private readonly PostgresContext _context; + private readonly DbContextOptions _options; public string Template { get; } @@ -68,20 +68,24 @@ namespace Kyoo.Tests string id = Guid.NewGuid().ToString().Replace('-', '_'); Template = $"kyoo_template_{id}"; - DbContextOptions options = new DbContextOptionsBuilder() + _options = new DbContextOptionsBuilder() .UseNpgsql(Connection) .Options; - _context = new PostgresContext(options); - _context.Database.EnsureCreated(); - TestSample.FillDatabase(_context); - _context.Database.CloseConnection(); + using PostgresContext context = new(_options); + context.Database.Migrate(); + + using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection(); + conn.Open(); + conn.ReloadTypes(); + + TestSample.FillDatabase(context); } public void Dispose() { - _context.Database.EnsureDeleted(); - _context.Dispose(); + using PostgresContext context = new(_options); + context.Database.EnsureDeleted(); } } From b8d4a2ed3363f895791d0683018427a55b51d081 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 16 Jun 2021 23:24:21 +0200 Subject: [PATCH 24/57] Fixing database removal --- Kyoo.Tests/Library/RepositoryTests.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs index f8b5b7a4..51db3061 100644 --- a/Kyoo.Tests/Library/RepositoryTests.cs +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -10,7 +10,7 @@ using Xunit; namespace Kyoo.Tests { - public abstract class RepositoryTests + public abstract class RepositoryTests : IDisposable, IAsyncDisposable where T : class, IResource, new() { protected readonly RepositoryActivator Repositories; @@ -21,7 +21,17 @@ namespace Kyoo.Tests Repositories = repositories; _repository = Repositories.LibraryManager.GetRepository(); } - + + public void Dispose() + { + Repositories.Dispose(); + } + + public ValueTask DisposeAsync() + { + return Repositories.DisposeAsync(); + } + [Fact] public async Task FillTest() { From e91083afa8b6976a5da2e970e860f8c12f2e9cd8 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 20 Jun 2021 13:54:51 +0200 Subject: [PATCH 25/57] Fixing test database fill --- Kyoo.CommonAPI/DatabaseContext.cs | 17 ++++++++ Kyoo.Postgresql/Kyoo.Postgresql.csproj | 12 +++--- ....cs => 20210619153358_Initial.Designer.cs} | 5 ++- ...4_Initial.cs => 20210619153358_Initial.cs} | 2 +- .../PostgresContextModelSnapshot.cs | 3 +- ....cs => 20210619154617_Initial.Designer.cs} | 5 ++- ...7_Initial.cs => 20210619154617_Initial.cs} | 2 +- .../20210619154654_Triggers.Designer.cs} | 5 ++- .../20210619154654_Triggers.cs} | 32 +++++++-------- .../Migrations/SqLiteContextModelSnapshot.cs | 3 +- .../{GlobalTests.cs => SanityTests.cs} | 21 ---------- .../Library/SpecificTests/SeasonTests.cs | 8 ++++ Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 16 ++++++++ Kyoo.Tests/Library/TestContext.cs | 7 +++- Kyoo.Tests/Library/TestSample.cs | 40 ++++++++++++++++--- .../Repositories/SeasonRepository.cs | 8 +--- .../Repositories/ShowRepository.cs | 2 +- Kyoo/Startup.cs | 5 ++- 18 files changed, 124 insertions(+), 69 deletions(-) rename Kyoo.Postgresql/Migrations/{20210616203804_Initial.Designer.cs => 20210619153358_Initial.Designer.cs} (99%) rename Kyoo.Postgresql/Migrations/{20210616203804_Initial.cs => 20210619153358_Initial.cs} (99%) rename Kyoo.SqLite/Migrations/{20210613135157_Initial.Designer.cs => 20210619154617_Initial.Designer.cs} (99%) rename Kyoo.SqLite/Migrations/{20210613135157_Initial.cs => 20210619154617_Initial.cs} (99%) rename Kyoo.SqLite/{Triggers/TriggersMigrations.Designer.cs => Migrations/20210619154654_Triggers.Designer.cs} (99%) rename Kyoo.SqLite/{Triggers/TriggersMigrations.cs => Migrations/20210619154654_Triggers.cs} (57%) rename Kyoo.Tests/Library/SpecificTests/{GlobalTests.cs => SanityTests.cs} (51%) diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index f3d7f867..cb6762b0 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -140,6 +140,23 @@ namespace Kyoo .Property(t => t.IsForced) .ValueGeneratedNever(); + 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() .HasMany(x => x.Libraries) .WithMany(x => x.Providers) diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 1add3667..d4fbfb84 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -27,14 +27,14 @@ - all - false - runtime + + + - all - false - runtime + + + diff --git a/Kyoo.Postgresql/Migrations/20210616203804_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210619153358_Initial.Designer.cs similarity index 99% rename from Kyoo.Postgresql/Migrations/20210616203804_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210619153358_Initial.Designer.cs index 83f90e5c..a4228834 100644 --- a/Kyoo.Postgresql/Migrations/20210616203804_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210619153358_Initial.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210616203804_Initial")] + [Migration("20210619153358_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -634,7 +634,8 @@ namespace Kyoo.Postgresql.Migrations { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") - .HasForeignKey("SeasonID"); + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") diff --git a/Kyoo.Postgresql/Migrations/20210616203804_Initial.cs b/Kyoo.Postgresql/Migrations/20210619153358_Initial.cs similarity index 99% rename from Kyoo.Postgresql/Migrations/20210616203804_Initial.cs rename to Kyoo.Postgresql/Migrations/20210619153358_Initial.cs index 94367a9d..d6d70251 100644 --- a/Kyoo.Postgresql/Migrations/20210616203804_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210619153358_Initial.cs @@ -432,7 +432,7 @@ namespace Kyoo.Postgresql.Migrations column: x => x.SeasonID, principalTable: "Seasons", principalColumn: "ID", - onDelete: ReferentialAction.Restrict); + onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Episodes_Shows_ShowID", column: x => x.ShowID, diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index a88a7ab2..f4ef9dd2 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -632,7 +632,8 @@ namespace Kyoo.Postgresql.Migrations { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") - .HasForeignKey("SeasonID"); + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") diff --git a/Kyoo.SqLite/Migrations/20210613135157_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210619154617_Initial.Designer.cs similarity index 99% rename from Kyoo.SqLite/Migrations/20210613135157_Initial.Designer.cs rename to Kyoo.SqLite/Migrations/20210619154617_Initial.Designer.cs index 188c7b25..6dc81b0e 100644 --- a/Kyoo.SqLite/Migrations/20210613135157_Initial.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210619154617_Initial.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210613135157_Initial")] + [Migration("20210619154617_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -614,7 +614,8 @@ namespace Kyoo.SqLite.Migrations { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") - .HasForeignKey("SeasonID"); + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") diff --git a/Kyoo.SqLite/Migrations/20210613135157_Initial.cs b/Kyoo.SqLite/Migrations/20210619154617_Initial.cs similarity index 99% rename from Kyoo.SqLite/Migrations/20210613135157_Initial.cs rename to Kyoo.SqLite/Migrations/20210619154617_Initial.cs index bece453b..a2bbf56f 100644 --- a/Kyoo.SqLite/Migrations/20210613135157_Initial.cs +++ b/Kyoo.SqLite/Migrations/20210619154617_Initial.cs @@ -424,7 +424,7 @@ namespace Kyoo.SqLite.Migrations column: x => x.SeasonID, principalTable: "Seasons", principalColumn: "ID", - onDelete: ReferentialAction.Restrict); + onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Episodes_Shows_ShowID", column: x => x.ShowID, diff --git a/Kyoo.SqLite/Triggers/TriggersMigrations.Designer.cs b/Kyoo.SqLite/Migrations/20210619154654_Triggers.Designer.cs similarity index 99% rename from Kyoo.SqLite/Triggers/TriggersMigrations.Designer.cs rename to Kyoo.SqLite/Migrations/20210619154654_Triggers.Designer.cs index 83bbdcd1..01d5e928 100644 --- a/Kyoo.SqLite/Triggers/TriggersMigrations.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210619154654_Triggers.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210613135215_Triggers")] + [Migration("20210619154654_Triggers")] partial class Triggers { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -614,7 +614,8 @@ namespace Kyoo.SqLite.Migrations { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") - .HasForeignKey("SeasonID"); + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") diff --git a/Kyoo.SqLite/Triggers/TriggersMigrations.cs b/Kyoo.SqLite/Migrations/20210619154654_Triggers.cs similarity index 57% rename from Kyoo.SqLite/Triggers/TriggersMigrations.cs rename to Kyoo.SqLite/Migrations/20210619154654_Triggers.cs index 242cb4a1..2dce467d 100644 --- a/Kyoo.SqLite/Triggers/TriggersMigrations.cs +++ b/Kyoo.SqLite/Migrations/20210619154654_Triggers.cs @@ -2,34 +2,34 @@ namespace Kyoo.SqLite.Migrations { - public partial class Triggers : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql(@" + public partial class Triggers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW BEGIN UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber WHERE ID == new.ID; END"); - migrationBuilder.Sql(@" + migrationBuilder.Sql(@" CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW BEGIN UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber WHERE ID == new.ID; END"); - migrationBuilder.Sql(@" + migrationBuilder.Sql(@" CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW BEGIN UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; END;"); - } + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;"); - migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;"); - migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;"); - } - } -} + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;"); + migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;"); + migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;"); + } + } +} \ No newline at end of file diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs index 16c6105a..a03f5cbe 100644 --- a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs +++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs @@ -612,7 +612,8 @@ namespace Kyoo.SqLite.Migrations { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") - .HasForeignKey("SeasonID"); + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") diff --git a/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs similarity index 51% rename from Kyoo.Tests/Library/SpecificTests/GlobalTests.cs rename to Kyoo.Tests/Library/SpecificTests/SanityTests.cs index 57b8c4c3..7d7794fa 100644 --- a/Kyoo.Tests/Library/SpecificTests/GlobalTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs @@ -23,27 +23,6 @@ namespace Kyoo.Tests.SpecificTests Assert.False(ReferenceEquals(TestSample.Get(), TestSample.Get())); } - [Fact] - public async Task DeleteShowWithEpisodeAndSeason() - { - Show show = TestSample.Get(); - show.Seasons = new[] - { - TestSample.Get() - }; - show.Seasons.First().Episodes = new[] - { - TestSample.Get() - }; - await _repositories.Context.AddAsync(show); - - Assert.Equal(1, await _repositories.LibraryManager.ShowRepository.GetCount()); - await _repositories.LibraryManager.ShowRepository.Delete(show); - Assert.Equal(0, await _repositories.LibraryManager.ShowRepository.GetCount()); - Assert.Equal(0, await _repositories.LibraryManager.SeasonRepository.GetCount()); - Assert.Equal(0, await _repositories.LibraryManager.EpisodeRepository.GetCount()); - } - public void Dispose() { _repositories.Dispose(); diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index 7690416a..bb529e05 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -12,6 +12,14 @@ namespace Kyoo.Tests.SpecificTests { } } + [Collection(nameof(Postgresql))] + public class PostgresSeasonTests : SeasonTests + { + public PostgresSeasonTests(PostgresFixture postgres) + : base(new RepositoryActivator(postgres)) + { } + } + public abstract class SeasonTests : RepositoryTests { private readonly ISeasonRepository _repository; diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index 33977be2..2f1bf331 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -268,5 +268,21 @@ namespace Kyoo.Tests.SpecificTests ICollection ret = await _repository.Search(query); KAssert.DeepEqual(value, ret.First()); } + + [Fact] + public async Task DeleteShowWithEpisodeAndSeason() + { + Show show = TestSample.Get(); + await Repositories.LibraryManager.Load(show, x => x.Seasons); + await Repositories.LibraryManager.Load(show, x => x.Episodes); + Assert.Equal(1, await _repository.GetCount()); + Assert.Equal(1, show.Seasons.Count); + Assert.Equal(1, show.Episodes.Count); + await _repository.Delete(show); + Assert.Equal(0, await Repositories.LibraryManager.ShowRepository.GetCount()); + Assert.Equal(0, await Repositories.LibraryManager.SeasonRepository.GetCount()); + Assert.Equal(0, await Repositories.LibraryManager.EpisodeRepository.GetCount()); + } + } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 0ecb4637..1662e12c 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -4,6 +4,7 @@ using Kyoo.Postgresql; using Kyoo.SqLite; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Npgsql; using Xunit; @@ -80,6 +81,7 @@ namespace Kyoo.Tests conn.ReloadTypes(); TestSample.FillDatabase(context); + conn.Close(); } public void Dispose() @@ -111,12 +113,15 @@ namespace Kyoo.Tests _context = new DbContextOptionsBuilder() .UseNpgsql(_connection) + .UseLoggerFactory(LoggerFactory.Create(x => x.AddConsole())) + .EnableSensitiveDataLogging() + .EnableDetailedErrors() .Options; } public static string GetConnectionString(string database) { - return $"Server=127.0.0.1;Port=5432;Database={database};User ID=kyoo;Password=kyooPassword"; + return $"Server=127.0.0.1;Port=5432;Database={database};User ID=kyoo;Password=kyooPassword;Include Error Detail=true"; } public override void Dispose() diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 6a729286..3e5c57da 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -6,6 +6,15 @@ namespace Kyoo.Tests { public static class TestSample { + private static readonly Dictionary> NewSamples = new() + { + { + typeof(Show), + () => new Show() + } + }; + + private static readonly Dictionary> Samples = new() { { @@ -26,8 +35,8 @@ namespace Kyoo.Tests "school students, they had long ceased to think of each other as friends.", Status = Status.Finished, TrailerUrl = null, - StartAir = new DateTime(2011), - EndAir = new DateTime(2011), + StartAir = new DateTime(2011, 1, 1), + EndAir = new DateTime(2011, 1, 1), Poster = "poster", Logo = "logo", Backdrop = "backdrop", @@ -84,13 +93,32 @@ namespace Kyoo.Tests { return (T)Samples[typeof(T)](); } + + public static T GetNew() + { + return (T)NewSamples[typeof(T)](); + } public static void FillDatabase(DatabaseContext context) { - context.Shows.Add(Get()); - context.Seasons.Add(Get()); - // context.Episodes.Add(Get()); - // context.People.Add(Get()); + Show show = Get(); + show.ID = 0; + context.Shows.Add(show); + + Season season = Get(); + season.ID = 0; + season.ShowID = 0; + season.Show = show; + context.Seasons.Add(season); + + Episode episode = Get(); + episode.ID = 0; + episode.ShowID = 0; + episode.Show = show; + episode.SeasonID = 0; + episode.Season = season; + context.Episodes.Add(episode); + context.SaveChanges(); } } diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index d4529606..fe042e66 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -123,13 +123,9 @@ namespace Kyoo.Controllers { if (obj == null) throw new ArgumentNullException(nameof(obj)); - - _database.Entry(obj).State = EntityState.Deleted; - obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); + + _database.Remove(obj); await _database.SaveChangesAsync(); - // - // if (obj.Episodes != null) - // await _episodes.Value.DeleteRange(obj.Episodes); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 360d5d28..769cb232 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -181,7 +181,7 @@ namespace Kyoo.Controllers /// public override async Task Delete(Show obj) { - _database.Entry(obj).State = EntityState.Deleted; + _database.Remove(obj); await _database.SaveChangesAsync(); } } diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index e52aaee1..983d1d6c 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -4,6 +4,7 @@ using Kyoo.Authentication; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Options; +using Kyoo.Postgresql; using Kyoo.SqLite; using Kyoo.Tasks; using Microsoft.AspNetCore.Builder; @@ -46,8 +47,8 @@ namespace Kyoo // TODO remove postgres from here and load it like a normal plugin. _plugins.LoadPlugins(new IPlugin[] { new CoreModule(configuration), - // new PostgresModule(configuration, host), - new SqLiteModule(configuration, host), + new PostgresModule(configuration, host), + // new SqLiteModule(configuration, host), new AuthenticationModule(configuration, loggerFactory, host) }); } From 61df4ca61a49d40344cffcade770522bc500cc10 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 20 Jun 2021 14:01:24 +0200 Subject: [PATCH 26/57] Fixing postgresql search --- Kyoo.Postgresql/PostgresContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index c6efa395..a12d52fa 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -107,7 +107,7 @@ namespace Kyoo.Postgresql public override Expression> Like(Expression> query, string format) { MethodInfo iLike = MethodOfUtils.MethodOf(EF.Functions.ILike); - MethodCallExpression call = Expression.Call(iLike, query.Body, Expression.Constant(format)); + MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format)); return Expression.Lambda>(call, query.Parameters); } From cbb38e6fa1408880ed09173060461889064f4fed Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 20 Jun 2021 23:18:54 +0200 Subject: [PATCH 27/57] Starting postgres triggers --- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 2 +- .../20210620120239_Triggers.Designer.cs | 988 ++++++++++++++++++ .../Migrations/20210620120239_Triggers.cs | 57 + .../Library/SpecificTests/SeasonTests.cs | 3 + Kyoo.Tests/Library/TestContext.cs | 2 + 5 files changed, 1051 insertions(+), 1 deletion(-) create mode 100644 Kyoo.Postgresql/Migrations/20210620120239_Triggers.Designer.cs create mode 100644 Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index d4fbfb84..6c67a653 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.Designer.cs b/Kyoo.Postgresql/Migrations/20210620120239_Triggers.Designer.cs new file mode 100644 index 00000000..6b87547b --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210620120239_Triggers.Designer.cs @@ -0,0 +1,988 @@ +// +using System; +using System.Collections.Generic; +using Kyoo.Models; +using Kyoo.Postgresql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20210620120239_Triggers")] + partial class Triggers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) + .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) + .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.7") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AbsoluteNumber") + .HasColumnType("integer"); + + b.Property("EpisodeNumber") + .HasColumnType("integer"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SeasonID") + .HasColumnType("integer"); + + b.Property("SeasonNumber") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Slug") + .HasColumnType("text"); + + b.Property("Thumb") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Paths") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("DataID") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("DataID") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("DataID") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("DataID") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ForPeople") + .HasColumnType("boolean"); + + b.Property("PeopleID") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Logo") + .HasColumnType("text"); + + b.Property("LogoExtension") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("SeasonNumber") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Slug") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Aliases") + .HasColumnType("text[]"); + + b.Property("Backdrop") + .HasColumnType("text"); + + b.Property("EndAir") + .HasColumnType("timestamp without time zone"); + + b.Property("IsMovie") + .HasColumnType("boolean"); + + b.Property("Logo") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartAir") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("status"); + + b.Property("StudioID") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TrailerUrl") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Codec") + .HasColumnType("text"); + + b.Property("EpisodeID") + .HasColumnType("integer"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsForced") + .HasColumnType("boolean"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("Slug") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TrackIndex") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("stream_type"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Email") + .HasColumnType("text"); + + b.Property>("ExtraData") + .HasColumnType("jsonb"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("WatchedPercentage") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("GenreLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Navigation("LibraryLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs b/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs new file mode 100644 index 00000000..bdb354a8 --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Kyoo.Postgresql.Migrations +{ + public partial class Triggers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + CREATE FUNCTION season_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + NEW.""Slug"" := CONCAT( + (SELECT ""Slug"" FROM ""Shows"" WHERE ""ID"" = NEW.""ShowID""), + NEW.""ShowID"", + OLD.""SeasonNumber"", + NEW.""SeasonNumber"", + '-s', + NEW.""SeasonNumber"" + ); + NEW.""Poster"" := 'NICE'; + RETURN NEW; + END + $$;"); + + migrationBuilder.Sql(@" + CREATE TRIGGER ""SeasonSlug"" AFTER INSERT OR UPDATE OF ""SeasonNumber"", ""ShowID"" ON ""Seasons"" + FOR EACH ROW EXECUTE PROCEDURE season_slug_update();"); + + + migrationBuilder.Sql(@" + CREATE FUNCTION show_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE ""Seasons"" SET ""Slug"" = CONCAT(new.""Slug"", '-s', ""SeasonNumber"") WHERE ""ShowID"" = NEW.""ID""; + RETURN NEW; + END + $$;"); + + migrationBuilder.Sql(@" + CREATE TRIGGER ""ShowSlug"" AFTER UPDATE OF ""Slug"" ON ""Shows"" + FOR EACH ROW EXECUTE PROCEDURE show_slug_update();"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"DROP FUNCTION ""season_slug_update"";"); + migrationBuilder.Sql(@"DROP TRIGGER ""SeasonSlug"";"); + migrationBuilder.Sql(@"DROP FUNCTION ""show_slug_update"";"); + migrationBuilder.Sql(@"DROP TRIGGER ""ShowSlug"";"); + } + } +} diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index bb529e05..820088d9 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -56,7 +56,10 @@ namespace Kyoo.Tests.SpecificTests SeasonNumber = 2 }, false); season = await _repository.Get(1); + Assert.Equal("anohana-s2_NICE", season.Slug + "_" + season.Poster); Assert.Equal("anohana-s2", season.Slug); } + + //TODO test insert trigger } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 1662e12c..37e1d7b8 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -66,6 +66,8 @@ namespace Kyoo.Tests public PostgresFixture() { + // TODO Assert.Skip when postgres is not available. (this needs xunit v3) + string id = Guid.NewGuid().ToString().Replace('-', '_'); Template = $"kyoo_template_{id}"; From 2ec51b20e05ce96c4384956a9ce1376f17143183 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 20 Jun 2021 23:37:31 +0200 Subject: [PATCH 28/57] Creating an insert test --- Kyoo.Tests/Library/SpecificTests/SeasonTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index 820088d9..cf5abe75 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -60,6 +60,16 @@ namespace Kyoo.Tests.SpecificTests Assert.Equal("anohana-s2", season.Slug); } - //TODO test insert trigger + [Fact] + public async Task SeasonCreationSlugTest() + { + Season season = await _repository.Create(new Season + { + Show = TestSample.Get(), + SeasonNumber = 2 + }); + Assert.Equal($"{TestSample.Get().Slug}-s2_NICE", season.Slug + "_" + season.Poster); + Assert.Equal($"{TestSample.Get().Slug}-s2", season.Slug); + } } } \ No newline at end of file From bd82fbfb2c876eace5158abd02f7a948b794699e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 21 Jun 2021 01:10:28 +0200 Subject: [PATCH 29/57] Fixing edit test --- Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs | 2 +- Kyoo.Tests/Library/SpecificTests/SeasonTests.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs b/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs index bdb354a8..268ee108 100644 --- a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs @@ -26,7 +26,7 @@ namespace Kyoo.Postgresql.Migrations $$;"); migrationBuilder.Sql(@" - CREATE TRIGGER ""SeasonSlug"" AFTER INSERT OR UPDATE OF ""SeasonNumber"", ""ShowID"" ON ""Seasons"" + CREATE TRIGGER ""SeasonSlug"" BEFORE INSERT OR UPDATE OF ""SeasonNumber"", ""ShowID"" ON ""Seasons"" FOR EACH ROW EXECUTE PROCEDURE season_slug_update();"); diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index cf5abe75..3e945251 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -56,7 +56,6 @@ namespace Kyoo.Tests.SpecificTests SeasonNumber = 2 }, false); season = await _repository.Get(1); - Assert.Equal("anohana-s2_NICE", season.Slug + "_" + season.Poster); Assert.Equal("anohana-s2", season.Slug); } @@ -65,7 +64,7 @@ namespace Kyoo.Tests.SpecificTests { Season season = await _repository.Create(new Season { - Show = TestSample.Get(), + ShowID = TestSample.Get().ID, SeasonNumber = 2 }); Assert.Equal($"{TestSample.Get().Slug}-s2_NICE", season.Slug + "_" + season.Poster); From d7d40aef241d93f4d481a818f1cfb1eba24e04a6 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 21 Jun 2021 21:59:54 +0200 Subject: [PATCH 30/57] Fixing seasons tests and running tests in parallel --- Kyoo.Common/Models/Resources/Episode.cs | 2 +- Kyoo.Common/Models/Resources/Season.cs | 3 +- Kyoo.Common/Models/Resources/Track.cs | 3 +- Kyoo.CommonAPI/DatabaseContext.cs | 19 +++++++++++ ....cs => 20210621175845_Initial.Designer.cs} | 14 +++++++- ...8_Initial.cs => 20210621175845_Initial.cs} | 18 ++++++++++ ...cs => 20210621175855_Triggers.Designer.cs} | 14 +++++++- ...Triggers.cs => 20210621175855_Triggers.cs} | 34 ++++++++----------- .../PostgresContextModelSnapshot.cs | 12 +++++++ ....cs => 20210621175330_Initial.Designer.cs} | 14 +++++++- ...7_Initial.cs => 20210621175330_Initial.cs} | 18 ++++++++++ ...cs => 20210621175342_Triggers.Designer.cs} | 14 +++++++- ...Triggers.cs => 20210621175342_Triggers.cs} | 0 .../Migrations/SqLiteContextModelSnapshot.cs | 12 +++++++ Kyoo.Tests/Kyoo.Tests.csproj | 1 + .../Library/SpecificTests/SeasonTests.cs | 8 ++--- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 5 ++- Kyoo.Tests/Library/TestContext.cs | 9 ++--- 18 files changed, 163 insertions(+), 37 deletions(-) rename Kyoo.Postgresql/Migrations/{20210619153358_Initial.Designer.cs => 20210621175845_Initial.Designer.cs} (98%) rename Kyoo.Postgresql/Migrations/{20210619153358_Initial.cs => 20210621175845_Initial.cs} (98%) rename Kyoo.Postgresql/Migrations/{20210620120239_Triggers.Designer.cs => 20210621175855_Triggers.Designer.cs} (98%) rename Kyoo.Postgresql/Migrations/{20210620120239_Triggers.cs => 20210621175855_Triggers.cs} (59%) rename Kyoo.SqLite/Migrations/{20210619154617_Initial.Designer.cs => 20210621175330_Initial.Designer.cs} (98%) rename Kyoo.SqLite/Migrations/{20210619154617_Initial.cs => 20210621175330_Initial.cs} (98%) rename Kyoo.SqLite/Migrations/{20210619154654_Triggers.Designer.cs => 20210621175342_Triggers.Designer.cs} (98%) rename Kyoo.SqLite/Migrations/{20210619154654_Triggers.cs => 20210621175342_Triggers.cs} (100%) diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 692e951c..e5a677d4 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -20,7 +20,7 @@ namespace Kyoo.Models public string Slug { get => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); - set + [UsedImplicitly] private set { Match match = Regex.Match(value, @"(?.*)-s(?\d*)e(?\d*)"); diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 35bb6ffe..142b7e24 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using JetBrains.Annotations; using Kyoo.Controllers; using Kyoo.Models.Attributes; @@ -18,7 +19,7 @@ namespace Kyoo.Models public string Slug { get => $"{ShowSlug}-s{SeasonNumber}"; - set + [UsedImplicitly] private set { Match match = Regex.Match(value, @"(?.*)-s(?\d*)"); diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 17736e07..21aa9d63 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using System.Text.RegularExpressions; +using JetBrains.Annotations; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -48,7 +49,7 @@ namespace Kyoo.Models }; return $"{EpisodeSlug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}"; } - set + [UsedImplicitly] private set { Match match = Regex.Match(value, @"(?.*)-s(?\d+)e(?\d+)" + @"(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..\w)?"); diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index cb6762b0..a2b7eeb2 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -302,15 +302,34 @@ namespace Kyoo 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(); } /// diff --git a/Kyoo.Postgresql/Migrations/20210619153358_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210621175845_Initial.Designer.cs similarity index 98% rename from Kyoo.Postgresql/Migrations/20210619153358_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210621175845_Initial.Designer.cs index a4228834..8b99205c 100644 --- a/Kyoo.Postgresql/Migrations/20210619153358_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210621175845_Initial.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210619153358_Initial")] + [Migration("20210621175845_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -86,6 +86,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("Thumb") @@ -98,6 +99,9 @@ namespace Kyoo.Postgresql.Migrations b.HasIndex("SeasonID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") .IsUnique(); @@ -432,6 +436,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("StartDate") @@ -442,6 +447,9 @@ namespace Kyoo.Postgresql.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber") .IsUnique(); @@ -559,6 +567,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("text"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("Title") @@ -572,6 +581,9 @@ namespace Kyoo.Postgresql.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") .IsUnique(); diff --git a/Kyoo.Postgresql/Migrations/20210619153358_Initial.cs b/Kyoo.Postgresql/Migrations/20210621175845_Initial.cs similarity index 98% rename from Kyoo.Postgresql/Migrations/20210619153358_Initial.cs rename to Kyoo.Postgresql/Migrations/20210621175845_Initial.cs index d6d70251..7fe643bd 100644 --- a/Kyoo.Postgresql/Migrations/20210619153358_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210621175845_Initial.cs @@ -564,6 +564,12 @@ namespace Kyoo.Postgresql.Migrations columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Episodes_Slug", + table: "Episodes", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Genres_Slug", table: "Genres", @@ -654,6 +660,12 @@ namespace Kyoo.Postgresql.Migrations columns: new[] { "ShowID", "SeasonNumber" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Seasons_Slug", + table: "Seasons", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Shows_Slug", table: "Shows", @@ -677,6 +689,12 @@ namespace Kyoo.Postgresql.Migrations columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Tracks_Slug", + table: "Tracks", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Users_Slug", table: "Users", diff --git a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.Designer.cs b/Kyoo.Postgresql/Migrations/20210621175855_Triggers.Designer.cs similarity index 98% rename from Kyoo.Postgresql/Migrations/20210620120239_Triggers.Designer.cs rename to Kyoo.Postgresql/Migrations/20210621175855_Triggers.Designer.cs index 6b87547b..6a4a0163 100644 --- a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210621175855_Triggers.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210620120239_Triggers")] + [Migration("20210621175855_Triggers")] partial class Triggers { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -86,6 +86,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("Thumb") @@ -98,6 +99,9 @@ namespace Kyoo.Postgresql.Migrations b.HasIndex("SeasonID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") .IsUnique(); @@ -432,6 +436,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("StartDate") @@ -442,6 +447,9 @@ namespace Kyoo.Postgresql.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber") .IsUnique(); @@ -559,6 +567,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("text"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("Title") @@ -572,6 +581,9 @@ namespace Kyoo.Postgresql.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") .IsUnique(); diff --git a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs b/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs similarity index 59% rename from Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs rename to Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs index 268ee108..6f6ed291 100644 --- a/Kyoo.Postgresql/Migrations/20210620120239_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs @@ -2,10 +2,10 @@ namespace Kyoo.Postgresql.Migrations { - public partial class Triggers : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { + public partial class Triggers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { migrationBuilder.Sql(@" CREATE FUNCTION season_slug_update() RETURNS TRIGGER @@ -14,13 +14,9 @@ namespace Kyoo.Postgresql.Migrations BEGIN NEW.""Slug"" := CONCAT( (SELECT ""Slug"" FROM ""Shows"" WHERE ""ID"" = NEW.""ShowID""), - NEW.""ShowID"", - OLD.""SeasonNumber"", - NEW.""SeasonNumber"", '-s', NEW.""SeasonNumber"" ); - NEW.""Poster"" := 'NICE'; RETURN NEW; END $$;"); @@ -41,17 +37,17 @@ namespace Kyoo.Postgresql.Migrations END $$;"); - migrationBuilder.Sql(@" + migrationBuilder.Sql(@" CREATE TRIGGER ""ShowSlug"" AFTER UPDATE OF ""Slug"" ON ""Shows"" FOR EACH ROW EXECUTE PROCEDURE show_slug_update();"); - } + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql(@"DROP FUNCTION ""season_slug_update"";"); - migrationBuilder.Sql(@"DROP TRIGGER ""SeasonSlug"";"); - migrationBuilder.Sql(@"DROP FUNCTION ""show_slug_update"";"); - migrationBuilder.Sql(@"DROP TRIGGER ""ShowSlug"";"); - } - } -} + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"DROP FUNCTION ""season_slug_update"";"); + migrationBuilder.Sql(@"DROP TRIGGER ""SeasonSlug"";"); + migrationBuilder.Sql(@"DROP FUNCTION ""show_slug_update"";"); + migrationBuilder.Sql(@"DROP TRIGGER ""ShowSlug"";"); + } + } +} \ No newline at end of file diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index f4ef9dd2..14f61708 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -84,6 +84,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("Thumb") @@ -96,6 +97,9 @@ namespace Kyoo.Postgresql.Migrations b.HasIndex("SeasonID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") .IsUnique(); @@ -430,6 +434,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("StartDate") @@ -440,6 +445,9 @@ namespace Kyoo.Postgresql.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber") .IsUnique(); @@ -557,6 +565,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("text"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("text"); b.Property("Title") @@ -570,6 +579,9 @@ namespace Kyoo.Postgresql.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") .IsUnique(); diff --git a/Kyoo.SqLite/Migrations/20210619154617_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210621175330_Initial.Designer.cs similarity index 98% rename from Kyoo.SqLite/Migrations/20210619154617_Initial.Designer.cs rename to Kyoo.SqLite/Migrations/20210621175330_Initial.Designer.cs index 6dc81b0e..26e94354 100644 --- a/Kyoo.SqLite/Migrations/20210619154617_Initial.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210621175330_Initial.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210619154617_Initial")] + [Migration("20210621175330_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -76,6 +76,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("INTEGER"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("Thumb") @@ -88,6 +89,9 @@ namespace Kyoo.SqLite.Migrations b.HasIndex("SeasonID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") .IsUnique(); @@ -416,6 +420,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("INTEGER"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("StartDate") @@ -426,6 +431,9 @@ namespace Kyoo.SqLite.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber") .IsUnique(); @@ -540,6 +548,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("TEXT"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("Title") @@ -553,6 +562,9 @@ namespace Kyoo.SqLite.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") .IsUnique(); diff --git a/Kyoo.SqLite/Migrations/20210619154617_Initial.cs b/Kyoo.SqLite/Migrations/20210621175330_Initial.cs similarity index 98% rename from Kyoo.SqLite/Migrations/20210619154617_Initial.cs rename to Kyoo.SqLite/Migrations/20210621175330_Initial.cs index a2bbf56f..fb73c02a 100644 --- a/Kyoo.SqLite/Migrations/20210619154617_Initial.cs +++ b/Kyoo.SqLite/Migrations/20210621175330_Initial.cs @@ -556,6 +556,12 @@ namespace Kyoo.SqLite.Migrations columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Episodes_Slug", + table: "Episodes", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Genres_Slug", table: "Genres", @@ -646,6 +652,12 @@ namespace Kyoo.SqLite.Migrations columns: new[] { "ShowID", "SeasonNumber" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Seasons_Slug", + table: "Seasons", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Shows_Slug", table: "Shows", @@ -669,6 +681,12 @@ namespace Kyoo.SqLite.Migrations columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Tracks_Slug", + table: "Tracks", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Users_Slug", table: "Users", diff --git a/Kyoo.SqLite/Migrations/20210619154654_Triggers.Designer.cs b/Kyoo.SqLite/Migrations/20210621175342_Triggers.Designer.cs similarity index 98% rename from Kyoo.SqLite/Migrations/20210619154654_Triggers.Designer.cs rename to Kyoo.SqLite/Migrations/20210621175342_Triggers.Designer.cs index 01d5e928..0f99af22 100644 --- a/Kyoo.SqLite/Migrations/20210619154654_Triggers.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210621175342_Triggers.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210619154654_Triggers")] + [Migration("20210621175342_Triggers")] partial class Triggers { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -76,6 +76,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("INTEGER"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("Thumb") @@ -88,6 +89,9 @@ namespace Kyoo.SqLite.Migrations b.HasIndex("SeasonID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") .IsUnique(); @@ -416,6 +420,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("INTEGER"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("StartDate") @@ -426,6 +431,9 @@ namespace Kyoo.SqLite.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber") .IsUnique(); @@ -540,6 +548,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("TEXT"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("Title") @@ -553,6 +562,9 @@ namespace Kyoo.SqLite.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") .IsUnique(); diff --git a/Kyoo.SqLite/Migrations/20210619154654_Triggers.cs b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs similarity index 100% rename from Kyoo.SqLite/Migrations/20210619154654_Triggers.cs rename to Kyoo.SqLite/Migrations/20210621175342_Triggers.cs diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs index a03f5cbe..fc0b3cbc 100644 --- a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs +++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs @@ -74,6 +74,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("INTEGER"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("Thumb") @@ -86,6 +87,9 @@ namespace Kyoo.SqLite.Migrations b.HasIndex("SeasonID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") .IsUnique(); @@ -414,6 +418,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("INTEGER"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("StartDate") @@ -424,6 +429,9 @@ namespace Kyoo.SqLite.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber") .IsUnique(); @@ -538,6 +546,7 @@ namespace Kyoo.SqLite.Migrations .HasColumnType("TEXT"); b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT"); b.Property("Title") @@ -551,6 +560,9 @@ namespace Kyoo.SqLite.Migrations b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") .IsUnique(); diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index 265ccb98..9d1e8311 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -16,6 +16,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index 3e945251..7f81bc3f 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; using Xunit; +using Xunit.Extensions.Ordering; namespace Kyoo.Tests.SpecificTests { @@ -11,9 +12,9 @@ namespace Kyoo.Tests.SpecificTests : base(new RepositoryActivator()) { } } - - [Collection(nameof(Postgresql))] - public class PostgresSeasonTests : SeasonTests + + + public class PostgresSeasonTests : SeasonTests, IAssemblyFixture { public PostgresSeasonTests(PostgresFixture postgres) : base(new RepositoryActivator(postgres)) @@ -67,7 +68,6 @@ namespace Kyoo.Tests.SpecificTests ShowID = TestSample.Get().ID, SeasonNumber = 2 }); - Assert.Equal($"{TestSample.Get().Slug}-s2_NICE", season.Slug + "_" + season.Poster); Assert.Equal($"{TestSample.Get().Slug}-s2", season.Slug); } } diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index 2f1bf331..d16f13e9 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -6,6 +6,7 @@ using Kyoo.Controllers; using Kyoo.Models; using Microsoft.EntityFrameworkCore; using Xunit; +using Xunit.Extensions.Ordering; namespace Kyoo.Tests.SpecificTests { @@ -16,8 +17,7 @@ namespace Kyoo.Tests.SpecificTests { } } - [Collection(nameof(Postgresql))] - public class PostgresShowTests : ShowTests + public class PostgresShowTests : ShowTests, IAssemblyFixture { public PostgresShowTests(PostgresFixture postgres) : base(new RepositoryActivator(postgres)) @@ -283,6 +283,5 @@ namespace Kyoo.Tests.SpecificTests Assert.Equal(0, await Repositories.LibraryManager.SeasonRepository.GetCount()); Assert.Equal(0, await Repositories.LibraryManager.EpisodeRepository.GetCount()); } - } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 37e1d7b8..82406454 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Logging; using Npgsql; using Xunit; +[assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")] + namespace Kyoo.Tests { public sealed class SqLiteTestContext : TestContext @@ -29,6 +31,9 @@ namespace Kyoo.Tests _context = new DbContextOptionsBuilder() .UseSqlite(_connection) + .UseLoggerFactory(LoggerFactory.Create(x => x.AddConsole())) + .EnableSensitiveDataLogging() + .EnableDetailedErrors() .Options; using DatabaseContext context = New(); @@ -52,10 +57,6 @@ namespace Kyoo.Tests } } - [CollectionDefinition(nameof(Postgresql))] - public class PostgresCollection : ICollectionFixture - {} - public sealed class PostgresFixture : IDisposable { private readonly DbContextOptions _options; From d2f774e32d9013987d6aebd132ae97fa8be59c73 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 21 Jun 2021 23:44:23 +0200 Subject: [PATCH 31/57] Cleaning namespaces and adding episodes tests --- Kyoo.Tests/Kyoo.Tests.csproj | 1 - .../Library/SpecificTests/EpisodeTest.cs | 32 +++++++++++++++++++ .../Library/SpecificTests/SanityTests.cs | 3 +- .../Library/SpecificTests/SeasonTests.cs | 30 +++++++++-------- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 32 +++++++++++-------- Kyoo.Tests/Library/TestContext.cs | 6 ++-- 6 files changed, 72 insertions(+), 32 deletions(-) create mode 100644 Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index 9d1e8311..265ccb98 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -16,7 +16,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs new file mode 100644 index 00000000..a7b3d743 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -0,0 +1,32 @@ +using Kyoo.Models; +using Xunit; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class EpisodeTests : AEpisodeTests + { + public EpisodeTests() + : base(new RepositoryActivator()) { } + } + } + + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class EpisodeTests : AEpisodeTests + { + public EpisodeTests(PostgresFixture postgres) + : base(new RepositoryActivator(postgres)) { } + } + } + + public abstract class AEpisodeTests : RepositoryTests + { + protected AEpisodeTests(RepositoryActivator repositories) + : base(repositories) + { } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs index 7d7794fa..098c4677 100644 --- a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs @@ -1,11 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading.Tasks; using Kyoo.Models; using Xunit; -namespace Kyoo.Tests.SpecificTests +namespace Kyoo.Tests.Library { public class GlobalTests : IDisposable, IAsyncDisposable { diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index 7f81bc3f..0c912d07 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -2,30 +2,34 @@ using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; using Xunit; -using Xunit.Extensions.Ordering; -namespace Kyoo.Tests.SpecificTests +namespace Kyoo.Tests.Library { - public class SqLiteSeasonTests : SeasonTests + namespace SqLite { - public SqLiteSeasonTests() - : base(new RepositoryActivator()) - { } + public class SeasonTests : ASeasonTests + { + public SeasonTests() + : base(new RepositoryActivator()) { } + } } - public class PostgresSeasonTests : SeasonTests, IAssemblyFixture + namespace PostgreSQL { - public PostgresSeasonTests(PostgresFixture postgres) - : base(new RepositoryActivator(postgres)) - { } + [Collection(nameof(Postgresql))] + public class SeasonTests : ASeasonTests + { + public SeasonTests(PostgresFixture postgres) + : base(new RepositoryActivator(postgres)) { } + } } - - public abstract class SeasonTests : RepositoryTests + + public abstract class ASeasonTests : RepositoryTests { private readonly ISeasonRepository _repository; - protected SeasonTests(RepositoryActivator repositories) + protected ASeasonTests(RepositoryActivator repositories) : base(repositories) { _repository = Repositories.LibraryManager.SeasonRepository; diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index d16f13e9..facb8e81 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -6,29 +6,33 @@ using Kyoo.Controllers; using Kyoo.Models; using Microsoft.EntityFrameworkCore; using Xunit; -using Xunit.Extensions.Ordering; -namespace Kyoo.Tests.SpecificTests +namespace Kyoo.Tests.Library { - public class SqLiteShowTests : ShowTests + namespace SqLite { - public SqLiteShowTests() - : base(new RepositoryActivator()) - { } + public class ShowTests : AShowTests + { + public ShowTests() + : base(new RepositoryActivator()) { } + } } - - public class PostgresShowTests : ShowTests, IAssemblyFixture + + namespace PostgreSQL { - public PostgresShowTests(PostgresFixture postgres) - : base(new RepositoryActivator(postgres)) - { } + [Collection(nameof(Postgresql))] + public class ShowTests : AShowTests + { + public ShowTests(PostgresFixture postgres) + : base(new RepositoryActivator(postgres)) { } + } } - - public abstract class ShowTests : RepositoryTests + + public abstract class AShowTests : RepositoryTests { private readonly IShowRepository _repository; - protected ShowTests(RepositoryActivator repositories) + protected AShowTests(RepositoryActivator repositories) : base(repositories) { _repository = Repositories.LibraryManager.ShowRepository; diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 82406454..61332a8d 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -8,8 +8,6 @@ using Microsoft.Extensions.Logging; using Npgsql; using Xunit; -[assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")] - namespace Kyoo.Tests { public sealed class SqLiteTestContext : TestContext @@ -56,6 +54,10 @@ namespace Kyoo.Tests return new SqLiteContext(_context); } } + + [CollectionDefinition(nameof(Postgresql))] + public class PostgresCollection : ICollectionFixture + {} public sealed class PostgresFixture : IDisposable { From 37c752229e5cd5c22e3559e90306e16699b84ae1 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 22 Jun 2021 23:19:32 +0200 Subject: [PATCH 32/57] Adding episodes triggers --- .../Models/Attributes/ComputedAttribute.cs | 10 +++ Kyoo.Common/Models/Resources/Episode.cs | 13 ++-- Kyoo.Common/Models/Resources/Season.cs | 13 ++-- Kyoo.Common/Models/Resources/Track.cs | 2 +- Kyoo.Common/Utility/Merger.cs | 2 +- Kyoo.CommonAPI/DatabaseContext.cs | 10 +++ Kyoo.CommonAPI/Kyoo.CommonAPI.csproj | 1 + Kyoo.CommonAPI/LocalRepository.cs | 2 + .../Migrations/20210621175342_Triggers.cs | 18 +++++ .../Library/SpecificTests/EpisodeTest.cs | 70 ++++++++++++++++++- .../Repositories/EpisodeRepository.cs | 9 ++- 11 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 Kyoo.Common/Models/Attributes/ComputedAttribute.cs diff --git a/Kyoo.Common/Models/Attributes/ComputedAttribute.cs b/Kyoo.Common/Models/Attributes/ComputedAttribute.cs new file mode 100644 index 00000000..b7f07048 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/ComputedAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + /// + /// An attribute to inform that the property is computed automatically and can't be assigned manually. + /// + [AttributeUsage(AttributeTargets.Property)] + public class ComputedAttribute : NotMergeableAttribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index e5a677d4..307ab115 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -17,12 +17,17 @@ namespace Kyoo.Models public int ID { get; set; } /// - public string Slug + [Computed] public string Slug { - get => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + get + { + if (ShowSlug == null && Show == null) + return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber); + return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + } [UsedImplicitly] private set { - Match match = Regex.Match(value, @"(?.*)-s(?\d*)e(?\d*)"); + Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); if (match.Success) { @@ -45,7 +50,7 @@ namespace Kyoo.Models } } } - + /// /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. /// diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 142b7e24..e3440670 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -16,12 +16,17 @@ namespace Kyoo.Models public int ID { get; set; } /// - public string Slug + [Computed] public string Slug { - get => $"{ShowSlug}-s{SeasonNumber}"; + get + { + if (ShowSlug == null && Show == null) + return $"{ShowID}-s{SeasonNumber}"; + return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}"; + } [UsedImplicitly] private set { - Match match = Regex.Match(value, @"(?.*)-s(?\d*)"); + Match match = Regex.Match(value ?? "", @"(?.+)-s(?\d+)"); if (!match.Success) throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); @@ -29,7 +34,7 @@ namespace Kyoo.Models SeasonNumber = int.Parse(match.Groups["season"].Value); } } - + /// /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. /// diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 21aa9d63..e0d543c2 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -29,7 +29,7 @@ namespace Kyoo.Models public int ID { get; set; } /// - public string Slug + [Computed] public string Slug { get { diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs index 047b5b09..d6378ca9 100644 --- a/Kyoo.Common/Utility/Merger.cs +++ b/Kyoo.Common/Utility/Merger.cs @@ -79,7 +79,7 @@ namespace Kyoo Type type = typeof(T); IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite + .Where(x => x.CanRead && x.CanWrite && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); if (where != null) diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index a2b7eeb2..2e425415 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -330,6 +330,16 @@ namespace Kyoo modelBuilder.Entity() .Property(x => x.Slug) .ValueGeneratedOnAddOrUpdate(); + + modelBuilder.Entity() + .Property(x => x.EpisodeNumber) + .HasDefaultValue(-1); + modelBuilder.Entity() + .Property(x => x.SeasonNumber) + .HasDefaultValue(-1); + modelBuilder.Entity() + .Property(x => x.AbsoluteNumber) + .HasDefaultValue(-1); } /// diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index f6add6b4..14d40203 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -14,6 +14,7 @@ + diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 9c23c774..8ab23443 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -257,6 +257,8 @@ namespace Kyoo.Controllers /// You can throw this if the resource is illegal and should not be saved. protected virtual Task Validate(T resource) { + if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute() != null) + return Task.CompletedTask; if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("Resource can't have null as a slug."); if (int.TryParse(resource.Slug, out int _)) diff --git a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs index 2dce467d..48bdb52a 100644 --- a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs @@ -18,10 +18,26 @@ namespace Kyoo.SqLite.Migrations UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber WHERE ID == new.ID; END"); + + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber || 'e' || EpisodeNumber + WHERE ID == new.ID; + END"); + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF EpisodeNumber, SeasonNumber, ShowID ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber || 'e' || EpisodeNumber + WHERE ID == new.ID; + END"); + + migrationBuilder.Sql(@" CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW BEGIN UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; + UPDATE Episodes SET Slug = new.Slug || '-s' || SeasonNumber || 'e' || EpisodeNumber WHERE ShowID = new.ID; END;"); } @@ -29,6 +45,8 @@ namespace Kyoo.SqLite.Migrations { migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;"); migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;"); + migrationBuilder.Sql("DROP TRIGGER EpisodeSlugInsert;"); + migrationBuilder.Sql("DROP TRIGGER EpisodeSlugUpdate;"); migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;"); } } diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index a7b3d743..d3603567 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; using Kyoo.Models; using Xunit; @@ -25,8 +27,70 @@ namespace Kyoo.Tests.Library public abstract class AEpisodeTests : RepositoryTests { - protected AEpisodeTests(RepositoryActivator repositories) - : base(repositories) - { } + private readonly IEpisodeRepository _repository; + + protected AEpisodeTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = repositories.LibraryManager.EpisodeRepository; + } + + [Fact] + public async Task SlugEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + Show show = new() + { + ID = episode.ShowID, + Slug = "new-slug" + }; + await Repositories.LibraryManager.ShowRepository.Edit(show, false); + episode = await _repository.Get(1); + Assert.Equal("new-slug-s1e1", episode.Slug); + } + + [Fact] + public async Task SeasonNumberEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + await _repository.Edit(new Episode + { + ID = 1, + SeasonNumber = 2 + }, false); + episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); + } + + [Fact] + public async Task EpisodeNumberEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + await _repository.Edit(new Episode + { + ID = 1, + EpisodeNumber = 2 + }, false); + episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + } + + [Fact] + public async Task EpisodeCreationSlugTest() + { + Episode season = await _repository.Create(new Episode + { + ShowID = TestSample.Get().ID, + SeasonNumber = 2, + EpisodeNumber = 4 + }); + Assert.Equal($"{TestSample.Get().Slug}-s2e4", season.Slug); + } + + + // TODO absolute numbering tests } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 5a01bd44..278e31b5 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -145,7 +145,7 @@ namespace Kyoo.Controllers /// The parameter is returned. private async Task ValidateTracks(Episode resource) { - resource.Tracks = await resource.Tracks.MapAsync((x, i) => + resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.MapAsync((x, i) => { x.Episode = resource; x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language @@ -153,7 +153,7 @@ namespace Kyoo.Controllers && x.Codec == y.Codec && x.Type == y.Type); return _tracks.Create(x); - }).ToListAsync(); + }).ToListAsync()); return resource; } @@ -161,13 +161,12 @@ namespace Kyoo.Controllers protected override async Task Validate(Episode resource) { await base.Validate(resource); - resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x => + await resource.ExternalIDs.ForEachAsync(async x => { x.Second = await _providers.CreateIfNotExists(x.Second); x.SecondID = x.Second.ID; _database.Entry(x.Second).State = EntityState.Detached; - return x; - }).ToListAsync(); + }); } /// From 8a3b9e1decca5841641d0a29982bfa9ef93a0706 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 23 Jun 2021 18:33:40 +0200 Subject: [PATCH 33/57] Fixing episodes defaults --- Kyoo.Common/Models/Resources/Episode.cs | 14 ++++++++++---- Kyoo.Common/Models/Resources/Season.cs | 2 +- Kyoo.Common/Utility/Merger.cs | 6 +++--- Kyoo.Common/Utility/Utility.cs | 12 ++++++++++++ Kyoo.CommonAPI/DatabaseContext.cs | 10 ---------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 307ab115..37cd51aa 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Text.RegularExpressions; using JetBrains.Annotations; using Kyoo.Controllers; @@ -25,8 +26,11 @@ namespace Kyoo.Models return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber); return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); } - [UsedImplicitly] private set + [UsedImplicitly] [NotNull] private set { + if (value == null) + throw new ArgumentNullException(nameof(value)); + Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); if (match.Success) @@ -76,19 +80,21 @@ namespace Kyoo.Models [LoadableRelation(nameof(SeasonID))] public Season Season { get; set; } /// - /// The season in witch this episode is in. This defaults to -1 if not specified. + /// The season in witch this episode is in. /// + [DefaultValue(-1)] public int SeasonNumber { get; set; } = -1; /// - /// The number of this episode is it's season. This defaults to -1 if not specified. + /// The number of this episode is it's season. /// + [DefaultValue(-1)] public int EpisodeNumber { get; set; } = -1; /// /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. - /// This defaults to -1 if not specified. /// + [DefaultValue(-1)] public int AbsoluteNumber { get; set; } = -1; /// diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index e3440670..028022b6 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -24,7 +24,7 @@ namespace Kyoo.Models return $"{ShowID}-s{SeasonNumber}"; return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}"; } - [UsedImplicitly] private set + [UsedImplicitly] [NotNull] private set { Match match = Regex.Match(value ?? "", @"(?.+)-s(?\d+)"); diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs index d6378ca9..417ea944 100644 --- a/Kyoo.Common/Utility/Merger.cs +++ b/Kyoo.Common/Utility/Merger.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -88,9 +89,8 @@ namespace Kyoo foreach (PropertyInfo property in properties) { object value = property.GetValue(second); - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; + object defaultValue = property.GetCustomAttribute()?.Value + ?? property.PropertyType.GetClrDefault(); if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) property.SetValue(first, value); diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs index 28699035..f7041c51 100644 --- a/Kyoo.Common/Utility/Utility.cs +++ b/Kyoo.Common/Utility/Utility.cs @@ -96,6 +96,18 @@ namespace Kyoo return str; } + /// + /// Get the default value of a type. + /// + /// The type to get the default value + /// The default value of the given type. + public static object GetClrDefault(this Type type) + { + return type.IsValueType + ? Activator.CreateInstance(type) + : null; + } + /// /// Return every in the inheritance tree of the parameter (interfaces are not returned) /// diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 2e425415..a2b7eeb2 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -330,16 +330,6 @@ namespace Kyoo modelBuilder.Entity() .Property(x => x.Slug) .ValueGeneratedOnAddOrUpdate(); - - modelBuilder.Entity() - .Property(x => x.EpisodeNumber) - .HasDefaultValue(-1); - modelBuilder.Entity() - .Property(x => x.SeasonNumber) - .HasDefaultValue(-1); - modelBuilder.Entity() - .Property(x => x.AbsoluteNumber) - .HasDefaultValue(-1); } /// From 27a69b5a345464e02e05c1794d96ea4e5183af59 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 23 Jun 2021 19:48:05 +0200 Subject: [PATCH 34/57] Adding language highlight --- .../Migrations/20210621175855_Triggers.cs | 20 +++++++++++++------ .../Migrations/20210621175342_Triggers.cs | 10 ++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs b/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs index 6f6ed291..25e3d51f 100644 --- a/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs @@ -6,6 +6,7 @@ namespace Kyoo.Postgresql.Migrations { protected override void Up(MigrationBuilder migrationBuilder) { + // language=PostgreSQL migrationBuilder.Sql(@" CREATE FUNCTION season_slug_update() RETURNS TRIGGER @@ -21,11 +22,13 @@ namespace Kyoo.Postgresql.Migrations END $$;"); + // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER ""SeasonSlug"" BEFORE INSERT OR UPDATE OF ""SeasonNumber"", ""ShowID"" ON ""Seasons"" + CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF ""SeasonNumber"", ""ShowID"" ON ""Seasons"" FOR EACH ROW EXECUTE PROCEDURE season_slug_update();"); + // language=PostgreSQL migrationBuilder.Sql(@" CREATE FUNCTION show_slug_update() RETURNS TRIGGER @@ -37,17 +40,22 @@ namespace Kyoo.Postgresql.Migrations END $$;"); + // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER ""ShowSlug"" AFTER UPDATE OF ""Slug"" ON ""Shows"" + CREATE TRIGGER show_slug_trigger AFTER UPDATE OF ""Slug"" ON ""Shows"" FOR EACH ROW EXECUTE PROCEDURE show_slug_update();"); } protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql(@"DROP FUNCTION ""season_slug_update"";"); - migrationBuilder.Sql(@"DROP TRIGGER ""SeasonSlug"";"); - migrationBuilder.Sql(@"DROP FUNCTION ""show_slug_update"";"); - migrationBuilder.Sql(@"DROP TRIGGER ""ShowSlug"";"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION season_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER show_slug_trigger ON \"Shows\";"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION show_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP TRIGGER season_slug_trigger;"); } } } \ No newline at end of file diff --git a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs index 48bdb52a..b6477eeb 100644 --- a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs @@ -6,12 +6,14 @@ namespace Kyoo.SqLite.Migrations { protected override void Up(MigrationBuilder migrationBuilder) { + // language=SQLite migrationBuilder.Sql(@" CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW BEGIN UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber WHERE ID == new.ID; END"); + // language=SQLite migrationBuilder.Sql(@" CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW BEGIN @@ -19,12 +21,14 @@ namespace Kyoo.SqLite.Migrations WHERE ID == new.ID; END"); + // language=SQLite migrationBuilder.Sql(@" CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW BEGIN UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber || 'e' || EpisodeNumber WHERE ID == new.ID; END"); + // language=SQLite migrationBuilder.Sql(@" CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF EpisodeNumber, SeasonNumber, ShowID ON Episodes FOR EACH ROW BEGIN @@ -33,6 +37,7 @@ namespace Kyoo.SqLite.Migrations END"); + // language=SQLite migrationBuilder.Sql(@" CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW BEGIN @@ -43,10 +48,15 @@ namespace Kyoo.SqLite.Migrations protected override void Down(MigrationBuilder migrationBuilder) { + // language=SQLite migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;"); + // language=SQLite migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;"); + // language=SQLite migrationBuilder.Sql("DROP TRIGGER EpisodeSlugInsert;"); + // language=SQLite migrationBuilder.Sql("DROP TRIGGER EpisodeSlugUpdate;"); + // language=SQLite migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;"); } } From dc16223e3e954b0086e0554b569696a5e452ddb0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 23 Jun 2021 20:15:39 +0200 Subject: [PATCH 35/57] Using snake case for postgresql --- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 1 + .../Migrations/20210621175845_Initial.cs | 782 ------------------ ....cs => 20210623174924_Initial.Designer.cs} | 559 +++++++++---- .../Migrations/20210623174924_Initial.cs | 782 ++++++++++++++++++ ...cs => 20210623174932_Triggers.Designer.cs} | 559 +++++++++---- ...Triggers.cs => 20210623174932_Triggers.cs} | 16 +- .../PostgresContextModelSnapshot.cs | 557 +++++++++---- Kyoo.Postgresql/PostgresContext.cs | 1 + 8 files changed, 1925 insertions(+), 1332 deletions(-) delete mode 100644 Kyoo.Postgresql/Migrations/20210621175845_Initial.cs rename Kyoo.Postgresql/Migrations/{20210621175845_Initial.Designer.cs => 20210623174924_Initial.Designer.cs} (57%) create mode 100644 Kyoo.Postgresql/Migrations/20210623174924_Initial.cs rename Kyoo.Postgresql/Migrations/{20210621175855_Triggers.Designer.cs => 20210623174932_Triggers.Designer.cs} (57%) rename Kyoo.Postgresql/Migrations/{20210621175855_Triggers.cs => 20210623174932_Triggers.cs} (70%) diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 6c67a653..096b1bcc 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -18,6 +18,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Kyoo.Postgresql/Migrations/20210621175845_Initial.cs b/Kyoo.Postgresql/Migrations/20210621175845_Initial.cs deleted file mode 100644 index 7fe643bd..00000000 --- a/Kyoo.Postgresql/Migrations/20210621175845_Initial.cs +++ /dev/null @@ -1,782 +0,0 @@ -using System; -using System.Collections.Generic; -using Kyoo.Models; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Postgresql.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:item_type", "show,movie,collection") - .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown") - .Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment"); - - migrationBuilder.CreateTable( - name: "Collections", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Poster = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Collections", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Genres", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Genres", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Libraries", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Paths = table.Column(type: "text[]", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Libraries", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "People", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Poster = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_People", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Providers", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Logo = table.Column(type: "text", nullable: true), - LogoExtension = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Providers", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Studios", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Studios", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Username = table.Column(type: "text", nullable: true), - Email = table.Column(type: "text", nullable: true), - Password = table.Column(type: "text", nullable: true), - Permissions = table.Column(type: "text[]", nullable: true), - ExtraData = table.Column>(type: "jsonb", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Collections_SecondID", - column: x => x.SecondID, - principalTable: "Collections", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Libraries_FirstID", - column: x => x.FirstID, - principalTable: "Libraries", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Libraries_FirstID", - column: x => x.FirstID, - principalTable: "Libraries", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Providers_SecondID", - column: x => x.SecondID, - principalTable: "Providers", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MetadataID", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false), - DataID = table.Column(type: "text", nullable: true), - Link = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_MetadataID_People_FirstID", - column: x => x.FirstID, - principalTable: "People", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataID_Providers_SecondID", - column: x => x.SecondID, - principalTable: "Providers", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Shows", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Title = table.Column(type: "text", nullable: true), - Aliases = table.Column(type: "text[]", nullable: true), - Path = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - Status = table.Column(type: "status", nullable: true), - TrailerUrl = table.Column(type: "text", nullable: true), - StartAir = table.Column(type: "timestamp without time zone", nullable: true), - EndAir = table.Column(type: "timestamp without time zone", nullable: true), - Poster = table.Column(type: "text", nullable: true), - Logo = table.Column(type: "text", nullable: true), - Backdrop = table.Column(type: "text", nullable: true), - IsMovie = table.Column(type: "boolean", nullable: false), - StudioID = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Shows", x => x.ID); - table.ForeignKey( - name: "FK_Shows_Studios_StudioID", - column: x => x.StudioID, - principalTable: "Studios", - principalColumn: "ID", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Collections_FirstID", - column: x => x.FirstID, - principalTable: "Collections", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Shows_SecondID", - column: x => x.SecondID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Libraries_FirstID", - column: x => x.FirstID, - principalTable: "Libraries", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Shows_SecondID", - column: x => x.SecondID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Genres_SecondID", - column: x => x.SecondID, - principalTable: "Genres", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Shows_FirstID", - column: x => x.FirstID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Shows_SecondID", - column: x => x.SecondID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Users_FirstID", - column: x => x.FirstID, - principalTable: "Users", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MetadataID", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false), - DataID = table.Column(type: "text", nullable: true), - Link = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_MetadataID_Providers_SecondID", - column: x => x.SecondID, - principalTable: "Providers", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataID_Shows_FirstID", - column: x => x.FirstID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PeopleRoles", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ForPeople = table.Column(type: "boolean", nullable: false), - PeopleID = table.Column(type: "integer", nullable: false), - ShowID = table.Column(type: "integer", nullable: false), - Type = table.Column(type: "text", nullable: true), - Role = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_PeopleRoles", x => x.ID); - table.ForeignKey( - name: "FK_PeopleRoles_People_PeopleID", - column: x => x.PeopleID, - principalTable: "People", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_PeopleRoles_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Seasons", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: true), - ShowID = table.Column(type: "integer", nullable: false), - SeasonNumber = table.Column(type: "integer", nullable: false), - Title = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - StartDate = table.Column(type: "timestamp without time zone", nullable: true), - EndDate = table.Column(type: "timestamp without time zone", nullable: true), - Poster = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Seasons", x => x.ID); - table.ForeignKey( - name: "FK_Seasons_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Episodes", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: true), - ShowID = table.Column(type: "integer", nullable: false), - SeasonID = table.Column(type: "integer", nullable: true), - SeasonNumber = table.Column(type: "integer", nullable: false), - EpisodeNumber = table.Column(type: "integer", nullable: false), - AbsoluteNumber = table.Column(type: "integer", nullable: false), - Path = table.Column(type: "text", nullable: true), - Thumb = table.Column(type: "text", nullable: true), - Title = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - ReleaseDate = table.Column(type: "timestamp without time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Episodes", x => x.ID); - table.ForeignKey( - name: "FK_Episodes_Seasons_SeasonID", - column: x => x.SeasonID, - principalTable: "Seasons", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Episodes_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MetadataID", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false), - DataID = table.Column(type: "text", nullable: true), - Link = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_MetadataID_Providers_SecondID", - column: x => x.SecondID, - principalTable: "Providers", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataID_Seasons_FirstID", - column: x => x.FirstID, - principalTable: "Seasons", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MetadataID", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false), - DataID = table.Column(type: "text", nullable: true), - Link = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_MetadataID_Episodes_FirstID", - column: x => x.FirstID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataID_Providers_SecondID", - column: x => x.SecondID, - principalTable: "Providers", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Tracks", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: true), - Title = table.Column(type: "text", nullable: true), - Language = table.Column(type: "text", nullable: true), - Codec = table.Column(type: "text", nullable: true), - IsDefault = table.Column(type: "boolean", nullable: false), - IsForced = table.Column(type: "boolean", nullable: false), - IsExternal = table.Column(type: "boolean", nullable: false), - Path = table.Column(type: "text", nullable: true), - Type = table.Column(type: "stream_type", nullable: false), - EpisodeID = table.Column(type: "integer", nullable: false), - TrackIndex = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tracks", x => x.ID); - table.ForeignKey( - name: "FK_Tracks_Episodes_EpisodeID", - column: x => x.EpisodeID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "WatchedEpisodes", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false), - WatchedPercentage = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_WatchedEpisodes", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_WatchedEpisodes_Episodes_SecondID", - column: x => x.SecondID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_WatchedEpisodes_Users_FirstID", - column: x => x.FirstID, - principalTable: "Users", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Collections_Slug", - table: "Collections", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Episodes_SeasonID", - table: "Episodes", - column: "SeasonID"); - - migrationBuilder.CreateIndex( - name: "IX_Episodes_ShowID_SeasonNumber_EpisodeNumber_AbsoluteNumber", - table: "Episodes", - columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Episodes_Slug", - table: "Episodes", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Genres_Slug", - table: "Genres", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Libraries_Slug", - table: "Libraries", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataID_SecondID", - table: "MetadataID", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataID_SecondID", - table: "MetadataID", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataID_SecondID", - table: "MetadataID", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataID_SecondID", - table: "MetadataID", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_People_Slug", - table: "People", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_PeopleRoles_PeopleID", - table: "PeopleRoles", - column: "PeopleID"); - - migrationBuilder.CreateIndex( - name: "IX_PeopleRoles_ShowID", - table: "PeopleRoles", - column: "ShowID"); - - migrationBuilder.CreateIndex( - name: "IX_Providers_Slug", - table: "Providers", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Seasons_ShowID_SeasonNumber", - table: "Seasons", - columns: new[] { "ShowID", "SeasonNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Seasons_Slug", - table: "Seasons", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Shows_Slug", - table: "Shows", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Shows_StudioID", - table: "Shows", - column: "StudioID"); - - migrationBuilder.CreateIndex( - name: "IX_Studios_Slug", - table: "Studios", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced", - table: "Tracks", - columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Tracks_Slug", - table: "Tracks", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Users_Slug", - table: "Users", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_WatchedEpisodes_SecondID", - table: "WatchedEpisodes", - column: "SecondID"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "MetadataID"); - - migrationBuilder.DropTable( - name: "MetadataID"); - - migrationBuilder.DropTable( - name: "MetadataID"); - - migrationBuilder.DropTable( - name: "MetadataID"); - - migrationBuilder.DropTable( - name: "PeopleRoles"); - - migrationBuilder.DropTable( - name: "Tracks"); - - migrationBuilder.DropTable( - name: "WatchedEpisodes"); - - migrationBuilder.DropTable( - name: "Collections"); - - migrationBuilder.DropTable( - name: "Libraries"); - - migrationBuilder.DropTable( - name: "Genres"); - - migrationBuilder.DropTable( - name: "Providers"); - - migrationBuilder.DropTable( - name: "People"); - - migrationBuilder.DropTable( - name: "Episodes"); - - migrationBuilder.DropTable( - name: "Users"); - - migrationBuilder.DropTable( - name: "Seasons"); - - migrationBuilder.DropTable( - name: "Shows"); - - migrationBuilder.DropTable( - name: "Studios"); - } - } -} diff --git a/Kyoo.Postgresql/Migrations/20210621175845_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210623174924_Initial.Designer.cs similarity index 57% rename from Kyoo.Postgresql/Migrations/20210621175845_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210623174924_Initial.Designer.cs index 8b99205c..c6943ac2 100644 --- a/Kyoo.Postgresql/Migrations/20210621175845_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210623174924_Initial.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210621175845_Initial")] + [Migration("20210623174924_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -31,27 +31,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_collections"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_collections_slug"); - b.ToTable("Collections"); + b.ToTable("collections"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -59,53 +66,69 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("AbsoluteNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("absolute_number"); b.Property("EpisodeNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("episode_number"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("ReleaseDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("release_date"); b.Property("SeasonID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_id"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Thumb") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("thumb"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_episodes"); - b.HasIndex("SeasonID"); + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); - b.ToTable("Episodes"); + b.ToTable("episodes"); }); modelBuilder.Entity("Kyoo.Models.Genre", b => @@ -113,21 +136,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_genres"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_genres_slug"); - b.ToTable("Genres"); + b.ToTable("genres"); }); modelBuilder.Entity("Kyoo.Models.Library", b => @@ -135,198 +163,252 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Paths") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("paths"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_libraries"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_libraries_slug"); - b.ToTable("Libraries"); + b.ToTable("libraries"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_collection_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_collection_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_collection_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_collection"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_collection_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_collection"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_provider"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_provider_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_provider"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_show_genre"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_show_genre_second_id"); - b.ToTable("Link"); + b.ToTable("link_show_genre"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_user_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_user_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_user_show"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_episode"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_episode_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_episode"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_people"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_people_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_people"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_season"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_season_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_season"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_show_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_show"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -334,24 +416,30 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_people_slug"); - b.ToTable("People"); + b.ToTable("people"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -359,30 +447,39 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("ForPeople") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("for_people"); b.Property("PeopleID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("people_id"); b.Property("Role") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("role"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Type") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people_roles"); - b.HasIndex("PeopleID"); + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); - b.HasIndex("ShowID"); + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); - b.ToTable("PeopleRoles"); + b.ToTable("people_roles"); }); modelBuilder.Entity("Kyoo.Models.Provider", b => @@ -390,27 +487,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("LogoExtension") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo_extension"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_providers"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_providers_slug"); - b.ToTable("Providers"); + b.ToTable("providers"); }); modelBuilder.Entity("Kyoo.Models.Season", b => @@ -418,42 +522,54 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("EndDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("StartDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_seasons"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); b.HasIndex("ShowID", "SeasonNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); - b.ToTable("Seasons"); + b.ToTable("seasons"); }); modelBuilder.Entity("Kyoo.Models.Show", b => @@ -461,59 +577,77 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Aliases") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("aliases"); b.Property("Backdrop") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("backdrop"); b.Property("EndAir") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("end_air"); b.Property("IsMovie") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_movie"); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("StartAir") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("start_air"); b.Property("Status") - .HasColumnType("status"); + .HasColumnType("status") + .HasColumnName("status"); b.Property("StudioID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("studio_id"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrailerUrl") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("trailer_url"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_shows"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_shows_slug"); - b.HasIndex("StudioID"); + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); - b.ToTable("Shows"); + b.ToTable("shows"); }); modelBuilder.Entity("Kyoo.Models.Studio", b => @@ -521,21 +655,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_studios"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_studios_slug"); - b.ToTable("Studios"); + b.ToTable("studios"); }); modelBuilder.Entity("Kyoo.Models.Track", b => @@ -543,51 +682,66 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Codec") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("codec"); b.Property("EpisodeID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("episode_id"); b.Property("IsDefault") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_default"); b.Property("IsExternal") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_external"); b.Property("IsForced") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_forced"); b.Property("Language") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("language"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrackIndex") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("track_index"); b.Property("Type") - .HasColumnType("stream_type"); + .HasColumnType("stream_type") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_tracks"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_tracks_slug"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_tracks_episode_id_type_language_track_index_is_forced"); - b.ToTable("Tracks"); + b.ToTable("tracks"); }); modelBuilder.Entity("Kyoo.Models.User", b => @@ -595,51 +749,65 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Email") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("email"); b.Property>("ExtraData") - .HasColumnType("jsonb"); + .HasColumnType("jsonb") + .HasColumnName("extra_data"); b.Property("Password") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("password"); b.Property("Permissions") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("permissions"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Username") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("username"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_users"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_users_slug"); - b.ToTable("Users"); + b.ToTable("users"); }); modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("WatchedPercentage") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("watched_percentage"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_watched_episodes"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_watched_episodes_second_id"); - b.ToTable("WatchedEpisodes"); + b.ToTable("watched_episodes"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -647,11 +815,13 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") .HasForeignKey("SeasonID") + .HasConstraintName("fk_episodes_seasons_season_id") .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") .HasForeignKey("ShowID") + .HasConstraintName("fk_episodes_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -665,12 +835,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Collection", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_collection_show_collections_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("CollectionLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_collection_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -684,12 +856,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("CollectionLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_collection_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Collection", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_collection_collections_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -703,12 +877,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ProviderLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_provider_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_provider_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -722,12 +898,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_show_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -741,12 +919,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "First") .WithMany("GenreLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_show_genre_shows_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Genre", "Second") .WithMany("ShowLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_show_genre_genres_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -760,12 +940,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_user_show_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_link_user_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -779,12 +961,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_episode_episodes_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_episode_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -798,12 +982,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_people_people_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_people_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -817,12 +1003,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_season_seasons_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_season_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -836,12 +1024,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_show_shows_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_show_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -855,12 +1045,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.People", "People") .WithMany("Roles") .HasForeignKey("PeopleID") + .HasConstraintName("fk_people_roles_people_people_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("People") .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -874,6 +1066,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Seasons") .HasForeignKey("ShowID") + .HasConstraintName("fk_seasons_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -884,7 +1077,8 @@ namespace Kyoo.Postgresql.Migrations { b.HasOne("Kyoo.Models.Studio", "Studio") .WithMany("Shows") - .HasForeignKey("StudioID"); + .HasForeignKey("StudioID") + .HasConstraintName("fk_shows_studios_studio_id"); b.Navigation("Studio"); }); @@ -894,6 +1088,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Episode", "Episode") .WithMany("Tracks") .HasForeignKey("EpisodeID") + .HasConstraintName("fk_tracks_episodes_episode_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -905,12 +1100,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("CurrentlyWatching") .HasForeignKey("FirstID") + .HasConstraintName("fk_watched_episodes_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Episode", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_watched_episodes_episodes_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Kyoo.Postgresql/Migrations/20210623174924_Initial.cs b/Kyoo.Postgresql/Migrations/20210623174924_Initial.cs new file mode 100644 index 00000000..45c8e00d --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210623174924_Initial.cs @@ -0,0 +1,782 @@ +using System; +using System.Collections.Generic; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:item_type", "show,movie,collection") + .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown") + .Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment"); + + migrationBuilder.CreateTable( + name: "collections", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + poster = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_collections", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "genres", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_genres", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "libraries", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + paths = table.Column(type: "text[]", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_libraries", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "people", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + poster = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_people", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "providers", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + logo = table.Column(type: "text", nullable: true), + logo_extension = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_providers", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "studios", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_studios", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + username = table.Column(type: "text", nullable: true), + email = table.Column(type: "text", nullable: true), + password = table.Column(type: "text", nullable: true), + permissions = table.Column(type: "text[]", nullable: true), + extra_data = table.Column>(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "link_library_collection", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_library_collection", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_library_collection_collections_second_id", + column: x => x.second_id, + principalTable: "collections", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_library_collection_libraries_first_id", + column: x => x.first_id, + principalTable: "libraries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_library_provider", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_library_provider", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_library_provider_libraries_first_id", + column: x => x.first_id, + principalTable: "libraries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_library_provider_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_people", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_people", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_people_people_first_id", + column: x => x.first_id, + principalTable: "people", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_people_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "shows", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + title = table.Column(type: "text", nullable: true), + aliases = table.Column(type: "text[]", nullable: true), + path = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + status = table.Column(type: "status", nullable: true), + trailer_url = table.Column(type: "text", nullable: true), + start_air = table.Column(type: "timestamp without time zone", nullable: true), + end_air = table.Column(type: "timestamp without time zone", nullable: true), + poster = table.Column(type: "text", nullable: true), + logo = table.Column(type: "text", nullable: true), + backdrop = table.Column(type: "text", nullable: true), + is_movie = table.Column(type: "boolean", nullable: false), + studio_id = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_shows", x => x.id); + table.ForeignKey( + name: "fk_shows_studios_studio_id", + column: x => x.studio_id, + principalTable: "studios", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "link_collection_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_collection_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_collection_show_collections_first_id", + column: x => x.first_id, + principalTable: "collections", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_collection_show_shows_second_id", + column: x => x.second_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_library_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_library_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_library_show_libraries_first_id", + column: x => x.first_id, + principalTable: "libraries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_library_show_shows_second_id", + column: x => x.second_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_show_genre", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_show_genre", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_show_genre_genres_second_id", + column: x => x.second_id, + principalTable: "genres", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_show_genre_shows_first_id", + column: x => x.first_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_user_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_user_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_user_show_shows_second_id", + column: x => x.second_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_user_show_users_first_id", + column: x => x.first_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_show_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_show_shows_first_id", + column: x => x.first_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "people_roles", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + for_people = table.Column(type: "boolean", nullable: false), + people_id = table.Column(type: "integer", nullable: false), + show_id = table.Column(type: "integer", nullable: false), + type = table.Column(type: "text", nullable: true), + role = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_people_roles", x => x.id); + table.ForeignKey( + name: "fk_people_roles_people_people_id", + column: x => x.people_id, + principalTable: "people", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_people_roles_shows_show_id", + column: x => x.show_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "seasons", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: true), + show_id = table.Column(type: "integer", nullable: false), + season_number = table.Column(type: "integer", nullable: false), + title = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + start_date = table.Column(type: "timestamp without time zone", nullable: true), + end_date = table.Column(type: "timestamp without time zone", nullable: true), + poster = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_seasons", x => x.id); + table.ForeignKey( + name: "fk_seasons_shows_show_id", + column: x => x.show_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "episodes", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: true), + show_id = table.Column(type: "integer", nullable: false), + season_id = table.Column(type: "integer", nullable: true), + season_number = table.Column(type: "integer", nullable: false), + episode_number = table.Column(type: "integer", nullable: false), + absolute_number = table.Column(type: "integer", nullable: false), + path = table.Column(type: "text", nullable: true), + thumb = table.Column(type: "text", nullable: true), + title = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + release_date = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_episodes", x => x.id); + table.ForeignKey( + name: "fk_episodes_seasons_season_id", + column: x => x.season_id, + principalTable: "seasons", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_episodes_shows_show_id", + column: x => x.show_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_season", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_season", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_season_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_season_seasons_first_id", + column: x => x.first_id, + principalTable: "seasons", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_episode", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_episode", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_episode_episodes_first_id", + column: x => x.first_id, + principalTable: "episodes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_episode_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tracks", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: true), + title = table.Column(type: "text", nullable: true), + language = table.Column(type: "text", nullable: true), + codec = table.Column(type: "text", nullable: true), + is_default = table.Column(type: "boolean", nullable: false), + is_forced = table.Column(type: "boolean", nullable: false), + is_external = table.Column(type: "boolean", nullable: false), + path = table.Column(type: "text", nullable: true), + type = table.Column(type: "stream_type", nullable: false), + episode_id = table.Column(type: "integer", nullable: false), + track_index = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tracks", x => x.id); + table.ForeignKey( + name: "fk_tracks_episodes_episode_id", + column: x => x.episode_id, + principalTable: "episodes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "watched_episodes", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + watched_percentage = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_watched_episodes", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_watched_episodes_episodes_second_id", + column: x => x.second_id, + principalTable: "episodes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_watched_episodes_users_first_id", + column: x => x.first_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_collections_slug", + table: "collections", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_episodes_season_id", + table: "episodes", + column: "season_id"); + + migrationBuilder.CreateIndex( + name: "ix_episodes_show_id_season_number_episode_number_absolute_numb", + table: "episodes", + columns: new[] { "show_id", "season_number", "episode_number", "absolute_number" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_episodes_slug", + table: "episodes", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_genres_slug", + table: "genres", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_libraries_slug", + table: "libraries", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_link_collection_show_second_id", + table: "link_collection_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_library_collection_second_id", + table: "link_library_collection", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_library_provider_second_id", + table: "link_library_provider", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_library_show_second_id", + table: "link_library_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_show_genre_second_id", + table: "link_show_genre", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_user_show_second_id", + table: "link_user_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_episode_second_id", + table: "metadata_id_episode", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_people_second_id", + table: "metadata_id_people", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_season_second_id", + table: "metadata_id_season", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_show_second_id", + table: "metadata_id_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_people_slug", + table: "people", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_people_roles_people_id", + table: "people_roles", + column: "people_id"); + + migrationBuilder.CreateIndex( + name: "ix_people_roles_show_id", + table: "people_roles", + column: "show_id"); + + migrationBuilder.CreateIndex( + name: "ix_providers_slug", + table: "providers", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_seasons_show_id_season_number", + table: "seasons", + columns: new[] { "show_id", "season_number" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_seasons_slug", + table: "seasons", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_shows_slug", + table: "shows", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_shows_studio_id", + table: "shows", + column: "studio_id"); + + migrationBuilder.CreateIndex( + name: "ix_studios_slug", + table: "studios", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tracks_episode_id_type_language_track_index_is_forced", + table: "tracks", + columns: new[] { "episode_id", "type", "language", "track_index", "is_forced" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tracks_slug", + table: "tracks", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_users_slug", + table: "users", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_watched_episodes_second_id", + table: "watched_episodes", + column: "second_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "link_collection_show"); + + migrationBuilder.DropTable( + name: "link_library_collection"); + + migrationBuilder.DropTable( + name: "link_library_provider"); + + migrationBuilder.DropTable( + name: "link_library_show"); + + migrationBuilder.DropTable( + name: "link_show_genre"); + + migrationBuilder.DropTable( + name: "link_user_show"); + + migrationBuilder.DropTable( + name: "metadata_id_episode"); + + migrationBuilder.DropTable( + name: "metadata_id_people"); + + migrationBuilder.DropTable( + name: "metadata_id_season"); + + migrationBuilder.DropTable( + name: "metadata_id_show"); + + migrationBuilder.DropTable( + name: "people_roles"); + + migrationBuilder.DropTable( + name: "tracks"); + + migrationBuilder.DropTable( + name: "watched_episodes"); + + migrationBuilder.DropTable( + name: "collections"); + + migrationBuilder.DropTable( + name: "libraries"); + + migrationBuilder.DropTable( + name: "genres"); + + migrationBuilder.DropTable( + name: "providers"); + + migrationBuilder.DropTable( + name: "people"); + + migrationBuilder.DropTable( + name: "episodes"); + + migrationBuilder.DropTable( + name: "users"); + + migrationBuilder.DropTable( + name: "seasons"); + + migrationBuilder.DropTable( + name: "shows"); + + migrationBuilder.DropTable( + name: "studios"); + } + } +} diff --git a/Kyoo.Postgresql/Migrations/20210621175855_Triggers.Designer.cs b/Kyoo.Postgresql/Migrations/20210623174932_Triggers.Designer.cs similarity index 57% rename from Kyoo.Postgresql/Migrations/20210621175855_Triggers.Designer.cs rename to Kyoo.Postgresql/Migrations/20210623174932_Triggers.Designer.cs index 6a4a0163..ebd1669f 100644 --- a/Kyoo.Postgresql/Migrations/20210621175855_Triggers.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210623174932_Triggers.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210621175855_Triggers")] + [Migration("20210623174932_Triggers")] partial class Triggers { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -31,27 +31,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_collections"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_collections_slug"); - b.ToTable("Collections"); + b.ToTable("collections"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -59,53 +66,69 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("AbsoluteNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("absolute_number"); b.Property("EpisodeNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("episode_number"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("ReleaseDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("release_date"); b.Property("SeasonID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_id"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Thumb") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("thumb"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_episodes"); - b.HasIndex("SeasonID"); + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); - b.ToTable("Episodes"); + b.ToTable("episodes"); }); modelBuilder.Entity("Kyoo.Models.Genre", b => @@ -113,21 +136,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_genres"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_genres_slug"); - b.ToTable("Genres"); + b.ToTable("genres"); }); modelBuilder.Entity("Kyoo.Models.Library", b => @@ -135,198 +163,252 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Paths") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("paths"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_libraries"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_libraries_slug"); - b.ToTable("Libraries"); + b.ToTable("libraries"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_collection_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_collection_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_collection_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_collection"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_collection_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_collection"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_provider"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_provider_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_provider"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_show_genre"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_show_genre_second_id"); - b.ToTable("Link"); + b.ToTable("link_show_genre"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_user_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_user_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_user_show"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_episode"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_episode_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_episode"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_people"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_people_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_people"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_season"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_season_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_season"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_show_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_show"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -334,24 +416,30 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_people_slug"); - b.ToTable("People"); + b.ToTable("people"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -359,30 +447,39 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("ForPeople") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("for_people"); b.Property("PeopleID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("people_id"); b.Property("Role") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("role"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Type") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people_roles"); - b.HasIndex("PeopleID"); + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); - b.HasIndex("ShowID"); + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); - b.ToTable("PeopleRoles"); + b.ToTable("people_roles"); }); modelBuilder.Entity("Kyoo.Models.Provider", b => @@ -390,27 +487,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("LogoExtension") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo_extension"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_providers"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_providers_slug"); - b.ToTable("Providers"); + b.ToTable("providers"); }); modelBuilder.Entity("Kyoo.Models.Season", b => @@ -418,42 +522,54 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("EndDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("StartDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_seasons"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); b.HasIndex("ShowID", "SeasonNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); - b.ToTable("Seasons"); + b.ToTable("seasons"); }); modelBuilder.Entity("Kyoo.Models.Show", b => @@ -461,59 +577,77 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Aliases") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("aliases"); b.Property("Backdrop") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("backdrop"); b.Property("EndAir") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("end_air"); b.Property("IsMovie") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_movie"); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("StartAir") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("start_air"); b.Property("Status") - .HasColumnType("status"); + .HasColumnType("status") + .HasColumnName("status"); b.Property("StudioID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("studio_id"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrailerUrl") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("trailer_url"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_shows"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_shows_slug"); - b.HasIndex("StudioID"); + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); - b.ToTable("Shows"); + b.ToTable("shows"); }); modelBuilder.Entity("Kyoo.Models.Studio", b => @@ -521,21 +655,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_studios"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_studios_slug"); - b.ToTable("Studios"); + b.ToTable("studios"); }); modelBuilder.Entity("Kyoo.Models.Track", b => @@ -543,51 +682,66 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Codec") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("codec"); b.Property("EpisodeID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("episode_id"); b.Property("IsDefault") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_default"); b.Property("IsExternal") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_external"); b.Property("IsForced") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_forced"); b.Property("Language") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("language"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrackIndex") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("track_index"); b.Property("Type") - .HasColumnType("stream_type"); + .HasColumnType("stream_type") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_tracks"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_tracks_slug"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_tracks_episode_id_type_language_track_index_is_forced"); - b.ToTable("Tracks"); + b.ToTable("tracks"); }); modelBuilder.Entity("Kyoo.Models.User", b => @@ -595,51 +749,65 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Email") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("email"); b.Property>("ExtraData") - .HasColumnType("jsonb"); + .HasColumnType("jsonb") + .HasColumnName("extra_data"); b.Property("Password") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("password"); b.Property("Permissions") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("permissions"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Username") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("username"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_users"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_users_slug"); - b.ToTable("Users"); + b.ToTable("users"); }); modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("WatchedPercentage") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("watched_percentage"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_watched_episodes"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_watched_episodes_second_id"); - b.ToTable("WatchedEpisodes"); + b.ToTable("watched_episodes"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -647,11 +815,13 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") .HasForeignKey("SeasonID") + .HasConstraintName("fk_episodes_seasons_season_id") .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") .HasForeignKey("ShowID") + .HasConstraintName("fk_episodes_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -665,12 +835,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Collection", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_collection_show_collections_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("CollectionLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_collection_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -684,12 +856,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("CollectionLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_collection_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Collection", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_collection_collections_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -703,12 +877,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ProviderLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_provider_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_provider_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -722,12 +898,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_show_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -741,12 +919,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "First") .WithMany("GenreLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_show_genre_shows_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Genre", "Second") .WithMany("ShowLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_show_genre_genres_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -760,12 +940,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_user_show_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_link_user_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -779,12 +961,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_episode_episodes_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_episode_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -798,12 +982,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_people_people_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_people_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -817,12 +1003,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_season_seasons_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_season_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -836,12 +1024,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_show_shows_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_show_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -855,12 +1045,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.People", "People") .WithMany("Roles") .HasForeignKey("PeopleID") + .HasConstraintName("fk_people_roles_people_people_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("People") .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -874,6 +1066,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Seasons") .HasForeignKey("ShowID") + .HasConstraintName("fk_seasons_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -884,7 +1077,8 @@ namespace Kyoo.Postgresql.Migrations { b.HasOne("Kyoo.Models.Studio", "Studio") .WithMany("Shows") - .HasForeignKey("StudioID"); + .HasForeignKey("StudioID") + .HasConstraintName("fk_shows_studios_studio_id"); b.Navigation("Studio"); }); @@ -894,6 +1088,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Episode", "Episode") .WithMany("Tracks") .HasForeignKey("EpisodeID") + .HasConstraintName("fk_tracks_episodes_episode_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -905,12 +1100,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("CurrentlyWatching") .HasForeignKey("FirstID") + .HasConstraintName("fk_watched_episodes_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Episode", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_watched_episodes_episodes_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs b/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs similarity index 70% rename from Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs rename to Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs index 25e3d51f..ffd88fc3 100644 --- a/Kyoo.Postgresql/Migrations/20210621175855_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs @@ -13,10 +13,10 @@ namespace Kyoo.Postgresql.Migrations LANGUAGE PLPGSQL AS $$ BEGIN - NEW.""Slug"" := CONCAT( - (SELECT ""Slug"" FROM ""Shows"" WHERE ""ID"" = NEW.""ShowID""), + NEW.slug := CONCAT( + (SELECT slug FROM shows WHERE id = NEW.show_id), '-s', - NEW.""SeasonNumber"" + NEW.season_number ); RETURN NEW; END @@ -24,7 +24,7 @@ namespace Kyoo.Postgresql.Migrations // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF ""SeasonNumber"", ""ShowID"" ON ""Seasons"" + CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF season_number, show_id ON seasons FOR EACH ROW EXECUTE PROCEDURE season_slug_update();"); @@ -35,14 +35,14 @@ namespace Kyoo.Postgresql.Migrations LANGUAGE PLPGSQL AS $$ BEGIN - UPDATE ""Seasons"" SET ""Slug"" = CONCAT(new.""Slug"", '-s', ""SeasonNumber"") WHERE ""ShowID"" = NEW.""ID""; + UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id; RETURN NEW; END $$;"); // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER show_slug_trigger AFTER UPDATE OF ""Slug"" ON ""Shows"" + CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows FOR EACH ROW EXECUTE PROCEDURE show_slug_update();"); } @@ -51,11 +51,11 @@ namespace Kyoo.Postgresql.Migrations // language=PostgreSQL migrationBuilder.Sql(@"DROP FUNCTION season_slug_update;"); // language=PostgreSQL - migrationBuilder.Sql("DROP TRIGGER show_slug_trigger ON \"Shows\";"); + migrationBuilder.Sql("DROP TRIGGER show_slug_trigger ON shows;"); // language=PostgreSQL migrationBuilder.Sql(@"DROP FUNCTION show_slug_update;"); // language=PostgreSQL - migrationBuilder.Sql(@"DROP TRIGGER season_slug_trigger;"); + migrationBuilder.Sql(@"DROP TRIGGER season_slug_trigger ON seasons;"); } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 14f61708..554ef95f 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -29,27 +29,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_collections"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_collections_slug"); - b.ToTable("Collections"); + b.ToTable("collections"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -57,53 +64,69 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("AbsoluteNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("absolute_number"); b.Property("EpisodeNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("episode_number"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("ReleaseDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("release_date"); b.Property("SeasonID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_id"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Thumb") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("thumb"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_episodes"); - b.HasIndex("SeasonID"); + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); - b.ToTable("Episodes"); + b.ToTable("episodes"); }); modelBuilder.Entity("Kyoo.Models.Genre", b => @@ -111,21 +134,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_genres"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_genres_slug"); - b.ToTable("Genres"); + b.ToTable("genres"); }); modelBuilder.Entity("Kyoo.Models.Library", b => @@ -133,198 +161,252 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Paths") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("paths"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_libraries"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_libraries_slug"); - b.ToTable("Libraries"); + b.ToTable("libraries"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_collection_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_collection_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_collection_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_collection"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_collection_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_collection"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_provider"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_provider_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_provider"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_show_genre"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_show_genre_second_id"); - b.ToTable("Link"); + b.ToTable("link_show_genre"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_user_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_user_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_user_show"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_episode"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_episode_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_episode"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_people"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_people_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_people"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_season"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_season_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_season"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_show_second_id"); - b.ToTable("MetadataID"); + b.ToTable("metadata_id_show"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -332,24 +414,30 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_people_slug"); - b.ToTable("People"); + b.ToTable("people"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -357,30 +445,39 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("ForPeople") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("for_people"); b.Property("PeopleID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("people_id"); b.Property("Role") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("role"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Type") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people_roles"); - b.HasIndex("PeopleID"); + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); - b.HasIndex("ShowID"); + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); - b.ToTable("PeopleRoles"); + b.ToTable("people_roles"); }); modelBuilder.Entity("Kyoo.Models.Provider", b => @@ -388,27 +485,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("LogoExtension") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo_extension"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_providers"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_providers_slug"); - b.ToTable("Providers"); + b.ToTable("providers"); }); modelBuilder.Entity("Kyoo.Models.Season", b => @@ -416,42 +520,54 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("EndDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("StartDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_seasons"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); b.HasIndex("ShowID", "SeasonNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); - b.ToTable("Seasons"); + b.ToTable("seasons"); }); modelBuilder.Entity("Kyoo.Models.Show", b => @@ -459,59 +575,77 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Aliases") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("aliases"); b.Property("Backdrop") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("backdrop"); b.Property("EndAir") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("end_air"); b.Property("IsMovie") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_movie"); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("StartAir") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp without time zone") + .HasColumnName("start_air"); b.Property("Status") - .HasColumnType("status"); + .HasColumnType("status") + .HasColumnName("status"); b.Property("StudioID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("studio_id"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrailerUrl") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("trailer_url"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_shows"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_shows_slug"); - b.HasIndex("StudioID"); + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); - b.ToTable("Shows"); + b.ToTable("shows"); }); modelBuilder.Entity("Kyoo.Models.Studio", b => @@ -519,21 +653,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_studios"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_studios_slug"); - b.ToTable("Studios"); + b.ToTable("studios"); }); modelBuilder.Entity("Kyoo.Models.Track", b => @@ -541,51 +680,66 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Codec") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("codec"); b.Property("EpisodeID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("episode_id"); b.Property("IsDefault") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_default"); b.Property("IsExternal") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_external"); b.Property("IsForced") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_forced"); b.Property("Language") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("language"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("Slug") .ValueGeneratedOnAddOrUpdate() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrackIndex") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("track_index"); b.Property("Type") - .HasColumnType("stream_type"); + .HasColumnType("stream_type") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_tracks"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_tracks_slug"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_tracks_episode_id_type_language_track_index_is_forced"); - b.ToTable("Tracks"); + b.ToTable("tracks"); }); modelBuilder.Entity("Kyoo.Models.User", b => @@ -593,51 +747,65 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Email") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("email"); b.Property>("ExtraData") - .HasColumnType("jsonb"); + .HasColumnType("jsonb") + .HasColumnName("extra_data"); b.Property("Password") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("password"); b.Property("Permissions") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("permissions"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Username") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("username"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_users"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_users_slug"); - b.ToTable("Users"); + b.ToTable("users"); }); modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("WatchedPercentage") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("watched_percentage"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_watched_episodes"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_watched_episodes_second_id"); - b.ToTable("WatchedEpisodes"); + b.ToTable("watched_episodes"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -645,11 +813,13 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") .HasForeignKey("SeasonID") + .HasConstraintName("fk_episodes_seasons_season_id") .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") .HasForeignKey("ShowID") + .HasConstraintName("fk_episodes_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -663,12 +833,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Collection", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_collection_show_collections_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("CollectionLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_collection_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -682,12 +854,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("CollectionLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_collection_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Collection", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_collection_collections_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -701,12 +875,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ProviderLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_provider_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_provider_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -720,12 +896,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_show_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -739,12 +917,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "First") .WithMany("GenreLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_show_genre_shows_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Genre", "Second") .WithMany("ShowLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_show_genre_genres_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -758,12 +938,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_user_show_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_link_user_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -777,12 +959,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_episode_episodes_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_episode_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -796,12 +980,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_people_people_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_people_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -815,12 +1001,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_season_seasons_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_season_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -834,12 +1022,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "First") .WithMany("ExternalIDs") .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_show_shows_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_show_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -853,12 +1043,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.People", "People") .WithMany("Roles") .HasForeignKey("PeopleID") + .HasConstraintName("fk_people_roles_people_people_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("People") .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -872,6 +1064,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Seasons") .HasForeignKey("ShowID") + .HasConstraintName("fk_seasons_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -882,7 +1075,8 @@ namespace Kyoo.Postgresql.Migrations { b.HasOne("Kyoo.Models.Studio", "Studio") .WithMany("Shows") - .HasForeignKey("StudioID"); + .HasForeignKey("StudioID") + .HasConstraintName("fk_shows_studios_studio_id"); b.Navigation("Studio"); }); @@ -892,6 +1086,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Episode", "Episode") .WithMany("Tracks") .HasForeignKey("EpisodeID") + .HasConstraintName("fk_tracks_episodes_episode_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -903,12 +1098,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("CurrentlyWatching") .HasForeignKey("FirstID") + .HasConstraintName("fk_watched_episodes_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Episode", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_watched_episodes_episodes_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index a12d52fa..f070a805 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -77,6 +77,7 @@ namespace Kyoo.Postgresql optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); } + optionsBuilder.UseSnakeCaseNamingConvention(); base.OnConfiguring(optionsBuilder); } From ac4f171a1cd6d3fc52bd20bd6d9c4aff95b17cfd Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 23 Jun 2021 20:25:56 +0200 Subject: [PATCH 36/57] Creating postgres episodes triggers --- .../Migrations/20210623174932_Triggers.cs | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs b/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs index ffd88fc3..4ab90659 100644 --- a/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs @@ -26,6 +26,30 @@ namespace Kyoo.Postgresql.Migrations migrationBuilder.Sql(@" CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF season_number, show_id ON seasons FOR EACH ROW EXECUTE PROCEDURE season_slug_update();"); + + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION episode_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + NEW.slug := CONCAT( + (SELECT slug FROM shows WHERE id = NEW.show_id), + '-s', + NEW.season_number, + 'e', + NEW.episode_number + ); + RETURN NEW; + END + $$;"); + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER episode_slug_trigger BEFORE INSERT OR UPDATE OF episode_number, season_number, show_id ON episodes + FOR EACH ROW EXECUTE PROCEDURE episode_slug_update();"); // language=PostgreSQL @@ -36,6 +60,7 @@ namespace Kyoo.Postgresql.Migrations AS $$ BEGIN UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id; + UPDATE episodes SET slug = CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) WHERE show_id = NEW.id; RETURN NEW; END $$;"); @@ -48,14 +73,18 @@ namespace Kyoo.Postgresql.Migrations protected override void Down(MigrationBuilder migrationBuilder) { - // language=PostgreSQL - migrationBuilder.Sql(@"DROP FUNCTION season_slug_update;"); // language=PostgreSQL migrationBuilder.Sql("DROP TRIGGER show_slug_trigger ON shows;"); // language=PostgreSQL migrationBuilder.Sql(@"DROP FUNCTION show_slug_update;"); // language=PostgreSQL migrationBuilder.Sql(@"DROP TRIGGER season_slug_trigger ON seasons;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION season_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER episode_slug_trigger ON episodes;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;"); } } } \ No newline at end of file From fdb63864623a6308f50c9730d411dfc175c96320 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 23 Jun 2021 23:35:00 +0200 Subject: [PATCH 37/57] Adding tests for absolute numbering --- .../Library/SpecificTests/EpisodeTest.cs | 75 ++++++++++++++++++- Kyoo.Tests/Library/TestSample.cs | 21 +++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index d3603567..868de9d7 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -81,16 +81,87 @@ namespace Kyoo.Tests.Library [Fact] public async Task EpisodeCreationSlugTest() { - Episode season = await _repository.Create(new Episode + Episode episode = await _repository.Create(new Episode { ShowID = TestSample.Get().ID, SeasonNumber = 2, EpisodeNumber = 4 }); - Assert.Equal($"{TestSample.Get().Slug}-s2e4", season.Slug); + Assert.Equal($"{TestSample.Get().Slug}-s2e4", episode.Slug); } // TODO absolute numbering tests + + + [Fact] + public void AbsoluteSlugTest() + { + Assert.Equal($"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", + TestSample.GetAbsoluteEpisode().Slug); + } + + [Fact] + public async Task EpisodeCreationAbsoluteSlugTest() + { + Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); + Assert.Equal($"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", episode.Slug); + } + + [Fact] + public async Task SlugEditAbsoluteTest() + { + Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); + Show show = new() + { + ID = episode.ShowID, + Slug = "new-slug" + }; + await Repositories.LibraryManager.ShowRepository.Edit(show, false); + episode = await _repository.Get(2); + Assert.Equal($"new-slug-3", episode.Slug); + } + + + [Fact] + public async Task AbsoluteNumberEditTest() + { + await _repository.Create(TestSample.GetAbsoluteEpisode()); + await _repository.Edit(new Episode + { + ID = 1, + AbsoluteNumber = 56 + }, false); + Episode episode = await _repository.Get(2); + Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); + } + + [Fact] + public async Task AbsoluteToNormalEditTest() + { + await _repository.Create(TestSample.GetAbsoluteEpisode()); + await _repository.Edit(new Episode + { + ID = 1, + SeasonNumber = 1, + EpisodeNumber = 2 + }, false); + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + } + + [Fact] + public async Task NormalToAbsoluteEditTest() + { + await _repository.Create(TestSample.GetAbsoluteEpisode()); + await _repository.Edit(new Episode + { + ID = 1, + SeasonNumber = -1, + AbsoluteNumber = 12 + }, false); + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 3e5c57da..cb8650fa 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -88,7 +88,7 @@ namespace Kyoo.Tests } } }; - + public static T Get() { return (T)Samples[typeof(T)](); @@ -121,5 +121,24 @@ namespace Kyoo.Tests context.SaveChanges(); } + + public static Episode GetAbsoluteEpisode() + { + return new() + { + ID = 2, + ShowSlug = "anohana", + ShowID = 1, + SeasonID = -1, + SeasonNumber = -1, + EpisodeNumber = -1, + AbsoluteNumber = 3, + Path = "/home/kyoo/anohana-3", + Thumb = "thumbnail", + Title = "Episode 3", + Overview = "Summary of the third absolute episode", + ReleaseDate = new DateTime(2020, 06, 05) + }; + } } } \ No newline at end of file From 5b0ebf39ee0239357bcb4cfa5015948e012481cb Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 24 Jun 2021 18:04:14 +0200 Subject: [PATCH 38/57] Loading the test context from environement variables --- Kyoo.Tests/Dockerfile | 3 +++ Kyoo.Tests/Library/TestContext.cs | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 Kyoo.Tests/Dockerfile diff --git a/Kyoo.Tests/Dockerfile b/Kyoo.Tests/Dockerfile new file mode 100644 index 00000000..423bfe57 --- /dev/null +++ b/Kyoo.Tests/Dockerfile @@ -0,0 +1,3 @@ +FROM mcr.microsoft.com/dotnet/sdk:5.0 +COPY ../ . +RUN dotnet tests '-p:SkipWebApp=true;SkipTranscoder=true' diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 61332a8d..68f27012 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -126,7 +126,11 @@ namespace Kyoo.Tests public static string GetConnectionString(string database) { - return $"Server=127.0.0.1;Port=5432;Database={database};User ID=kyoo;Password=kyooPassword;Include Error Detail=true"; + string server = Environment.GetEnvironmentVariable("SERVER") ?? "127.0.0.1"; + string port = Environment.GetEnvironmentVariable("PORT") ?? "5432"; + string username = Environment.GetEnvironmentVariable("USERNAME") ?? "kyoo"; + string password = Environment.GetEnvironmentVariable("PASSWORD") ?? "kyooPassword"; + return $"Server={server};Port={port};Database={database};User ID={username};Password={password};Include Error Detail=true"; } public override void Dispose() From db5ae6f28575a8fd6769028c9779761c609107b5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 26 Jun 2021 16:13:06 +0200 Subject: [PATCH 39/57] Using nullables instead of -1 --- Kyoo.Common/Controllers/IMetadataProvider.cs | 2 +- Kyoo.Common/Controllers/IProviderManager.cs | 2 +- Kyoo.Common/Models/Resources/Episode.cs | 48 +++++++------------ Kyoo.Common/Models/Resources/Season.cs | 4 +- Kyoo.Common/Models/Resources/Show.cs | 3 +- Kyoo.Common/Models/Resources/Track.cs | 4 +- Kyoo.Common/Models/Resources/User.cs | 2 +- Kyoo.Common/Models/WatchItem.cs | 34 ++++++++----- .../Migrations/20210621175342_Triggers.cs | 25 ++++++++-- .../Library/SpecificTests/EpisodeTest.cs | 28 ++++++----- Kyoo.Tests/Library/TestSample.cs | 5 +- Kyoo/Controllers/ProviderManager.cs | 13 +++-- .../Repositories/EpisodeRepository.cs | 2 +- .../Repositories/TrackRepository.cs | 4 +- Kyoo/Startup.cs | 4 +- Kyoo/Tasks/Crawler.cs | 31 ++++++------ 16 files changed, 113 insertions(+), 98 deletions(-) diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs index ce9592f9..a0c30cbb 100644 --- a/Kyoo.Common/Controllers/IMetadataProvider.cs +++ b/Kyoo.Common/Controllers/IMetadataProvider.cs @@ -16,6 +16,6 @@ namespace Kyoo.Controllers Task GetSeason(Show show, int seasonNumber); - Task GetEpisode(Show show, int seasonNumber, int episodeNumber, int absoluteNumber); + Task GetEpisode(Show show, int? seasonNumber, int? episodeNumber, int? absoluteNumber); } } diff --git a/Kyoo.Common/Controllers/IProviderManager.cs b/Kyoo.Common/Controllers/IProviderManager.cs index d1136052..dd83a283 100644 --- a/Kyoo.Common/Controllers/IProviderManager.cs +++ b/Kyoo.Common/Controllers/IProviderManager.cs @@ -11,7 +11,7 @@ namespace Kyoo.Controllers Task SearchShow(string showName, bool isMovie, Library library); Task> SearchShows(string showName, bool isMovie, Library library); Task GetSeason(Show show, int seasonNumber, Library library); - Task GetEpisode(Show show, string episodePath, int seasonNumber, int episodeNumber, int absoluteNumber, Library library); + Task GetEpisode(Show show, string episodePath, int? seasonNumber, int? episodeNumber, int? absoluteNumber, Library library); Task> GetPeople(Show show, Library library); } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 37cd51aa..7c5bd7c7 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Text.RegularExpressions; using JetBrains.Annotations; using Kyoo.Controllers; @@ -10,9 +9,8 @@ namespace Kyoo.Models { /// /// A class to represent a single show's episode. - /// This is also used internally for movies (their number is juste set to -1). /// - public class Episode : IResource, IOnMerge + public class Episode : IResource { /// public int ID { get; set; } @@ -31,6 +29,7 @@ namespace Kyoo.Models if (value == null) throw new ArgumentNullException(nameof(value)); + Console.WriteLine(value); Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); if (match.Success) @@ -44,13 +43,13 @@ namespace Kyoo.Models match = Regex.Match(value, @"(?.*)-(?\d*)"); if (match.Success) { - ShowSlug = match.Groups["Show"].Value; + ShowSlug = match.Groups["show"].Value; AbsoluteNumber = int.Parse(match.Groups["absolute"].Value); } else ShowSlug = value; - SeasonNumber = -1; - EpisodeNumber = -1; + SeasonNumber = null; + EpisodeNumber = null; } } } @@ -82,20 +81,17 @@ namespace Kyoo.Models /// /// The season in witch this episode is in. /// - [DefaultValue(-1)] - public int SeasonNumber { get; set; } = -1; + public int? SeasonNumber { get; set; } /// /// The number of this episode is it's season. /// - [DefaultValue(-1)] - public int EpisodeNumber { get; set; } = -1; + public int? EpisodeNumber { get; set; } /// /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. /// - [DefaultValue(-1)] - public int AbsoluteNumber { get; set; } = -1; + public int? AbsoluteNumber { get; set; } /// /// The path of the video file for this episode. Any format supported by a is allowed. @@ -141,43 +137,31 @@ namespace Kyoo.Models /// The slug of the show. It can't be null. /// /// The season in which the episode is. - /// If this is a movie or if the episode should be referred by it's absolute number, set this to -1. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to null. /// /// /// The number of the episode in it's season. - /// If this is a movie or if the episode should be referred by it's absolute number, set this to -1. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to null. /// /// /// The absolute number of this show. - /// If you don't know it or this is a movie, use -1 + /// If you don't know it or this is a movie, use null /// /// The slug corresponding to the given arguments /// The given show slug was null. public static string GetSlug([NotNull] string showSlug, - int seasonNumber = -1, - int episodeNumber = -1, - int absoluteNumber = -1) + int? seasonNumber, + int? episodeNumber, + int? absoluteNumber = null) { if (showSlug == null) throw new ArgumentNullException(nameof(showSlug)); return seasonNumber switch { - -1 when absoluteNumber == -1 => showSlug, - -1 => $"{showSlug}-{absoluteNumber}", + null when absoluteNumber == null => showSlug, + null => $"{showSlug}-{absoluteNumber}", _ => $"{showSlug}-s{seasonNumber}e{episodeNumber}" }; } - - /// - public void OnMerge(object merged) - { - Episode other = (Episode)merged; - if (SeasonNumber == -1 && other.SeasonNumber != -1) - SeasonNumber = other.SeasonNumber; - if (EpisodeNumber == -1 && other.EpisodeNumber != -1) - EpisodeNumber = other.EpisodeNumber; - if (AbsoluteNumber == -1 && other.AbsoluteNumber != -1) - AbsoluteNumber = other.AbsoluteNumber; - } } } diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 028022b6..07411cf3 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -50,9 +50,9 @@ namespace Kyoo.Models [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } /// - /// The number of this season. This can be set to 0 to indicate specials. This defaults to -1 for unset. + /// The number of this season. This can be set to 0 to indicate specials. /// - public int SeasonNumber { get; set; } = -1; + public int SeasonNumber { get; set; } /// /// The title of this season. diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index 788ffff3..5232dee2 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -118,7 +118,8 @@ namespace Kyoo.Models [LoadableRelation] public ICollection Seasons { get; set; } /// - /// The list of episodes in this show. If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to -1). + /// The list of episodes in this show. + /// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null). /// Having an episode is necessary to store metadata and tracks. /// [LoadableRelation] public ICollection Episodes { get; set; } diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index e0d543c2..38006392 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -63,8 +63,8 @@ namespace Kyoo.Models } EpisodeSlug = Episode.GetSlug(match.Groups["show"].Value, - match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : -1, - match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1); + match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : null, + match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : null); Language = match.Groups["language"].Value; IsForced = match.Groups["forced"].Success; if (match.Groups["type"].Success) diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs index 1f497541..05f56534 100644 --- a/Kyoo.Common/Models/Resources/User.cs +++ b/Kyoo.Common/Models/Resources/User.cs @@ -63,7 +63,7 @@ namespace Kyoo.Models public class WatchedEpisode : Link { /// - /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100). + /// Where the player has stopped watching the episode (between 0 and 100). /// public int WatchedPercentage { get; set; } } diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index a7e15fd1..3b64d54e 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -37,20 +37,19 @@ namespace Kyoo.Models public string ShowSlug { get; set; } /// - /// The season in witch this episode is in. This defaults to -1 if not specified. + /// The season in witch this episode is in. /// - public int SeasonNumber { get; set; } + public int? SeasonNumber { get; set; } /// - /// The number of this episode is it's season. This defaults to -1 if not specified. + /// The number of this episode is it's season. /// - public int EpisodeNumber { get; set; } + public int? EpisodeNumber { get; set; } /// /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. - /// This defaults to -1 if not specified. /// - public int AbsoluteNumber { get; set; } + public int? AbsoluteNumber { get; set; } /// /// The title of this episode. @@ -148,21 +147,30 @@ namespace Kyoo.Models await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Tracks); - if (!ep.Show.IsMovie) + if (!ep.Show.IsMovie && ep.SeasonNumber != null && ep.EpisodeNumber != null) { if (ep.EpisodeNumber > 1) - previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1); + previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value - 1); else if (ep.SeasonNumber > 1) { - int count = await library.GetCount(x => x.ShowID == ep.ShowID - && x.SeasonNumber == ep.SeasonNumber - 1); - previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber - 1, count); + previous = (await library.GetAll(x => x.ShowID == ep.ShowID + && x.SeasonNumber == ep.SeasonNumber.Value - 1, + limit: 1, + sort: new Sort(x => x.EpisodeNumber, true)) + ).FirstOrDefault(); } if (ep.EpisodeNumber >= await library.GetCount(x => x.SeasonID == ep.SeasonID)) - next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber + 1, 1); + next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value + 1, 1); else - next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1); + next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value + 1); + } + else if (!ep.Show.IsMovie && ep.AbsoluteNumber != null) + { + previous = await library.GetOrDefault(x => x.ShowID == ep.ShowID + && x.AbsoluteNumber == ep.EpisodeNumber + 1); + next = await library.GetOrDefault(x => x.ShowID == ep.ShowID + && x.AbsoluteNumber == ep.AbsoluteNumber + 1); } return new WatchItem diff --git a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs index b6477eeb..0f0596dd 100644 --- a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs @@ -25,14 +25,25 @@ namespace Kyoo.SqLite.Migrations migrationBuilder.Sql(@" CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW BEGIN - UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber || 'e' || EpisodeNumber + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CASE (SeasonNumber) + WHEN NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END WHERE ID == new.ID; END"); // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF EpisodeNumber, SeasonNumber, ShowID ON Episodes FOR EACH ROW + CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID + ON Episodes FOR EACH ROW BEGIN - UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber || 'e' || EpisodeNumber + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CASE (SeasonNumber) + WHEN NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END WHERE ID == new.ID; END"); @@ -42,7 +53,13 @@ namespace Kyoo.SqLite.Migrations CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW BEGIN UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; - UPDATE Episodes SET Slug = new.Slug || '-s' || SeasonNumber || 'e' || EpisodeNumber WHERE ShowID = new.ID; + UPDATE Episodes + SET Slug = new.Slug || + CASE (SeasonNumber) + WHEN NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END + WHERE ShowID = new.ID; END;"); } diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index 868de9d7..22c4cdee 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -55,11 +55,12 @@ namespace Kyoo.Tests.Library { Episode episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - await _repository.Edit(new Episode + episode = await _repository.Edit(new Episode { ID = 1, SeasonNumber = 2 }, false); + Assert.Equal($"{TestSample.Get().Slug}-s2e2", episode.Slug); episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); } @@ -69,11 +70,12 @@ namespace Kyoo.Tests.Library { Episode episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - await _repository.Edit(new Episode + episode = await _repository.Edit(new Episode { ID = 1, EpisodeNumber = 2 }, false); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); } @@ -127,12 +129,13 @@ namespace Kyoo.Tests.Library public async Task AbsoluteNumberEditTest() { await _repository.Create(TestSample.GetAbsoluteEpisode()); - await _repository.Edit(new Episode + Episode episode = await _repository.Edit(new Episode { - ID = 1, + ID = 2, AbsoluteNumber = 56 }, false); - Episode episode = await _repository.Get(2); + Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); + episode = await _repository.Get(2); Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); } @@ -140,27 +143,28 @@ namespace Kyoo.Tests.Library public async Task AbsoluteToNormalEditTest() { await _repository.Create(TestSample.GetAbsoluteEpisode()); - await _repository.Edit(new Episode + Episode episode = await _repository.Edit(new Episode { - ID = 1, + ID = 2, SeasonNumber = 1, EpisodeNumber = 2 }, false); - Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + episode = await _repository.Get(2); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); } [Fact] public async Task NormalToAbsoluteEditTest() { - await _repository.Create(TestSample.GetAbsoluteEpisode()); - await _repository.Edit(new Episode + Episode episode = await _repository.Edit(new Episode { ID = 1, - SeasonNumber = -1, + SeasonNumber = null, AbsoluteNumber = 12 }, false); - Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); + episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); } } diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index cb8650fa..937fac9a 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -129,9 +129,8 @@ namespace Kyoo.Tests ID = 2, ShowSlug = "anohana", ShowID = 1, - SeasonID = -1, - SeasonNumber = -1, - EpisodeNumber = -1, + SeasonNumber = null, + EpisodeNumber = null, AbsoluteNumber = 3, Path = "/home/kyoo/anohana-3", Thumb = "thumbnail", diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 4a0a0cd1..6ed5f796 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -122,16 +122,15 @@ namespace Kyoo.Controllers season.Show = show; season.ShowID = show.ID; season.ShowSlug = show.Slug; - season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber; season.Title ??= $"Season {season.SeasonNumber}"; return season; } public async Task GetEpisode(Show show, string episodePath, - int seasonNumber, - int episodeNumber, - int absoluteNumber, + int? seasonNumber, + int? episodeNumber, + int? absoluteNumber, Library library) { Episode episode = await GetMetadata( @@ -142,9 +141,9 @@ namespace Kyoo.Controllers episode.ShowID = show.ID; episode.ShowSlug = show.Slug; episode.Path = episodePath; - episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber; - episode.EpisodeNumber = episode.EpisodeNumber != -1 ? episode.EpisodeNumber : episodeNumber; - episode.AbsoluteNumber = episode.AbsoluteNumber != -1 ? episode.AbsoluteNumber : absoluteNumber; + episode.SeasonNumber ??= seasonNumber; + episode.EpisodeNumber ??= episodeNumber; + episode.AbsoluteNumber ??= absoluteNumber; return episode; } diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 278e31b5..6f555571 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -99,7 +99,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await _database.Episodes - .Where(x => x.EpisodeNumber != -1) + .Where(x => x.EpisodeNumber != null) .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index b33f48a4..98b05f30 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -67,8 +67,8 @@ namespace Kyoo.Controllers } string showSlug = match.Groups["show"].Value; - int seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : -1; - int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1; + int? seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : null; + int? episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : null; string language = match.Groups["language"].Value; bool forced = match.Groups["forced"].Success; if (match.Groups["type"].Success) diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 983d1d6c..27c05c56 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -47,8 +47,8 @@ namespace Kyoo // TODO remove postgres from here and load it like a normal plugin. _plugins.LoadPlugins(new IPlugin[] { new CoreModule(configuration), - new PostgresModule(configuration, host), - // new SqLiteModule(configuration, host), + // new PostgresModule(configuration, host), + new SqLiteModule(configuration, host), new AuthenticationModule(configuration, loggerFactory, host) }); } diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index ebe3eaed..9f1185dd 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -210,18 +210,20 @@ namespace Kyoo.Tasks string showPath = Path.GetDirectoryName(path); string collectionName = match.Groups["Collection"].Value; string showName = match.Groups["Show"].Value; - int seasonNumber = int.TryParse(match.Groups["Season"].Value, out int tmp) ? tmp : -1; - int episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : -1; - int absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : -1; + int? seasonNumber = int.TryParse(match.Groups["Season"].Value, out int tmp) ? tmp : null; + int? episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : null; + int? absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : null; Collection collection = await GetCollection(libraryManager, collectionName, library); - bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1; + bool isMovie = seasonNumber == null && episodeNumber == null && absoluteNumber == null; Show show = await GetShow(libraryManager, showName, showPath, isMovie, library); if (isMovie) await libraryManager!.Create(await GetMovie(show, path)); else { - Season season = await GetSeason(libraryManager, show, seasonNumber, library); + Season season = seasonNumber != null + ? await GetSeason(libraryManager, show, seasonNumber.Value, library) + : null; Episode episode = await GetEpisode(libraryManager, show, season, @@ -315,8 +317,6 @@ namespace Kyoo.Tasks int seasonNumber, Library library) { - if (seasonNumber == -1) - return default; try { Season season = await libraryManager.Get(show.Slug, seasonNumber); @@ -343,21 +343,24 @@ namespace Kyoo.Tasks private async Task GetEpisode(ILibraryManager libraryManager, Show show, Season season, - int episodeNumber, - int absoluteNumber, + int? episodeNumber, + int? absoluteNumber, string episodePath, Library library) { Episode episode = await MetadataProvider.GetEpisode(show, episodePath, - season?.SeasonNumber ?? -1, + season?.SeasonNumber, episodeNumber, absoluteNumber, library); - - season ??= await GetSeason(libraryManager, show, episode.SeasonNumber, library); - episode.Season = season; - episode.SeasonID = season?.ID; + + if (episode.SeasonNumber != null) + { + season ??= await GetSeason(libraryManager, show, episode.SeasonNumber.Value, library); + episode.Season = season; + episode.SeasonID = season?.ID; + } await ThumbnailsManager.Validate(episode); await GetTracks(episode); return episode; From d4c674f2be5dce8dee9ea2504076a44e6ca87700 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 26 Jun 2021 18:29:49 +0200 Subject: [PATCH 40/57] Fixing sqlite episodes nullable --- Kyoo.Common/Models/Resources/Episode.cs | 2 +- ...ial.Designer.cs => 20210626141337_Initial.Designer.cs} | 8 ++++---- ...0210621175330_Initial.cs => 20210626141337_Initial.cs} | 6 +++--- ...rs.Designer.cs => 20210626141347_Triggers.Designer.cs} | 8 ++++---- ...10621175342_Triggers.cs => 20210626141347_Triggers.cs} | 0 Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs | 6 +++--- Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) rename Kyoo.SqLite/Migrations/{20210621175330_Initial.Designer.cs => 20210626141337_Initial.Designer.cs} (99%) rename Kyoo.SqLite/Migrations/{20210621175330_Initial.cs => 20210626141337_Initial.cs} (99%) rename Kyoo.SqLite/Migrations/{20210621175342_Triggers.Designer.cs => 20210626141347_Triggers.Designer.cs} (99%) rename Kyoo.SqLite/Migrations/{20210621175342_Triggers.cs => 20210626141347_Triggers.cs} (100%) diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 7c5bd7c7..aa947836 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -40,7 +40,7 @@ namespace Kyoo.Models } else { - match = Regex.Match(value, @"(?.*)-(?\d*)"); + match = Regex.Match(value, @"(?.+)-(?\d+)"); if (match.Success) { ShowSlug = match.Groups["show"].Value; diff --git a/Kyoo.SqLite/Migrations/20210621175330_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs similarity index 99% rename from Kyoo.SqLite/Migrations/20210621175330_Initial.Designer.cs rename to Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs index 26e94354..1337ce6b 100644 --- a/Kyoo.SqLite/Migrations/20210621175330_Initial.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210621175330_Initial")] + [Migration("20210626141337_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -51,10 +51,10 @@ namespace Kyoo.SqLite.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AbsoluteNumber") + b.Property("AbsoluteNumber") .HasColumnType("INTEGER"); - b.Property("EpisodeNumber") + b.Property("EpisodeNumber") .HasColumnType("INTEGER"); b.Property("Overview") @@ -69,7 +69,7 @@ namespace Kyoo.SqLite.Migrations b.Property("SeasonID") .HasColumnType("INTEGER"); - b.Property("SeasonNumber") + b.Property("SeasonNumber") .HasColumnType("INTEGER"); b.Property("ShowID") diff --git a/Kyoo.SqLite/Migrations/20210621175330_Initial.cs b/Kyoo.SqLite/Migrations/20210626141337_Initial.cs similarity index 99% rename from Kyoo.SqLite/Migrations/20210621175330_Initial.cs rename to Kyoo.SqLite/Migrations/20210626141337_Initial.cs index fb73c02a..87d98348 100644 --- a/Kyoo.SqLite/Migrations/20210621175330_Initial.cs +++ b/Kyoo.SqLite/Migrations/20210626141337_Initial.cs @@ -407,9 +407,9 @@ namespace Kyoo.SqLite.Migrations Slug = table.Column(type: "TEXT", nullable: true), ShowID = table.Column(type: "INTEGER", nullable: false), SeasonID = table.Column(type: "INTEGER", nullable: true), - SeasonNumber = table.Column(type: "INTEGER", nullable: false), - EpisodeNumber = table.Column(type: "INTEGER", nullable: false), - AbsoluteNumber = table.Column(type: "INTEGER", nullable: false), + SeasonNumber = table.Column(type: "INTEGER", nullable: true), + EpisodeNumber = table.Column(type: "INTEGER", nullable: true), + AbsoluteNumber = table.Column(type: "INTEGER", nullable: true), Path = table.Column(type: "TEXT", nullable: true), Thumb = table.Column(type: "TEXT", nullable: true), Title = table.Column(type: "TEXT", nullable: true), diff --git a/Kyoo.SqLite/Migrations/20210621175342_Triggers.Designer.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs similarity index 99% rename from Kyoo.SqLite/Migrations/20210621175342_Triggers.Designer.cs rename to Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs index 0f99af22..02045e3f 100644 --- a/Kyoo.SqLite/Migrations/20210621175342_Triggers.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Kyoo.SqLite.Migrations { [DbContext(typeof(SqLiteContext))] - [Migration("20210621175342_Triggers")] + [Migration("20210626141347_Triggers")] partial class Triggers { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -51,10 +51,10 @@ namespace Kyoo.SqLite.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AbsoluteNumber") + b.Property("AbsoluteNumber") .HasColumnType("INTEGER"); - b.Property("EpisodeNumber") + b.Property("EpisodeNumber") .HasColumnType("INTEGER"); b.Property("Overview") @@ -69,7 +69,7 @@ namespace Kyoo.SqLite.Migrations b.Property("SeasonID") .HasColumnType("INTEGER"); - b.Property("SeasonNumber") + b.Property("SeasonNumber") .HasColumnType("INTEGER"); b.Property("ShowID") diff --git a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs similarity index 100% rename from Kyoo.SqLite/Migrations/20210621175342_Triggers.cs rename to Kyoo.SqLite/Migrations/20210626141347_Triggers.cs diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs index fc0b3cbc..58ded130 100644 --- a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs +++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs @@ -49,10 +49,10 @@ namespace Kyoo.SqLite.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AbsoluteNumber") + b.Property("AbsoluteNumber") .HasColumnType("INTEGER"); - b.Property("EpisodeNumber") + b.Property("EpisodeNumber") .HasColumnType("INTEGER"); b.Property("Overview") @@ -67,7 +67,7 @@ namespace Kyoo.SqLite.Migrations b.Property("SeasonID") .HasColumnType("INTEGER"); - b.Property("SeasonNumber") + b.Property("SeasonNumber") .HasColumnType("INTEGER"); b.Property("ShowID") diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index 22c4cdee..156bc9b2 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -60,7 +60,7 @@ namespace Kyoo.Tests.Library ID = 1, SeasonNumber = 2 }, false); - Assert.Equal($"{TestSample.Get().Slug}-s2e2", episode.Slug); + Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); } From 27ed4709705c3dddbc0b36a0c01cad98faf4056b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 27 Jun 2021 16:19:00 +0200 Subject: [PATCH 41/57] Fixing an episode test --- Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index 156bc9b2..1433860e 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -157,12 +157,10 @@ namespace Kyoo.Tests.Library [Fact] public async Task NormalToAbsoluteEditTest() { - Episode episode = await _repository.Edit(new Episode - { - ID = 1, - SeasonNumber = null, - AbsoluteNumber = 12 - }, false); + Episode episode = await _repository.Get(1); + episode.SeasonNumber = null; + episode.AbsoluteNumber = 12; + episode = await _repository.Edit(episode, true); Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); From 6f871398155371dbc0ef723ec74eeddb34be5145 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 27 Jun 2021 20:21:03 +0200 Subject: [PATCH 42/57] Fixing SQLite episode triggers --- Kyoo.Common/Models/Resources/Episode.cs | 4 ++-- Kyoo.Common/Models/Resources/Season.cs | 2 +- Kyoo.Common/Models/Resources/Show.cs | 2 +- Kyoo.Common/Models/Resources/Track.cs | 2 +- Kyoo.Common/Utility/Merger.cs | 8 ++------ Kyoo.CommonAPI/DatabaseContext.cs | 8 -------- ...er.cs => 20210627141933_Initial.Designer.cs} | 8 ++++---- ...924_Initial.cs => 20210627141933_Initial.cs} | 6 +++--- ...r.cs => 20210627141941_Triggers.Designer.cs} | 8 ++++---- ...2_Triggers.cs => 20210627141941_Triggers.cs} | 17 +++++++++++------ .../Migrations/PostgresContextModelSnapshot.cs | 6 +++--- .../Migrations/20210626141347_Triggers.cs | 12 ++++++------ .../Repositories/EpisodeRepository.cs | 6 +++++- Kyoo/Startup.cs | 4 ++-- 14 files changed, 45 insertions(+), 48 deletions(-) rename Kyoo.Postgresql/Migrations/{20210623174924_Initial.Designer.cs => 20210627141933_Initial.Designer.cs} (99%) rename Kyoo.Postgresql/Migrations/{20210623174924_Initial.cs => 20210627141933_Initial.cs} (99%) rename Kyoo.Postgresql/Migrations/{20210623174932_Triggers.Designer.cs => 20210627141941_Triggers.Designer.cs} (99%) rename Kyoo.Postgresql/Migrations/{20210623174932_Triggers.cs => 20210627141941_Triggers.cs} (80%) diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index aa947836..594763be 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -60,7 +60,7 @@ namespace Kyoo.Models [SerializeIgnore] public string ShowSlug { private get; set; } /// - /// The ID of the Show containing this episode. This value is only set when the has been loaded. + /// The ID of the Show containing this episode. /// [SerializeIgnore] public int ShowID { get; set; } /// @@ -69,7 +69,7 @@ namespace Kyoo.Models [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } /// - /// The ID of the Season containing this episode. This value is only set when the has been loaded. + /// The ID of the Season containing this episode. /// [SerializeIgnore] public int? SeasonID { get; set; } /// diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 07411cf3..9b292020 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -41,7 +41,7 @@ namespace Kyoo.Models [SerializeIgnore] public string ShowSlug { private get; set; } /// - /// The ID of the Show containing this season. This value is only set when the has been loaded. + /// The ID of the Show containing this season. /// [SerializeIgnore] public int ShowID { get; set; } /// diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index 5232dee2..ffb2ae49 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -94,7 +94,7 @@ namespace Kyoo.Models [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } /// - /// The ID of the Studio that made this show. This value is only set when the has been loaded. + /// The ID of the Studio that made this show. /// [SerializeIgnore] public int? StudioID { get; set; } /// diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 38006392..00ae745c 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -119,7 +119,7 @@ namespace Kyoo.Models [SerializeIgnore] public StreamType Type { get; set; } /// - /// The ID of the episode that uses this track. This value is only set when the has been loaded. + /// The ID of the episode that uses this track. /// [SerializeIgnore] public int EpisodeID { get; set; } /// diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs index 417ea944..55cc17e3 100644 --- a/Kyoo.Common/Utility/Merger.cs +++ b/Kyoo.Common/Utility/Merger.cs @@ -163,13 +163,9 @@ namespace Kyoo Type type = typeof(T); foreach (PropertyInfo property in type.GetProperties()) { - if (!property.CanWrite) + if (!property.CanWrite || property.GetCustomAttribute() != null) continue; - - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - property.SetValue(obj, defaultValue); + property.SetValue(obj, property.PropertyType.GetClrDefault()); } return obj; diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index a2b7eeb2..afca55b5 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -132,14 +132,6 @@ namespace Kyoo { base.OnModelCreating(modelBuilder); - modelBuilder.Entity() - .Property(t => t.IsDefault) - .ValueGeneratedNever(); - - modelBuilder.Entity() - .Property(t => t.IsForced) - .ValueGeneratedNever(); - modelBuilder.Entity() .HasMany(x => x.Seasons) .WithOne(x => x.Show) diff --git a/Kyoo.Postgresql/Migrations/20210623174924_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs similarity index 99% rename from Kyoo.Postgresql/Migrations/20210623174924_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs index c6943ac2..f1733a77 100644 --- a/Kyoo.Postgresql/Migrations/20210623174924_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210623174924_Initial")] + [Migration("20210627141933_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -69,11 +69,11 @@ namespace Kyoo.Postgresql.Migrations .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AbsoluteNumber") + b.Property("AbsoluteNumber") .HasColumnType("integer") .HasColumnName("absolute_number"); - b.Property("EpisodeNumber") + b.Property("EpisodeNumber") .HasColumnType("integer") .HasColumnName("episode_number"); @@ -93,7 +93,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer") .HasColumnName("season_id"); - b.Property("SeasonNumber") + b.Property("SeasonNumber") .HasColumnType("integer") .HasColumnName("season_number"); diff --git a/Kyoo.Postgresql/Migrations/20210623174924_Initial.cs b/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs similarity index 99% rename from Kyoo.Postgresql/Migrations/20210623174924_Initial.cs rename to Kyoo.Postgresql/Migrations/20210627141933_Initial.cs index 45c8e00d..50bed8f7 100644 --- a/Kyoo.Postgresql/Migrations/20210623174924_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs @@ -415,9 +415,9 @@ namespace Kyoo.Postgresql.Migrations slug = table.Column(type: "text", nullable: true), show_id = table.Column(type: "integer", nullable: false), season_id = table.Column(type: "integer", nullable: true), - season_number = table.Column(type: "integer", nullable: false), - episode_number = table.Column(type: "integer", nullable: false), - absolute_number = table.Column(type: "integer", nullable: false), + season_number = table.Column(type: "integer", nullable: true), + episode_number = table.Column(type: "integer", nullable: true), + absolute_number = table.Column(type: "integer", nullable: true), path = table.Column(type: "text", nullable: true), thumb = table.Column(type: "text", nullable: true), title = table.Column(type: "text", nullable: true), diff --git a/Kyoo.Postgresql/Migrations/20210623174932_Triggers.Designer.cs b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs similarity index 99% rename from Kyoo.Postgresql/Migrations/20210623174932_Triggers.Designer.cs rename to Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs index ebd1669f..fc019baf 100644 --- a/Kyoo.Postgresql/Migrations/20210623174932_Triggers.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210623174932_Triggers")] + [Migration("20210627141941_Triggers")] partial class Triggers { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -69,11 +69,11 @@ namespace Kyoo.Postgresql.Migrations .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AbsoluteNumber") + b.Property("AbsoluteNumber") .HasColumnType("integer") .HasColumnName("absolute_number"); - b.Property("EpisodeNumber") + b.Property("EpisodeNumber") .HasColumnType("integer") .HasColumnName("episode_number"); @@ -93,7 +93,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer") .HasColumnName("season_id"); - b.Property("SeasonNumber") + b.Property("SeasonNumber") .HasColumnType("integer") .HasColumnName("season_number"); diff --git a/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs similarity index 80% rename from Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs rename to Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs index 4ab90659..bfa66554 100644 --- a/Kyoo.Postgresql/Migrations/20210623174932_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs @@ -37,10 +37,10 @@ namespace Kyoo.Postgresql.Migrations BEGIN NEW.slug := CONCAT( (SELECT slug FROM shows WHERE id = NEW.show_id), - '-s', - NEW.season_number, - 'e', - NEW.episode_number + CASE + WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number) + ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number) + END ); RETURN NEW; END @@ -48,7 +48,8 @@ namespace Kyoo.Postgresql.Migrations // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER episode_slug_trigger BEFORE INSERT OR UPDATE OF episode_number, season_number, show_id ON episodes + CREATE TRIGGER episode_slug_trigger + BEFORE INSERT OR UPDATE OF absolute_number, episode_number, season_number, show_id ON episodes FOR EACH ROW EXECUTE PROCEDURE episode_slug_update();"); @@ -60,7 +61,11 @@ namespace Kyoo.Postgresql.Migrations AS $$ BEGIN UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id; - UPDATE episodes SET slug = CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) WHERE show_id = NEW.id; + UPDATE episodes SET slug = CASE + WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number) + ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) + END + WHERE show_id = NEW.id; RETURN NEW; END $$;"); diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 554ef95f..e5044c60 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -67,11 +67,11 @@ namespace Kyoo.Postgresql.Migrations .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AbsoluteNumber") + b.Property("AbsoluteNumber") .HasColumnType("integer") .HasColumnName("absolute_number"); - b.Property("EpisodeNumber") + b.Property("EpisodeNumber") .HasColumnType("integer") .HasColumnName("episode_number"); @@ -91,7 +91,7 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("integer") .HasColumnName("season_id"); - b.Property("SeasonNumber") + b.Property("SeasonNumber") .HasColumnType("integer") .HasColumnName("season_number"); diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs index 0f0596dd..d6d3ca22 100644 --- a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs @@ -27,8 +27,8 @@ namespace Kyoo.SqLite.Migrations BEGIN UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || - CASE (SeasonNumber) - WHEN NULL THEN '-' || AbsoluteNumber + CASE + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber END WHERE ID == new.ID; @@ -40,8 +40,8 @@ namespace Kyoo.SqLite.Migrations BEGIN UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || - CASE (SeasonNumber) - WHEN NULL THEN '-' || AbsoluteNumber + CASE + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber END WHERE ID == new.ID; @@ -55,8 +55,8 @@ namespace Kyoo.SqLite.Migrations UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; UPDATE Episodes SET Slug = new.Slug || - CASE (SeasonNumber) - WHEN NULL THEN '-' || AbsoluteNumber + CASE + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber END WHERE ShowID = new.ID; diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 6f555571..2ffa3f1d 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -114,6 +114,9 @@ namespace Kyoo.Controllers obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); return await ValidateTracks(obj); + // TODO check if this is needed + // obj.Slug = await _database.Entry(obj).Property(x => x.Slug). + // return obj; } /// @@ -148,7 +151,8 @@ namespace Kyoo.Controllers resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.MapAsync((x, i) => { x.Episode = resource; - x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language + // TODO use a trigger for the next line. + x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language && x.IsForced == y.IsForced && x.Codec == y.Codec && x.Type == y.Type); diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 27c05c56..983d1d6c 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -47,8 +47,8 @@ namespace Kyoo // TODO remove postgres from here and load it like a normal plugin. _plugins.LoadPlugins(new IPlugin[] { new CoreModule(configuration), - // new PostgresModule(configuration, host), - new SqLiteModule(configuration, host), + new PostgresModule(configuration, host), + // new SqLiteModule(configuration, host), new AuthenticationModule(configuration, loggerFactory, host) }); } From b5f31eba02ca12fbac804a8d4aa6f3cdde340263 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 27 Jun 2021 21:45:57 +0200 Subject: [PATCH 43/57] Handling tests logging and fixing tests CI --- .github/workflows/tests.yml | 15 +++++++++++ Kyoo.Common/Models/Resources/Episode.cs | 1 - Kyoo.Tests/Dockerfile | 3 --- Kyoo.Tests/Kyoo.Tests.csproj | 1 + Kyoo.Tests/Library/RepositoryActivator.cs | 7 +++--- .../Library/SpecificTests/EpisodeTest.cs | 11 +++++--- .../Library/SpecificTests/SanityTests.cs | 5 ++-- .../Library/SpecificTests/SeasonTests.cs | 9 ++++--- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 9 ++++--- Kyoo.Tests/Library/TestContext.cs | 25 +++++++++++++------ 10 files changed, 57 insertions(+), 29 deletions(-) delete mode 100644 Kyoo.Tests/Dockerfile diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67ab67dd..d9a5b261 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,17 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + container: mcr.microsoft.com/dotnet/sdk:5.0 + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v2 - name: Setup .NET @@ -17,3 +28,7 @@ jobs: run: dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' - name: Test run: dotnet test --no-build + env: + POSTGRES_HOST: postgres + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: postgres diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 594763be..cdb7fb8f 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -29,7 +29,6 @@ namespace Kyoo.Models if (value == null) throw new ArgumentNullException(nameof(value)); - Console.WriteLine(value); Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); if (match.Success) diff --git a/Kyoo.Tests/Dockerfile b/Kyoo.Tests/Dockerfile deleted file mode 100644 index 423bfe57..00000000 --- a/Kyoo.Tests/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:5.0 -COPY ../ . -RUN dotnet tests '-p:SkipWebApp=true;SkipTranscoder=true' diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index 265ccb98..93873bc7 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -14,6 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index 9f7f78fb..e7249e3a 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Kyoo.Controllers; +using Xunit.Abstractions; namespace Kyoo.Tests { @@ -12,11 +13,11 @@ namespace Kyoo.Tests private readonly DatabaseContext _database; - public RepositoryActivator(PostgresFixture postgres = null) + public RepositoryActivator(ITestOutputHelper output, PostgresFixture postgres = null) { Context = postgres == null - ? new SqLiteTestContext() - : new PostgresTestContext(postgres); + ? new SqLiteTestContext(output) + : new PostgresTestContext(postgres, output); _database = Context.New(); ProviderRepository provider = new(_database); diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index 1433860e..7a617fdb 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; using Xunit; +using Xunit.Abstractions; namespace Kyoo.Tests.Library { @@ -9,8 +10,8 @@ namespace Kyoo.Tests.Library { public class EpisodeTests : AEpisodeTests { - public EpisodeTests() - : base(new RepositoryActivator()) { } + public EpisodeTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } } } @@ -20,8 +21,8 @@ namespace Kyoo.Tests.Library [Collection(nameof(Postgresql))] public class EpisodeTests : AEpisodeTests { - public EpisodeTests(PostgresFixture postgres) - : base(new RepositoryActivator(postgres)) { } + public EpisodeTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } } } @@ -165,5 +166,7 @@ namespace Kyoo.Tests.Library episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); } + + // TODO add movies tests. } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs index 098c4677..78637d35 100644 --- a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Kyoo.Models; using Xunit; +using Xunit.Abstractions; namespace Kyoo.Tests.Library { @@ -10,9 +11,9 @@ namespace Kyoo.Tests.Library { private readonly RepositoryActivator _repositories; - public GlobalTests() + public GlobalTests(ITestOutputHelper output) { - _repositories = new RepositoryActivator(); + _repositories = new RepositoryActivator(output); } [Fact] diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs index 0c912d07..39be8b82 100644 --- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; using Xunit; +using Xunit.Abstractions; namespace Kyoo.Tests.Library { @@ -9,8 +10,8 @@ namespace Kyoo.Tests.Library { public class SeasonTests : ASeasonTests { - public SeasonTests() - : base(new RepositoryActivator()) { } + public SeasonTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } } } @@ -20,8 +21,8 @@ namespace Kyoo.Tests.Library [Collection(nameof(Postgresql))] public class SeasonTests : ASeasonTests { - public SeasonTests(PostgresFixture postgres) - : base(new RepositoryActivator(postgres)) { } + public SeasonTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } } } diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index facb8e81..8940f0c3 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -6,6 +6,7 @@ using Kyoo.Controllers; using Kyoo.Models; using Microsoft.EntityFrameworkCore; using Xunit; +using Xunit.Abstractions; namespace Kyoo.Tests.Library { @@ -13,8 +14,8 @@ namespace Kyoo.Tests.Library { public class ShowTests : AShowTests { - public ShowTests() - : base(new RepositoryActivator()) { } + public ShowTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } } } @@ -23,8 +24,8 @@ namespace Kyoo.Tests.Library [Collection(nameof(Postgresql))] public class ShowTests : AShowTests { - public ShowTests(PostgresFixture postgres) - : base(new RepositoryActivator(postgres)) { } + public ShowTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } } } diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index 68f27012..fa2935b8 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Npgsql; using Xunit; +using Xunit.Abstractions; namespace Kyoo.Tests { @@ -22,14 +23,18 @@ namespace Kyoo.Tests /// private readonly DbContextOptions _context; - public SqLiteTestContext() + public SqLiteTestContext(ITestOutputHelper output) { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); _context = new DbContextOptionsBuilder() .UseSqlite(_connection) - .UseLoggerFactory(LoggerFactory.Create(x => x.AddConsole())) + .UseLoggerFactory(LoggerFactory.Create(x => + { + x.ClearProviders(); + x.AddXunit(output); + })) .EnableSensitiveDataLogging() .EnableDetailedErrors() .Options; @@ -101,7 +106,7 @@ namespace Kyoo.Tests private readonly NpgsqlConnection _connection; private readonly DbContextOptions _context; - public PostgresTestContext(PostgresFixture template) + public PostgresTestContext(PostgresFixture template, ITestOutputHelper output) { string id = Guid.NewGuid().ToString().Replace('-', '_'); string database = $"kyoo_test_{id}"; @@ -118,7 +123,11 @@ namespace Kyoo.Tests _context = new DbContextOptionsBuilder() .UseNpgsql(_connection) - .UseLoggerFactory(LoggerFactory.Create(x => x.AddConsole())) + .UseLoggerFactory(LoggerFactory.Create(x => + { + x.ClearProviders(); + x.AddXunit(output); + })) .EnableSensitiveDataLogging() .EnableDetailedErrors() .Options; @@ -126,10 +135,10 @@ namespace Kyoo.Tests public static string GetConnectionString(string database) { - string server = Environment.GetEnvironmentVariable("SERVER") ?? "127.0.0.1"; - string port = Environment.GetEnvironmentVariable("PORT") ?? "5432"; - string username = Environment.GetEnvironmentVariable("USERNAME") ?? "kyoo"; - string password = Environment.GetEnvironmentVariable("PASSWORD") ?? "kyooPassword"; + string server = Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "127.0.0.1"; + string port = Environment.GetEnvironmentVariable("POSTGRES_PORT") ?? "5432"; + string username = Environment.GetEnvironmentVariable("POSTGRES_USERNAME") ?? "kyoo"; + string password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "kyooPassword"; return $"Server={server};Port={port};Database={database};User ID={username};Password={password};Include Error Detail=true"; } From 23977eed1fea71658d60d7d8218edce6f0fbf4f1 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 27 Jun 2021 22:16:13 +0200 Subject: [PATCH 44/57] Handling coverage in the CI --- .github/workflows/analysis.yml | 23 +++++++++++++++++------ .github/workflows/tests.yml | 15 ++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 01ef1a22..4754bc28 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -2,7 +2,7 @@ name: Analysis on: [push, pull_request] jobs: - build: + analysis: name: Static Analysis runs-on: ubuntu-latest steps: @@ -28,16 +28,27 @@ jobs: run: | mkdir -p ./.sonar/scanner dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + - name: Wait for tests to run + uses: lewagon/wait-on-check-action@master + with: + ref: ${{github.ref}} + check-name: tests + repo-token: ${{secrets.GITHUB_TOKEN}} + running-workflow-name: analysis + allowed-conclusions: success,skipped,cancelled,neutral,failed + - name: Download coverage report + uses: dawidd6/action-download-artifact@v2 + with: + commit: ${{env.COMMIT_SHA}} + workflow: tests.yml + name: coverage.xml + github_token: ${{secrets.GITHUB_TOKEN}} - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} shell: bash run: | - dotnet test \ - '-p:CollectCoverage=true;CoverletOutputFormat=opencover' \ - '-p:SkipTranscoder=true;SkipWebApp=true' || echo "Test failed. Skipping..." - dotnet build-server shutdown ./.sonar/scanner/dotnet-sonarscanner begin \ @@ -45,7 +56,7 @@ jobs: -o:"anonymus-raccoon" \ -d:sonar.login="${{ secrets.SONAR_TOKEN }}" \ -d:sonar.host.url="https://sonarcloud.io" \ - -d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + -d:sonar.cs.opencover.reportsPaths="./coverage.xml" dotnet build --no-incremental '-p:SkipTranscoder=true;SkipWebApp=true' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d9a5b261..0c0f5cdb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: Testing on: [push, pull_request] jobs: - build: + tests: runs-on: ubuntu-latest container: mcr.microsoft.com/dotnet/sdk:5.0 services: @@ -18,17 +18,18 @@ jobs: --health-retries 5 steps: - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.x - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' + run: dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' - name: Test - run: dotnet test --no-build + run: dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover' env: POSTGRES_HOST: postgres POSTGRES_USERNAME: postgres POSTGRES_PASSWORD: postgres + - name: Upload coverage report + uses: actions/upload-artifact@v2 + with: + name: coverage.xml + path: "**/coverage.opencover.xml" \ No newline at end of file From c643222984b4c0329047aa3dc14be58ad2ecc097 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 27 Jun 2021 22:19:31 +0200 Subject: [PATCH 45/57] Fixing coverage artifact --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c0f5cdb..238a6f76 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,4 +32,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: coverage.xml - path: "**/coverage.opencover.xml" \ No newline at end of file + path: "Kyoo.Tests/coverage.opencover.xml" \ No newline at end of file From 94c4c7d146a37190bae333fb1c28658c609308c1 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 27 Jun 2021 22:23:12 +0200 Subject: [PATCH 46/57] Fixing sonarcloud coverage search path --- .github/workflows/analysis.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 4754bc28..ce7b185c 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -56,7 +56,7 @@ jobs: -o:"anonymus-raccoon" \ -d:sonar.login="${{ secrets.SONAR_TOKEN }}" \ -d:sonar.host.url="https://sonarcloud.io" \ - -d:sonar.cs.opencover.reportsPaths="./coverage.xml" + -d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" dotnet build --no-incremental '-p:SkipTranscoder=true;SkipWebApp=true' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 238a6f76..0c0f5cdb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,4 +32,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: coverage.xml - path: "Kyoo.Tests/coverage.opencover.xml" \ No newline at end of file + path: "**/coverage.opencover.xml" \ No newline at end of file From aad4336f48cd85f7789ca18d6c479f9363f74612 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 28 Jun 2021 10:53:13 +0200 Subject: [PATCH 47/57] Adding debug prints for the analysis --- .github/workflows/analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index ce7b185c..ea7b58e3 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -41,7 +41,6 @@ jobs: with: commit: ${{env.COMMIT_SHA}} workflow: tests.yml - name: coverage.xml github_token: ${{secrets.GITHUB_TOKEN}} - name: Build and analyze env: @@ -49,6 +48,7 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} shell: bash run: | + find . -name 'coverage.opencover.xml' dotnet build-server shutdown ./.sonar/scanner/dotnet-sonarscanner begin \ From 52e4626b75d10db65cf30b930e68087f84361cf2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 28 Jun 2021 14:17:09 +0200 Subject: [PATCH 48/57] Adding library item tests --- Kyoo.Tests/KAssert.cs | 3 +- .../Library/SpecificTests/LibraryItemTest.cs | 60 +++++++++++++++++++ Kyoo.Tests/Library/TestSample.cs | 15 +++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs diff --git a/Kyoo.Tests/KAssert.cs b/Kyoo.Tests/KAssert.cs index 7ac753dd..fa38160d 100644 --- a/Kyoo.Tests/KAssert.cs +++ b/Kyoo.Tests/KAssert.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Reflection; using JetBrains.Annotations; using Xunit; @@ -19,7 +20,7 @@ namespace Kyoo.Tests [AssertionMethod] public static void DeepEqual(T expected, T value) { - foreach (PropertyInfo property in typeof(T).GetProperties()) + foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Instance)) Assert.Equal(property.GetValue(expected), property.GetValue(value)); } diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs b/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs new file mode 100644 index 00000000..595ca736 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class LibraryItemTest : ALibraryItemTest + { + public LibraryItemTest(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class LibraryItemTest : ALibraryItemTest + { + public LibraryItemTest(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ALibraryItemTest + { + private readonly ILibraryItemRepository _repository; + + public ALibraryItemTest(RepositoryActivator repositories) + { + _repository = repositories.LibraryManager.LibraryItemRepository; + } + + [Fact] + public async Task CountTest() + { + Assert.Equal(2, await _repository.GetCount()); + } + + [Fact] + public async Task GetShowTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(1); + KAssert.DeepEqual(expected, actual); + } + + [Fact] + public async Task GetCollectionTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(-1); + KAssert.DeepEqual(expected, actual); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 937fac9a..2dea690a 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -17,6 +17,17 @@ namespace Kyoo.Tests private static readonly Dictionary> Samples = new() { + { + typeof(Collection), + () => new Collection + { + ID = 1, + Slug = "collection", + Name = "Collection", + Overview = "A nice collection for tests", + Poster = "Poster" + } + }, { typeof(Show), () => new Show @@ -101,6 +112,10 @@ namespace Kyoo.Tests public static void FillDatabase(DatabaseContext context) { + Collection collection = Get(); + collection.ID = 0; + context.Collections.Add(collection); + Show show = Get(); show.ID = 0; context.Shows.Add(show); From cfdaa46053f1c7a1706e630214d718fba91576af Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 28 Jun 2021 20:08:11 +0200 Subject: [PATCH 49/57] Sanitizing tests outputs --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c0f5cdb..cc5d37b6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,8 @@ jobs: POSTGRES_HOST: postgres POSTGRES_USERNAME: postgres POSTGRES_PASSWORD: postgres + - name: Sanitize coverage output + run: sed -i "s/$(pwd)//" Kyoo.Tests/coverage.opencover.xml - name: Upload coverage report uses: actions/upload-artifact@v2 with: From 531abee95b187991807cf6dbbae63eb539cc00d9 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 28 Jun 2021 20:12:40 +0200 Subject: [PATCH 50/57] Fixing sed --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc5d37b6..bd010b83 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: POSTGRES_USERNAME: postgres POSTGRES_PASSWORD: postgres - name: Sanitize coverage output - run: sed -i "s/$(pwd)//" Kyoo.Tests/coverage.opencover.xml + run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml - name: Upload coverage report uses: actions/upload-artifact@v2 with: From 0c537e1cc164feac75cd8438961de9ee980f1422 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 28 Jun 2021 21:59:08 +0200 Subject: [PATCH 51/57] Creating views for library items --- Kyoo.CommonAPI/DatabaseContext.cs | 10 ++++ .../Migrations/20210627141941_Triggers.cs | 21 +++++++++ Kyoo.Postgresql/PostgresContext.cs | 4 ++ .../Migrations/20210626141347_Triggers.cs | 19 ++++++++ Kyoo.SqLite/SqLiteContext.cs | 4 ++ Kyoo.Tests/Library/RepositoryActivator.cs | 4 +- .../Repositories/LibraryItemRepository.cs | 46 ++++--------------- 7 files changed, 68 insertions(+), 40 deletions(-) diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index afca55b5..5bf9e87a 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -74,6 +74,14 @@ namespace Kyoo /// 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 . @@ -322,6 +330,8 @@ namespace Kyoo modelBuilder.Entity() .Property(x => x.Slug) .ValueGeneratedOnAddOrUpdate(); + + // modelBuilder.Ignore(); } /// diff --git a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs index bfa66554..5e81e797 100644 --- a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs @@ -74,6 +74,25 @@ namespace Kyoo.Postgresql.Migrations migrationBuilder.Sql(@" CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows FOR EACH ROW EXECUTE PROCEDURE show_slug_update();"); + + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE VIEW library_items AS + SELECT s.id, s.slug, s.title, s.overview, s.status, s.start_air, s.end_air, s.poster, CASE + WHEN s.is_movie THEN 'movie'::item_type + ELSE 'show'::item_type + END AS type + FROM shows AS s + WHERE NOT (EXISTS ( + SELECT 1 + FROM link_collection_show AS l + INNER JOIN collections AS c ON l.first_id = c.id + WHERE s.id = l.second_id)) + UNION ALL + SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status, + NULL AS start_air, NULL AS end_air, c0.poster, 'collection'::item_type AS type + FROM collections AS c0"); } protected override void Down(MigrationBuilder migrationBuilder) @@ -90,6 +109,8 @@ namespace Kyoo.Postgresql.Migrations migrationBuilder.Sql("DROP TRIGGER episode_slug_trigger ON episodes;"); // language=PostgreSQL migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP VIEW library_items;"); } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index f070a805..b0e534ed 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -91,6 +91,10 @@ namespace Kyoo.Postgresql modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); + modelBuilder.Entity() + .ToView("library_items") + .HasKey(x => x.ID); + modelBuilder.Entity() .Property(x => x.ExtraData) .HasColumnType("jsonb"); diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs index d6d3ca22..7d3c7c6a 100644 --- a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs @@ -61,6 +61,25 @@ namespace Kyoo.SqLite.Migrations END WHERE ShowID = new.ID; END;"); + + + // language=SQLite + migrationBuilder.Sql(@" + CREATE VIEW LibraryItems AS + SELECT s.ID, s.Slug, s.Title, s.Overview, s.Status, s.StartAir, s.EndAir, s.Poster, CASE + WHEN s.IsMovie THEN 1 + ELSE 0 + END AS Type + FROM Shows AS s + WHERE NOT (EXISTS ( + SELECT 1 + FROM 'Link' AS l + INNER JOIN Collections AS c ON l.FirstID = c.ID + WHERE s.ID = l.SecondID)) + UNION ALL + SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 3 AS Status, + NULL AS StartAir, NULL AS EndAir, c0.Poster, 2 AS Type + FROM collections AS c0"); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Kyoo.SqLite/SqLiteContext.cs b/Kyoo.SqLite/SqLiteContext.cs index 81b1626e..23145cf1 100644 --- a/Kyoo.SqLite/SqLiteContext.cs +++ b/Kyoo.SqLite/SqLiteContext.cs @@ -108,6 +108,10 @@ namespace Kyoo.SqLite modelBuilder.Entity() .Property(x => x.ExtraData) .HasConversion(jsonConvertor); + + modelBuilder.Entity() + .ToView("LibraryItems") + .HasKey(x => x.ID); base.OnModelCreating(modelBuilder); } diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index e7249e3a..bf7b7b41 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -30,9 +30,7 @@ namespace Kyoo.Tests ShowRepository show = new(_database, studio, people, genre, provider); SeasonRepository season = new(_database, provider); LibraryItemRepository libraryItem = new(_database, - new Lazy(() => LibraryManager.LibraryRepository), - new Lazy(() => LibraryManager.ShowRepository), - new Lazy(() => LibraryManager.CollectionRepository)); + new Lazy(() => LibraryManager.LibraryRepository)); TrackRepository track = new(_database); EpisodeRepository episode = new(_database, provider, track); diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index 703ece0b..37391c5d 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -22,15 +22,7 @@ namespace Kyoo.Controllers /// A lazy loaded library repository to validate queries (check if a library does exist) /// private readonly Lazy _libraries; - /// - /// A lazy loaded show repository to get a show from it's id. - /// - private readonly Lazy _shows; - /// - /// A lazy loaded collection repository to get a collection from it's id. - /// - private readonly Lazy _collections; - + /// protected override Expression> DefaultSort => x => x.Title; @@ -38,60 +30,41 @@ namespace Kyoo.Controllers /// /// Create a new . /// - /// The databse instance + /// The database instance /// A lazy loaded library repository - /// A lazy loaded show repository - /// A lazy loaded collection repository public LibraryItemRepository(DatabaseContext database, - Lazy libraries, - Lazy shows, - Lazy collections) + Lazy libraries) : base(database) { _database = database; _libraries = libraries; - _shows = shows; - _collections = collections; } /// - public override async Task GetOrDefault(int id) + public override Task GetOrDefault(int id) { - return id > 0 - ? new LibraryItem(await _shows.Value.GetOrDefault(id)) - : new LibraryItem(await _collections.Value.GetOrDefault(-id)); + return _database.LibraryItems.FirstOrDefaultAsync(x => x.ID == id); } /// public override Task GetOrDefault(string slug) { - throw new InvalidOperationException("You can't get a library item by a slug."); + return _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug); } - /// - /// Get a basic queryable with the right mapping from shows & collections. - /// Shows contained in a collection are excluded. - /// - private IQueryable ItemsQuery - => _database.Shows - .Where(x => !x.Collections.Any()) - .Select(LibraryItem.FromShow) - .Concat(_database.Collections - .Select(LibraryItem.FromCollection)); - /// public override Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) { - return ApplyFilters(ItemsQuery, where, sort, limit); + return ApplyFilters(_database.LibraryItems, where, sort, limit); } /// public override Task GetCount(Expression> where = null) { - IQueryable query = ItemsQuery; + IQueryable query = _database.LibraryItems; if (where != null) query = query.Where(where); return query.CountAsync(); @@ -100,7 +73,7 @@ namespace Kyoo.Controllers /// public override async Task> Search(string query) { - return await ItemsQuery + return await _database.LibraryItems .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) @@ -109,7 +82,6 @@ namespace Kyoo.Controllers /// public override Task Create(LibraryItem obj) => throw new InvalidOperationException(); - /// public override Task CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException(); /// From 27f2751bd0841142143919e4ca4225dafb63d793 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 29 Jun 2021 22:44:32 +0200 Subject: [PATCH 52/57] Adding movies episodes triggers --- .../Migrations/20210627141941_Triggers.cs | 2 ++ .../Migrations/20210626141347_Triggers.cs | 3 ++ .../Library/SpecificTests/EpisodeTest.cs | 22 ++++++++++++- .../Library/SpecificTests/LibraryItemTest.cs | 31 ++++++++++++++++++- Kyoo.Tests/Library/TestSample.cs | 15 +++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs index 5e81e797..51763bb8 100644 --- a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs @@ -38,6 +38,7 @@ namespace Kyoo.Postgresql.Migrations NEW.slug := CONCAT( (SELECT slug FROM shows WHERE id = NEW.show_id), CASE + WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number) ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number) END @@ -62,6 +63,7 @@ namespace Kyoo.Postgresql.Migrations BEGIN UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id; UPDATE episodes SET slug = CASE + WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number) ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) END diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs index 7d3c7c6a..6a70c8b1 100644 --- a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs @@ -28,6 +28,7 @@ namespace Kyoo.SqLite.Migrations UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber END @@ -41,6 +42,7 @@ namespace Kyoo.SqLite.Migrations UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber END @@ -56,6 +58,7 @@ namespace Kyoo.SqLite.Migrations UPDATE Episodes SET Slug = new.Slug || CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber END diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index 7a617fdb..6b1adf27 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -167,6 +167,26 @@ namespace Kyoo.Tests.Library Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); } - // TODO add movies tests. + [Fact] + public async Task MovieEpisodeTest() + { + Episode episode = await _repository.Create(TestSample.GetMovieEpisode()); + Assert.Equal(TestSample.Get().Slug, episode.Slug); + episode = await _repository.Get(3); + Assert.Equal(TestSample.Get().Slug, episode.Slug); + } + + [Fact] + public async Task MovieEpisodeEditTest() + { + await _repository.Create(TestSample.GetMovieEpisode()); + await Repositories.LibraryManager.Edit(new Show + { + ID = 1, + Slug = "john-wick" + }, false); + Episode episode = await _repository.Get(3); + Assert.Equal("john-wick", episode.Slug); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs b/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs index 595ca736..b2db4f66 100644 --- a/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; @@ -29,9 +30,11 @@ namespace Kyoo.Tests.Library public abstract class ALibraryItemTest { private readonly ILibraryItemRepository _repository; + private readonly RepositoryActivator _repositories; - public ALibraryItemTest(RepositoryActivator repositories) + protected ALibraryItemTest(RepositoryActivator repositories) { + _repositories = repositories; _repository = repositories.LibraryManager.LibraryItemRepository; } @@ -56,5 +59,31 @@ namespace Kyoo.Tests.Library LibraryItem actual = await _repository.Get(-1); KAssert.DeepEqual(expected, actual); } + + [Fact] + public async Task GetShowSlugTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(TestSample.Get().Slug); + KAssert.DeepEqual(expected, actual); + } + + [Fact] + public async Task GetCollectionSlugTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(TestSample.Get().Slug); + KAssert.DeepEqual(expected, actual); + } + + [Fact] + public async Task GetDuplicatedSlugTests() + { + await _repositories.LibraryManager.Create(new Collection() + { + Slug = TestSample.Get().Slug + }); + await Assert.ThrowsAsync(() => _repository.Get(TestSample.Get().Slug)); + } } } \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 2dea690a..f5651b80 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -154,5 +154,20 @@ namespace Kyoo.Tests ReleaseDate = new DateTime(2020, 06, 05) }; } + + public static Episode GetMovieEpisode() + { + return new() + { + ID = 3, + ShowSlug = "anohana", + ShowID = 1, + Path = "/home/kyoo/john-wick", + Thumb = "thumb", + Title = "John wick", + Overview = "A movie episode test", + ReleaseDate = new DateTime(1595, 05, 12) + }; + } } } \ No newline at end of file From 6bd7b47fd9a6ab34662d2816c6d686a36340d71f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 29 Jun 2021 23:51:12 +0200 Subject: [PATCH 53/57] Adding failing tests for tracks --- .../{EpisodeTest.cs => EpisodeTests.cs} | 0 .../Library/SpecificTests/TrackTests.cs | 38 +++++++++++++++++++ Kyoo.Tests/Library/TestSample.cs | 24 ++++++++++++ .../Repositories/EpisodeRepository.cs | 2 +- 4 files changed, 63 insertions(+), 1 deletion(-) rename Kyoo.Tests/Library/SpecificTests/{EpisodeTest.cs => EpisodeTests.cs} (100%) create mode 100644 Kyoo.Tests/Library/SpecificTests/TrackTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs rename to Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/TrackTests.cs b/Kyoo.Tests/Library/SpecificTests/TrackTests.cs new file mode 100644 index 00000000..6506e9d4 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/TrackTests.cs @@ -0,0 +1,38 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class TrackTests : ATrackTests + { + public TrackTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class TrackTests : ATrackTests + { + public TrackTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ATrackTests : RepositoryTests + { + private readonly ITrackRepository _repository; + + protected ATrackTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = repositories.LibraryManager.TrackRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index f5651b80..04490604 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -88,6 +88,24 @@ namespace Kyoo.Tests ReleaseDate = new DateTime(2020, 06, 05) } }, + { + typeof(Track), + () => new Track + { + ID = 1, + EpisodeID = 1, + Codec = "subrip", + Language = "eng", + Path = "/path", + Title = "Subtitle track", + Type = StreamType.Subtitle, + EpisodeSlug = Get().Slug, + IsDefault = true, + IsExternal = false, + IsForced = false, + TrackIndex = 1 + } + }, { typeof(People), () => new People @@ -133,6 +151,12 @@ namespace Kyoo.Tests episode.SeasonID = 0; episode.Season = season; context.Episodes.Add(episode); + + Track track = Get(); + track.ID = 0; + track.EpisodeID = 0; + track.Episode = episode; + context.Tracks.Add(track); context.SaveChanges(); } diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 2ffa3f1d..39356960 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -127,7 +127,7 @@ namespace Kyoo.Controllers if (changed.Tracks != null || resetOld) { - await _tracks.DeleteAll(x => x.EpisodeID == resource.ID); + await Database.Entry(resource).Collection(x => x.Tracks).LoadAsync(); resource.Tracks = changed.Tracks; await ValidateTracks(resource); } From dc42ed031fefbf966a41d069dfdb82aec9675dfa Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 13 Jul 2021 15:18:25 +0200 Subject: [PATCH 54/57] Handling tracks slugs --- .github/workflows/analysis.yml | 2 +- .github/workflows/tests.yml | 4 +- Kyoo.Common/Controllers/ILibraryManager.cs | 19 --- Kyoo.Common/Controllers/IRepository.cs | 20 +-- .../Implementations/LibraryManager.cs | 12 -- Kyoo.Common/Models/Resources/Track.cs | 68 +++++---- Kyoo.CommonAPI/DatabaseContext.cs | 48 ------- .../Migrations/20210627141941_Triggers.cs | 92 ++++++++++-- .../Migrations/20210626141347_Triggers.cs | 133 ++++++++++++++---- Kyoo.Tests/KAssert.cs | 1 - .../Library/SpecificTests/TrackTests.cs | 13 ++ .../Repositories/EpisodeRepository.cs | 10 +- .../Repositories/TrackRepository.cs | 67 +-------- Kyoo/Startup.cs | 1 - Kyoo/Views/SubtitleApi.cs | 17 +-- 15 files changed, 258 insertions(+), 249 deletions(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index ea7b58e3..1924ffdf 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -35,7 +35,7 @@ jobs: check-name: tests repo-token: ${{secrets.GITHUB_TOKEN}} running-workflow-name: analysis - allowed-conclusions: success,skipped,cancelled,neutral,failed + allowed-conclusions: success,skipped,cancelled,neutral,failure - name: Download coverage report uses: dawidd6/action-download-artifact@v2 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bd010b83..60cafa6a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,9 +29,11 @@ jobs: POSTGRES_USERNAME: postgres POSTGRES_PASSWORD: postgres - name: Sanitize coverage output + if: ${{ always() }} run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml - name: Upload coverage report + if: ${{ always() }} uses: actions/upload-artifact@v2 with: name: coverage.xml - path: "**/coverage.opencover.xml" \ No newline at end of file + path: "**/coverage.opencover.xml" diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 53c7061a..490f8073 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -149,16 +149,6 @@ namespace Kyoo.Controllers [ItemNotNull] Task Get(string showSlug, int seasonNumber, int episodeNumber); - /// - /// Get a track from it's slug and it's type. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// If the item is not found - /// The track found - [ItemNotNull] - Task Get(string slug, StreamType type = StreamType.Unknown); - /// /// Get the resource by it's ID or null if it is not found. /// @@ -224,15 +214,6 @@ namespace Kyoo.Controllers [ItemCanBeNull] Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); - /// - /// Get a track from it's slug and it's type or null if it is not found. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// The track found - [ItemCanBeNull] - Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); - /// /// Load a related resource diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 352ebe04..f7be69ad 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -375,25 +375,7 @@ namespace Kyoo.Controllers /// /// A repository to handle tracks /// - public interface ITrackRepository : IRepository - { - /// - /// Get a track from it's slug and it's type. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// If the item is not found - /// The track found - Task Get(string slug, StreamType type = StreamType.Unknown); - - /// - /// Get a track from it's slug and it's type or null if it is not found. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// The track found - Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); - } + public interface ITrackRepository : IRepository { } /// /// A repository to handle libraries. diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index 66581157..bc30e756 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -114,12 +114,6 @@ namespace Kyoo.Controllers return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber); } - /// - public Task Get(string slug, StreamType type = StreamType.Unknown) - { - return TrackRepository.Get(slug, type); - } - /// public async Task GetOrDefault(int id) where T : class, IResource @@ -165,12 +159,6 @@ namespace Kyoo.Controllers return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber); } - /// - public async Task GetOrDefault(string slug, StreamType type = StreamType.Unknown) - { - return await TrackRepository.GetOrDefault(slug, type); - } - /// public Task Load(T obj, Expression> member) where T : class, IResource diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 00ae745c..f093699b 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -33,42 +33,27 @@ namespace Kyoo.Models { get { - string type = Type switch - { - StreamType.Subtitle => "", - StreamType.Video => "video.", - StreamType.Audio => "audio.", - StreamType.Attachment => "font.", - _ => "" - }; + string type = Type.ToString().ToLower(); string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; - string codec = Codec switch - { - "subrip" => ".srt", - {} x => $".{x}" - }; - return $"{EpisodeSlug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}"; + string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString(); + return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}"; } [UsedImplicitly] private set { - Match match = Regex.Match(value, @"(?.*)-s(?\d+)e(?\d+)" - + @"(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..\w)?"); + if (value == null) + throw new ArgumentNullException(nameof(value)); + Match match = Regex.Match(value, + @"(?[^\.]+)\.(?\w{0,3})(-(?\d+))?(\.(?forced))?\.(?\w+)(\.\w*)?"); if (!match.Success) - { - match = Regex.Match(value, @"(?.*)\.(?.{0,3})(?-forced)?(\..\w)?"); - if (!match.Success) - throw new ArgumentException("Invalid track slug. " + - "Format: {episodeSlug}.{language}[-forced][.{extension}]"); - } + throw new ArgumentException("Invalid track slug. " + + "Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]"); - EpisodeSlug = Episode.GetSlug(match.Groups["show"].Value, - match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : null, - match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : null); - Language = match.Groups["language"].Value; + EpisodeSlug = match.Groups["ep"].Value; + Language = match.Groups["lang"].Value; + TrackIndex = int.Parse(match.Groups["index"].Value); IsForced = match.Groups["forced"].Success; - if (match.Groups["type"].Success) - Type = Enum.Parse(match.Groups["type"].Value, true); + Type = Enum.Parse(match.Groups["type"].Value, true); } } @@ -167,5 +152,32 @@ namespace Kyoo.Models _ => mkvLanguage }; } + + /// + /// Utility method to edit a track slug (this only return a slug with the modification, nothing is stored) + /// + /// The slug to edit + /// The new type of this + /// + /// + /// + /// + public static string EditSlug(string baseSlug, + StreamType type = StreamType.Unknown, + string language = null, + int? index = null, + bool? forced = null) + { + Track track = new() {Slug = baseSlug}; + if (type != StreamType.Unknown) + track.Type = type; + if (language != null) + track.Language = language; + if (index != null) + track.TrackIndex = index.Value; + if (forced != null) + track.IsForced = forced.Value; + return track.Slug; + } } } diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 5bf9e87a..2213d807 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -330,8 +330,6 @@ namespace Kyoo modelBuilder.Entity() .Property(x => x.Slug) .ValueGeneratedOnAddOrUpdate(); - - // modelBuilder.Ignore(); } /// @@ -502,52 +500,6 @@ namespace Kyoo } } - /// - /// 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. /// diff --git a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs index 51763bb8..16569748 100644 --- a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs +++ b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs @@ -13,8 +13,8 @@ namespace Kyoo.Postgresql.Migrations LANGUAGE PLPGSQL AS $$ BEGIN - NEW.slug := CONCAT( - (SELECT slug FROM shows WHERE id = NEW.show_id), + NEW.slug := CONCAT( + (SELECT slug FROM shows WHERE id = NEW.show_id), '-s', NEW.season_number ); @@ -38,10 +38,10 @@ namespace Kyoo.Postgresql.Migrations NEW.slug := CONCAT( (SELECT slug FROM shows WHERE id = NEW.show_id), CASE - WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL - WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number) - ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number) - END + WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL + WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number) + ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number) + END ); RETURN NEW; END @@ -63,21 +63,81 @@ namespace Kyoo.Postgresql.Migrations BEGIN UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id; UPDATE episodes SET slug = CASE - WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug - WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number) - ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) - END - WHERE show_id = NEW.id; + WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug + WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number) + ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) + END WHERE show_id = NEW.id; RETURN NEW; END $$;"); - // language=PostgreSQL migrationBuilder.Sql(@" CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows FOR EACH ROW EXECUTE PROCEDURE show_slug_update();"); + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION episode_update_tracks_slug() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE tracks SET slug = CONCAT( + NEW.slug, + '.', language, + CASE (track_index) + WHEN 0 THEN '' + ELSE CONCAT('-', track_index) + END, + CASE (is_forced) + WHEN false THEN '' + ELSE '-forced' + END, + '.', type + ) WHERE episode_id = NEW.id; + RETURN NEW; + END; + $$;"); + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER episode_track_slug_trigger AFTER UPDATE OF slug ON episodes + FOR EACH ROW EXECUTE PROCEDURE episode_update_tracks_slug();"); + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION track_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + IF NEW.track_index = 0 THEN + NEW.track_index := (SELECT COUNT(*) FROM tracks + WHERE episode_id = NEW.episode_id AND type = NEW.type + AND language = NEW.language AND is_forced = NEW.is_forced); + END IF; + NEW.slug := CONCAT( + (SELECT slug FROM episodes WHERE id = NEW.episode_id), + '.', NEW.language, + CASE (NEW.track_index) + WHEN 0 THEN '' + ELSE CONCAT('-', NEW.track_index) + END, + CASE (NEW.is_forced) + WHEN false THEN '' + ELSE '-forced' + END, + '.', NEW.type + ); + RETURN NEW; + END + $$;"); + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER track_slug_trigger + BEFORE INSERT OR UPDATE OF episode_id, is_forced, language, track_index, type ON tracks + FOR EACH ROW EXECUTE PROCEDURE track_slug_update();"); + + // language=PostgreSQL migrationBuilder.Sql(@" CREATE VIEW library_items AS @@ -112,6 +172,14 @@ namespace Kyoo.Postgresql.Migrations // language=PostgreSQL migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;"); // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER track_slug_trigger ON tracks;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION track_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER episode_track_slug_trigger ON episodes;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION episode_update_tracks_slug;"); + // language=PostgreSQL migrationBuilder.Sql(@"DROP VIEW library_items;"); } } diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs index 6a70c8b1..f3ae8325 100644 --- a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs @@ -25,13 +25,13 @@ namespace Kyoo.SqLite.Migrations migrationBuilder.Sql(@" CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW BEGIN - UPDATE Episodes - SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || - CASE - WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' - WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber - ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber - END + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END WHERE ID == new.ID; END"); // language=SQLite @@ -39,29 +39,114 @@ namespace Kyoo.SqLite.Migrations CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID ON Episodes FOR EACH ROW BEGIN - UPDATE Episodes - SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || - CASE - WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' - WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber - ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber - END + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END WHERE ID == new.ID; END"); - - + + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER TrackSlugInsert + AFTER INSERT ON Tracks + FOR EACH ROW + BEGIN + UPDATE Tracks SET TrackIndex = ( + SELECT COUNT(*) FROM Tracks + WHERE EpisodeID = new.EpisodeID AND Type = new.Type + AND Language = new.Language AND IsForced = new.IsForced + ) WHERE ID = new.ID AND TrackIndex = 0; + UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) || + '.' || Language || + CASE (TrackIndex) + WHEN 0 THEN '' + ELSE '-' || (TrackIndex) + END || + CASE (IsForced) + WHEN false THEN '' + ELSE '-forced' + END || + CASE (Type) + WHEN 1 THEN '.video' + WHEN 2 THEN '.audio' + WHEN 3 THEN '.subtitle' + ELSE '.' || Type + END + WHERE ID = new.ID; + END;"); + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER TrackSlugUpdate + AFTER UPDATE OF EpisodeID, IsForced, Language, TrackIndex, Type ON Tracks + FOR EACH ROW + BEGIN + UPDATE Tracks SET TrackIndex = ( + SELECT COUNT(*) FROM Tracks + WHERE EpisodeID = new.EpisodeID AND Type = new.Type + AND Language = new.Language AND IsForced = new.IsForced + ) WHERE ID = new.ID AND TrackIndex = 0; + UPDATE Tracks SET Slug = + (SELECT Slug FROM Episodes WHERE ID = EpisodeID) || + '.' || Language || + CASE (TrackIndex) + WHEN 0 THEN '' + ELSE '-' || (TrackIndex) + END || + CASE (IsForced) + WHEN false THEN '' + ELSE '-forced' + END || + CASE (Type) + WHEN 1 THEN '.video' + WHEN 2 THEN '.audio' + WHEN 3 THEN '.subtitle' + ELSE '.' || Type + END + WHERE ID = new.ID; + END;"); + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeUpdateTracksSlug + AFTER UPDATE OF Slug ON Episodes + FOR EACH ROW + BEGIN + UPDATE Tracks SET Slug = + NEW.Slug || + '.' || Language || + CASE (TrackIndex) + WHEN 0 THEN '' + ELSE '-' || TrackIndex + END || + CASE (IsForced) + WHEN false THEN '' + ELSE '-forced' + END || + CASE (Type) + WHEN 1 THEN '.video' + WHEN 2 THEN '.audio' + WHEN 3 THEN '.subtitle' + ELSE '.' || Type + END + WHERE EpisodeID = NEW.ID; + END;"); + + // language=SQLite migrationBuilder.Sql(@" CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW BEGIN - UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; - UPDATE Episodes - SET Slug = new.Slug || - CASE - WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' - WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber - ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber - END + UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; + UPDATE Episodes + SET Slug = new.Slug || + CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END WHERE ShowID = new.ID; END;"); diff --git a/Kyoo.Tests/KAssert.cs b/Kyoo.Tests/KAssert.cs index fa38160d..80209c98 100644 --- a/Kyoo.Tests/KAssert.cs +++ b/Kyoo.Tests/KAssert.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Reflection; using JetBrains.Annotations; using Xunit; diff --git a/Kyoo.Tests/Library/SpecificTests/TrackTests.cs b/Kyoo.Tests/Library/SpecificTests/TrackTests.cs index 6506e9d4..3c2e2043 100644 --- a/Kyoo.Tests/Library/SpecificTests/TrackTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/TrackTests.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; using Xunit; @@ -34,5 +35,17 @@ namespace Kyoo.Tests.Library { _repository = repositories.LibraryManager.TrackRepository; } + + [Fact] + public async Task SlugEditTest() + { + await Repositories.LibraryManager.ShowRepository.Edit(new Show + { + ID = 1, + Slug = "new-slug" + }, false); + Track track = await _repository.Get(1); + Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug); + } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 39356960..2cf7ff1f 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -127,7 +127,7 @@ namespace Kyoo.Controllers if (changed.Tracks != null || resetOld) { - await Database.Entry(resource).Collection(x => x.Tracks).LoadAsync(); + await _tracks.DeleteAll(x => x.EpisodeID == resource.ID); resource.Tracks = changed.Tracks; await ValidateTracks(resource); } @@ -148,14 +148,10 @@ namespace Kyoo.Controllers /// The parameter is returned. private async Task ValidateTracks(Episode resource) { - resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.MapAsync((x, i) => + resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.SelectAsync(x => { x.Episode = resource; - // TODO use a trigger for the next line. - x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language - && x.IsForced == y.IsForced - && x.Codec == y.Codec - && x.Type == y.Type); + x.EpisodeSlug = resource.Slug; return _tracks.Create(x); }).ToListAsync()); return resource; diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 98b05f30..9b642a93 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers @@ -33,56 +30,6 @@ namespace Kyoo.Controllers { _database = database; } - - - /// - Task IRepository.Get(string slug) - { - return Get(slug); - } - - /// - public async Task Get(string slug, StreamType type = StreamType.Unknown) - { - Track ret = await GetOrDefault(slug, type); - if (ret == null) - throw new ItemNotFoundException($"No track found with the slug {slug} and the type {type}."); - return ret; - } - - /// - public Task GetOrDefault(string slug, StreamType type = StreamType.Unknown) - { - Match match = Regex.Match(slug, - @"(?.*)-s(?\d+)e(?\d+)(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..*)?"); - - if (!match.Success) - { - if (int.TryParse(slug, out int id)) - return GetOrDefault(id); - match = Regex.Match(slug, @"(?.*)\.(?.{0,3})(?-forced)?(\..*)?"); - if (!match.Success) - throw new ArgumentException("Invalid track slug. " + - "Format: {episodeSlug}.{language}[-forced][.{extension}]"); - } - - string showSlug = match.Groups["show"].Value; - int? seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : null; - int? episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : null; - string language = match.Groups["language"].Value; - bool forced = match.Groups["forced"].Success; - if (match.Groups["type"].Success) - type = Enum.Parse(match.Groups["type"].Value, true); - - IQueryable query = _database.Tracks.Where(x => x.Episode.Show.Slug == showSlug - && x.Episode.SeasonNumber == seasonNumber - && x.Episode.EpisodeNumber == episodeNumber - && x.Language == language - && x.IsForced == forced); - if (type != StreamType.Unknown) - return query.FirstOrDefaultAsync(x => x.Type == type); - return query.FirstOrDefaultAsync(); - } /// public override Task> Search(string query) @@ -93,23 +40,19 @@ namespace Kyoo.Controllers /// public override async Task Create(Track obj) { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + if (obj.EpisodeID <= 0) { obj.EpisodeID = obj.Episode?.ID ?? 0; if (obj.EpisodeID <= 0) throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID})."); } - + await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - await _database.SaveOrRetry(obj, (x, i) => - { - if (i > 10) - throw new DuplicatedItemException($"More than 10 same tracks exists {x.Slug}. Aborting..."); - x.TrackIndex++; - return x; - }); + await _database.SaveChangesAsync(); return obj; } diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 983d1d6c..4d5995ef 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -5,7 +5,6 @@ using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Options; using Kyoo.Postgresql; -using Kyoo.SqLite; using Kyoo.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 5e680a73..7f05eedf 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -1,5 +1,4 @@ -using System; -using Kyoo.Models; +using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.IO; @@ -27,19 +26,9 @@ namespace Kyoo.Api [Permission(nameof(SubtitleApi), Kind.Read)] public async Task GetSubtitle(string slug, string extension) { - Track subtitle; - try - { - subtitle = await _libraryManager.GetOrDefault(slug, StreamType.Subtitle); - } - catch (ArgumentException ex) - { - return BadRequest(new {error = ex.Message}); - } - - if (subtitle is not {Type: StreamType.Subtitle}) + Track subtitle = await _libraryManager.GetOrDefault(Track.EditSlug(slug, StreamType.Subtitle)); + if (subtitle == null) return NotFound(); - if (subtitle.Codec == "subrip" && extension == "vtt") return new ConvertSubripToVtt(subtitle.Path, _files); return _files.FileResult(subtitle.Path); From 123898dc988980a134990561a35f9eb1e5b07dc3 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 13 Jul 2021 16:14:42 +0200 Subject: [PATCH 55/57] Adding generic tests for every resources --- Kyoo.Common/Controllers/ILibraryManager.cs | 5 ++ .../Implementations/LibraryManager.cs | 3 + Kyoo.CommonAPI/DatabaseContext.cs | 5 ++ .../Migrations/20210627141933_Initial.cs | 2 +- .../Migrations/20210626141337_Initial.cs | 2 +- Kyoo.Tests/Library/RepositoryActivator.cs | 4 +- .../Library/SpecificTests/CollectionsTests.cs | 37 +++++++++ .../Library/SpecificTests/GenreTests.cs | 37 +++++++++ .../Library/SpecificTests/LibraryTests.cs | 36 +++++++++ .../Library/SpecificTests/PeopleTests.cs | 37 +++++++++ .../Library/SpecificTests/ProviderTests.cs | 37 +++++++++ .../Library/SpecificTests/StudioTests.cs | 37 +++++++++ Kyoo.Tests/Library/SpecificTests/UserTests.cs | 37 +++++++++ Kyoo.Tests/Library/TestSample.cs | 79 +++++++++++++++++++ .../Repositories/LibraryRepository.cs | 9 ++- 15 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs create mode 100644 Kyoo.Tests/Library/SpecificTests/GenreTests.cs create mode 100644 Kyoo.Tests/Library/SpecificTests/LibraryTests.cs create mode 100644 Kyoo.Tests/Library/SpecificTests/PeopleTests.cs create mode 100644 Kyoo.Tests/Library/SpecificTests/ProviderTests.cs create mode 100644 Kyoo.Tests/Library/SpecificTests/StudioTests.cs create mode 100644 Kyoo.Tests/Library/SpecificTests/UserTests.cs diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 490f8073..938d3029 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -77,6 +77,11 @@ namespace Kyoo.Controllers /// IProviderRepository ProviderRepository { get; } + /// + /// The repository that handle users. + /// + IUserRepository UserRepository { get; } + /// /// Get the resource by it's ID /// diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index bc30e756..12ea0b2a 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -37,6 +37,8 @@ namespace Kyoo.Controllers public IGenreRepository GenreRepository { get; } /// public IProviderRepository ProviderRepository { get; } + /// + public IUserRepository UserRepository { get; } /// @@ -58,6 +60,7 @@ namespace Kyoo.Controllers StudioRepository = GetRepository() as IStudioRepository; GenreRepository = GetRepository() as IGenreRepository; ProviderRepository = GetRepository() as IProviderRepository; + UserRepository = GetRepository() as IUserRepository; } /// diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 2213d807..36cdc47b 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -157,6 +157,11 @@ namespace Kyoo .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) diff --git a/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs b/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs index 50bed8f7..29b51490 100644 --- a/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs @@ -226,7 +226,7 @@ namespace Kyoo.Postgresql.Migrations column: x => x.studio_id, principalTable: "studios", principalColumn: "id", - onDelete: ReferentialAction.Restrict); + onDelete: ReferentialAction.SetNull); }); migrationBuilder.CreateTable( diff --git a/Kyoo.SqLite/Migrations/20210626141337_Initial.cs b/Kyoo.SqLite/Migrations/20210626141337_Initial.cs index 87d98348..88823571 100644 --- a/Kyoo.SqLite/Migrations/20210626141337_Initial.cs +++ b/Kyoo.SqLite/Migrations/20210626141337_Initial.cs @@ -218,7 +218,7 @@ namespace Kyoo.SqLite.Migrations column: x => x.StudioID, principalTable: "Studios", principalColumn: "ID", - onDelete: ReferentialAction.Restrict); + onDelete: ReferentialAction.SetNull); }); migrationBuilder.CreateTable( diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs index bf7b7b41..ee6aa4df 100644 --- a/Kyoo.Tests/Library/RepositoryActivator.cs +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -33,6 +33,7 @@ namespace Kyoo.Tests new Lazy(() => LibraryManager.LibraryRepository)); TrackRepository track = new(_database); EpisodeRepository episode = new(_database, provider, track); + UserRepository user = new(_database); LibraryManager = new LibraryManager(new IBaseRepository[] { provider, @@ -45,7 +46,8 @@ namespace Kyoo.Tests track, people, studio, - genre + genre, + user }); } diff --git a/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs b/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs new file mode 100644 index 00000000..7a5976de --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class CollectionTests : ACollectionTests + { + public CollectionTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class CollectionTests : ACollectionTests + { + public CollectionTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ACollectionTests : RepositoryTests + { + private readonly ICollectionRepository _repository; + + protected ACollectionTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.CollectionRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/GenreTests.cs b/Kyoo.Tests/Library/SpecificTests/GenreTests.cs new file mode 100644 index 00000000..d79dba5e --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/GenreTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class GenreTests : AGenreTests + { + public GenreTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class GenreTests : AGenreTests + { + public GenreTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AGenreTests : RepositoryTests + { + private readonly IGenreRepository _repository; + + protected AGenreTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.GenreRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs b/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs new file mode 100644 index 00000000..fbed1793 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs @@ -0,0 +1,36 @@ +using Kyoo.Controllers; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class LibraryTests : ALibraryTests + { + public LibraryTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class LibraryTests : ALibraryTests + { + public LibraryTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ALibraryTests : RepositoryTests + { + private readonly ILibraryRepository _repository; + + protected ALibraryTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.LibraryRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs b/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs new file mode 100644 index 00000000..fc8b788d --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class PeopleTests : APeopleTests + { + public PeopleTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class PeopleTests : APeopleTests + { + public PeopleTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class APeopleTests : RepositoryTests + { + private readonly IPeopleRepository _repository; + + protected APeopleTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.PeopleRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs b/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs new file mode 100644 index 00000000..853e34a1 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class ProviderTests : AProviderTests + { + public ProviderTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class ProviderTests : AProviderTests + { + public ProviderTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AProviderTests : RepositoryTests + { + private readonly IProviderRepository _repository; + + protected AProviderTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.ProviderRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/StudioTests.cs b/Kyoo.Tests/Library/SpecificTests/StudioTests.cs new file mode 100644 index 00000000..f5093b19 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/StudioTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class StudioTests : AStudioTests + { + public StudioTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class StudioTests : AStudioTests + { + public StudioTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AStudioTests : RepositoryTests + { + private readonly IStudioRepository _repository; + + protected AStudioTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.StudioRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/UserTests.cs b/Kyoo.Tests/Library/SpecificTests/UserTests.cs new file mode 100644 index 00000000..be67296d --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/UserTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class UserTests : AUserTests + { + public UserTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class UserTests : AUserTests + { + public UserTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AUserTests : RepositoryTests + { + private readonly IUserRepository _repository; + + protected AUserTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.UserRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs index 04490604..adbe7d84 100644 --- a/Kyoo.Tests/Library/TestSample.cs +++ b/Kyoo.Tests/Library/TestSample.cs @@ -17,6 +17,16 @@ namespace Kyoo.Tests private static readonly Dictionary> Samples = new() { + { + typeof(Models.Library), + () => new Models.Library + { + ID = 1, + Slug = "deck", + Name = "Deck", + Paths = new[] {"/path/to/deck"} + } + }, { typeof(Collection), () => new Collection @@ -115,6 +125,47 @@ namespace Kyoo.Tests Name = "The Actor", Poster = "NicePoster" } + }, + { + typeof(Studio), + () => new Studio + { + ID = 1, + Slug = "hyper-studio", + Name = "Hyper studio" + } + }, + { + typeof(Genre), + () => new Genre + { + ID = 1, + Slug = "action", + Name = "Action" + } + }, + { + typeof(Provider), + () => new Provider + { + ID = 1, + Slug = "tvdb", + Name = "The TVDB", + Logo = "path/tvdb.svg", + LogoExtension = "svg" + } + }, + { + typeof(User), + () => new User + { + ID = 1, + Slug = "user", + Username = "User", + Email = "user@im-a-user.com", + Password = "MD5-encoded", + Permissions = new [] {"overall.read"} + } } }; @@ -157,7 +208,35 @@ namespace Kyoo.Tests track.EpisodeID = 0; track.Episode = episode; context.Tracks.Add(track); + + Studio studio = Get(); + studio.ID = 0; + studio.Shows = new List {show}; + context.Studios.Add(studio); + + Genre genre = Get(); + genre.ID = 0; + genre.Shows = new List {show}; + context.Genres.Add(genre); + + People people = Get(); + people.ID = 0; + context.People.Add(people); + Provider provider = Get(); + provider.ID = 0; + context.Providers.Add(provider); + + Models.Library library = Get(); + library.ID = 0; + library.Collections = new List {collection}; + library.Providers = new List {provider}; + context.Libraries.Add(library); + + User user = Get(); + user.ID = 0; + context.Users.Add(user); + context.SaveChanges(); } diff --git a/Kyoo/Controllers/Repositories/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index d569f6fe..02194b77 100644 --- a/Kyoo/Controllers/Repositories/LibraryRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryRepository.cs @@ -63,9 +63,12 @@ namespace Kyoo.Controllers protected override async Task Validate(Library resource) { await base.Validate(resource); - resource.Providers = await resource.Providers - .SelectAsync(x => _providers.CreateIfNotExists(x)) - .ToListAsync(); + await resource.ProviderLinks.ForEachAsync(async id => + { + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; + }); } /// From 31da4bb69248b7d11f988c8ef93238375d902211 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 13 Jul 2021 17:43:17 +0200 Subject: [PATCH 56/57] Fixing test coverage for the common --- .github/workflows/tests.yml | 4 +++- Kyoo.CommonAPI/Kyoo.CommonAPI.csproj | 6 +++--- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 2 +- Kyoo.SqLite/Kyoo.SqLite.csproj | 4 ++-- Kyoo.Tests/Kyoo.Tests.csproj | 1 + Kyoo/Kyoo.csproj | 6 +++--- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60cafa6a..796d5b8e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,9 @@ jobs: - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' + run: | + dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' -p:CopyLocalLockFileAssemblies=true + cp ./Kyoo.Common/bin/Debug/net5.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll ./Kyoo.Tests/bin/Debug/net5.0/ - name: Test run: dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover' env: diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index 14d40203..451c387d 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 096b1bcc..408e10ad 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Kyoo.SqLite/Kyoo.SqLite.csproj b/Kyoo.SqLite/Kyoo.SqLite.csproj index 74665497..357b5ae0 100644 --- a/Kyoo.SqLite/Kyoo.SqLite.csproj +++ b/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -19,11 +19,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index 93873bc7..d198dae6 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -15,6 +15,7 @@ all + diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 1fc38b98..438f3e9b 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -35,9 +35,9 @@ - - - + + + From 8255c2f800ed0a7629679c107156194a1aeb78b0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 13 Jul 2021 17:47:14 +0200 Subject: [PATCH 57/57] Runing analysis only once for PR --- .github/workflows/analysis.yml | 1 + .github/workflows/tests.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 1924ffdf..fc1679cb 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -5,6 +5,7 @@ jobs: analysis: name: Static Analysis runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 796d5b8e..65a3646f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,7 @@ on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository container: mcr.microsoft.com/dotnet/sdk:5.0 services: postgres: