diff --git a/back/Kyoo.ruleset b/back/Kyoo.ruleset index 13d29e23..9a9c6d14 100644 --- a/back/Kyoo.ruleset +++ b/back/Kyoo.ruleset @@ -33,10 +33,11 @@ + + - diff --git a/back/src/Directory.Build.props b/back/src/Directory.Build.props index c0c382c2..90c33385 100644 --- a/back/src/Directory.Build.props +++ b/back/src/Directory.Build.props @@ -46,7 +46,7 @@ $(MSBuildThisFileDirectory)../Kyoo.ruleset - 1591 + 1591;1305;8618 diff --git a/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs b/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs index 792e1978..fbe8d1ab 100644 --- a/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs @@ -20,7 +20,6 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Threading.Tasks; -using JetBrains.Annotations; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; @@ -40,11 +39,6 @@ namespace Kyoo.Abstractions.Controllers IRepository GetRepository() where T : class, IResource; - /// - /// The repository that handle libraries. - /// - ILibraryRepository LibraryRepository { get; } - /// /// The repository that handle libraries items (a wrapper around shows and collections). /// @@ -55,6 +49,11 @@ namespace Kyoo.Abstractions.Controllers /// ICollectionRepository CollectionRepository { get; } + /// + /// The repository that handle shows. + /// + IMovieRepository MovieRepository { get; } + /// /// The repository that handle shows. /// @@ -80,16 +79,6 @@ namespace Kyoo.Abstractions.Controllers /// IStudioRepository StudioRepository { get; } - /// - /// The repository that handle genres. - /// - IGenreRepository GenreRepository { get; } - - /// - /// The repository that handle providers. - /// - IProviderRepository ProviderRepository { get; } - /// /// The repository that handle users. /// @@ -102,7 +91,6 @@ namespace Kyoo.Abstractions.Controllers /// The type of the resource /// If the item is not found /// The resource found - [ItemNotNull] Task Get(int id) where T : class, IResource; @@ -113,7 +101,6 @@ namespace Kyoo.Abstractions.Controllers /// The type of the resource /// If the item is not found /// The resource found - [ItemNotNull] Task Get(string slug) where T : class, IResource; @@ -124,7 +111,6 @@ namespace Kyoo.Abstractions.Controllers /// The type of the resource /// If the item is not found /// The first resource found that match the where function - [ItemNotNull] Task Get(Expression> where) where T : class, IResource; @@ -135,7 +121,6 @@ namespace Kyoo.Abstractions.Controllers /// The season's number /// If the item is not found /// The season found - [ItemNotNull] Task Get(int showID, int seasonNumber); /// @@ -145,7 +130,6 @@ namespace Kyoo.Abstractions.Controllers /// The season's number /// If the item is not found /// The season found - [ItemNotNull] Task Get(string showSlug, int seasonNumber); /// @@ -156,7 +140,6 @@ namespace Kyoo.Abstractions.Controllers /// The episode's number /// If the item is not found /// The episode found - [ItemNotNull] Task Get(int showID, int seasonNumber, int episodeNumber); /// @@ -167,7 +150,6 @@ namespace Kyoo.Abstractions.Controllers /// The episode's number /// If the item is not found /// The episode found - [ItemNotNull] Task Get(string showSlug, int seasonNumber, int episodeNumber); /// @@ -176,8 +158,7 @@ namespace Kyoo.Abstractions.Controllers /// The id of the resource /// The type of the resource /// The resource found - [ItemCanBeNull] - Task GetOrDefault(int id) + Task GetOrDefault(int id) where T : class, IResource; /// @@ -186,8 +167,7 @@ namespace Kyoo.Abstractions.Controllers /// The slug of the resource /// The type of the resource /// The resource found - [ItemCanBeNull] - Task GetOrDefault(string slug) + Task GetOrDefault(string slug) where T : class, IResource; /// @@ -197,8 +177,7 @@ namespace Kyoo.Abstractions.Controllers /// A custom sort method to handle cases where multiples items match the filters. /// The type of the resource /// The first resource found that match the where function - [ItemCanBeNull] - Task GetOrDefault(Expression> where, Sort sortBy = default) + Task GetOrDefault(Expression> where, Sort? sortBy = default) where T : class, IResource; /// @@ -207,8 +186,7 @@ namespace Kyoo.Abstractions.Controllers /// The id of the show /// The season's number /// The season found - [ItemCanBeNull] - Task GetOrDefault(int showID, int seasonNumber); + Task GetOrDefault(int showID, int seasonNumber); /// /// Get a season from it's show slug and it's seasonNumber or null if it is not found. @@ -216,8 +194,7 @@ namespace Kyoo.Abstractions.Controllers /// The slug of the show /// The season's number /// The season found - [ItemCanBeNull] - Task GetOrDefault(string showSlug, int seasonNumber); + Task GetOrDefault(string showSlug, int seasonNumber); /// /// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found. @@ -226,8 +203,7 @@ namespace Kyoo.Abstractions.Controllers /// The season's number /// The episode's number /// The episode found - [ItemCanBeNull] - Task GetOrDefault(int showID, int seasonNumber, int episodeNumber); + Task GetOrDefault(int showID, int seasonNumber, int episodeNumber); /// /// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found. @@ -236,8 +212,7 @@ namespace Kyoo.Abstractions.Controllers /// The season's number /// The episode's number /// The episode found - [ItemCanBeNull] - Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); + Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); /// /// Load a related resource @@ -253,7 +228,7 @@ namespace Kyoo.Abstractions.Controllers /// /// /// - Task Load([NotNull] T obj, Expression> member, bool force = false) + Task Load(T obj, Expression> member, bool force = false) where T : class, IResource where T2 : class, IResource; @@ -271,7 +246,7 @@ namespace Kyoo.Abstractions.Controllers /// /// /// - Task Load([NotNull] T obj, Expression>> member, bool force = false) + Task Load(T obj, Expression>> member, bool force = false) where T : class, IResource where T2 : class; @@ -288,7 +263,7 @@ namespace Kyoo.Abstractions.Controllers /// /// /// - Task Load([NotNull] T obj, string memberName, bool force = false) + Task Load(T obj, string memberName, bool force = false) where T : class, IResource; /// @@ -303,35 +278,7 @@ namespace Kyoo.Abstractions.Controllers /// /// /// A representing the asynchronous operation. - Task Load([NotNull] IResource obj, string memberName, bool force = false); - - /// - /// Get items (A wrapper around shows or collections) from a library. - /// - /// The ID of the library - /// A filter function - /// Sort information (sort order and sort by) - /// How many items to return and where to start - /// No library exist with the given ID. - /// A list of items that match every filters - Task> GetItemsFromLibrary(int id, - Expression> where = null, - Sort sort = default, - Pagination limit = default); - - /// - /// Get items (A wrapper around shows or collections) from a library. - /// - /// The slug of the library - /// A filter function - /// Sort information (sort order and sort by) - /// How many items to return and where to start - /// No library exist with the given slug. - /// A list of items that match every filters - Task> GetItemsFromLibrary(string slug, - Expression> where = null, - Sort sort = default, - Pagination limit = default); + Task Load(IResource obj, string memberName, bool force = false); /// /// Get people's roles from a show. @@ -343,9 +290,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given ID. /// A list of items that match every filters Task> GetPeopleFromShow(int showID, - Expression> where = null, - Sort sort = default, - Pagination limit = default); + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get people's roles from a show. @@ -357,9 +304,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given slug. /// A list of items that match every filters Task> GetPeopleFromShow(string showSlug, - Expression> where = null, - Sort sort = default, - Pagination limit = default); + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get people's roles from a person. @@ -371,9 +318,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given ID. /// A list of items that match every filters Task> GetRolesFromPeople(int id, - Expression> where = null, - Sort sort = default, - Pagination limit = default); + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get people's roles from a person. @@ -385,27 +332,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given slug. /// A list of items that match every filters Task> GetRolesFromPeople(string slug, - Expression> where = null, - Sort sort = default, - Pagination limit = default); - - /// - /// Setup relations between a show, a library and a collection - /// - /// The show's ID to setup relations with - /// The library's ID to setup relations with (optional) - /// The collection's ID to setup relations with (optional) - /// A representing the asynchronous operation. - Task AddShowLink(int showID, int? libraryID, int? collectionID); - - /// - /// Setup relations between a show, a library and a collection - /// - /// The show to setup relations with - /// The library to setup relations with (optional) - /// The collection to setup relations with (optional) - /// A representing the asynchronous operation. - Task AddShowLink([NotNull] Show show, Library library, Collection collection); + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get all resources with filters @@ -415,9 +344,9 @@ namespace Kyoo.Abstractions.Controllers /// How many items to return and where to start /// The type of resources to load /// A list of resources that match every filters - Task> GetAll(Expression> where = null, - Sort sort = default, - Pagination limit = default) + Task> GetAll(Expression>? where = null, + Sort? sort = default, + Pagination? limit = default) where T : class, IResource; /// @@ -426,7 +355,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// The type of resources to load /// A list of resources that match every filters - Task GetCount(Expression> where = null) + Task GetCount(Expression>? where = null) where T : class, IResource; /// @@ -444,7 +373,7 @@ namespace Kyoo.Abstractions.Controllers /// The item to register /// The type of resource /// The resource registers and completed by database's information (related items and so on) - Task Create([NotNull] T item) + Task Create(T item) where T : class, IResource; /// @@ -453,18 +382,31 @@ namespace Kyoo.Abstractions.Controllers /// The item to register /// The type of resource /// The newly created item or the existing value if it existed. - Task CreateIfNotExists([NotNull] T item) + Task CreateIfNotExists(T item) where T : class, IResource; /// /// Edit a resource /// /// The resource to edit, it's ID can't change. - /// Should old properties of the resource be discarded or should null values considered as not changed? /// The type of resources /// If the item is not found /// The resource edited and completed by database's information (related items and so on) - Task Edit(T item, bool resetOld) + Task Edit(T item) + where T : class, IResource; + + /// + /// Edit only specific properties of a resource + /// + /// The id of the resource to edit + /// + /// A method that will be called when you need to update every properties that you want to + /// persist. It can return false to abort the process via an ArgumentException + /// + /// The type of resources + /// If the item is not found + /// The resource edited and completed by database's information (related items and so on) + Task Patch(int id, Func> patch) where T : class, IResource; /// diff --git a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs index a45827f0..fb00b768 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs @@ -20,7 +20,6 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Threading.Tasks; -using JetBrains.Annotations; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; @@ -45,7 +44,6 @@ namespace Kyoo.Abstractions.Controllers /// The id of the resource /// If the item could not be found. /// The resource found - [ItemNotNull] Task Get(int id); /// @@ -54,7 +52,6 @@ namespace Kyoo.Abstractions.Controllers /// The slug of the resource /// If the item could not be found. /// The resource found - [ItemNotNull] Task Get(string slug); /// @@ -63,7 +60,6 @@ namespace Kyoo.Abstractions.Controllers /// A predicate to filter the resource. /// If the item could not be found. /// The resource found - [ItemNotNull] Task Get(Expression> where); /// @@ -71,16 +67,14 @@ namespace Kyoo.Abstractions.Controllers /// /// The id of the resource /// The resource found - [ItemCanBeNull] - Task GetOrDefault(int id); + Task GetOrDefault(int id); /// /// Get a resource from it's slug or null if it is not found. /// /// The slug of the resource /// The resource found - [ItemCanBeNull] - Task GetOrDefault(string slug); + Task GetOrDefault(string slug); /// /// Get the first resource that match the predicate or null if it is not found. @@ -88,15 +82,13 @@ namespace Kyoo.Abstractions.Controllers /// A predicate to filter the resource. /// A custom sort method to handle cases where multiples items match the filters. /// The resource found - [ItemCanBeNull] - Task GetOrDefault(Expression> where, Sort sortBy = default); + Task GetOrDefault(Expression> where, Sort? sortBy = default); /// /// Search for resources. /// /// The query string. /// A list of resources found - [ItemNotNull] Task> Search(string query); /// @@ -106,33 +98,30 @@ namespace Kyoo.Abstractions.Controllers /// Sort information about the query (sort by, sort order) /// How pagination should be done (where to start and how many to return) /// A list of resources that match every filters - [ItemNotNull] - Task> GetAll(Expression> where = null, - Sort sort = default, - Pagination limit = default); + Task> GetAll(Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get the number of resources that match the filter's predicate. /// /// A filter predicate /// How many resources matched that filter - Task GetCount(Expression> where = null); + Task GetCount(Expression>? where = null); /// /// Create a new resource. /// /// The item to register /// The resource registers and completed by database's information (related items and so on) - [ItemNotNull] - Task Create([NotNull] T obj); + Task Create(T obj); /// /// Create a new resource if it does not exist already. If it does, the existing value is returned instead. /// /// The object to create /// The newly created item or the existing value if it existed. - [ItemNotNull] - Task CreateIfNotExists([NotNull] T obj); + Task CreateIfNotExists(T obj); /// /// Called when a resource has been created. @@ -140,14 +129,24 @@ namespace Kyoo.Abstractions.Controllers event ResourceEventHandler OnCreated; /// - /// Edit a resource + /// Edit a resource and replace every property /// /// The resource to edit, it's ID can't change. - /// Should old properties of the resource be discarded or should null values considered as not changed? /// If the item is not found /// The resource edited and completed by database's information (related items and so on) - [ItemNotNull] - Task Edit([NotNull] T edited, bool resetOld); + Task Edit(T edited); + + /// + /// Edit only specific properties of a resource + /// + /// The id of the resource to edit + /// + /// A method that will be called when you need to update every properties that you want to + /// persist. It can return false to abort the process via an ArgumentException + /// + /// If the item is not found + /// The resource edited and completed by database's information (related items and so on) + Task Patch(int id, Func> patch); /// /// Called when a resource has been edited. @@ -176,14 +175,14 @@ namespace Kyoo.Abstractions.Controllers /// The resource to delete /// If the item is not found /// A representing the asynchronous operation. - Task Delete([NotNull] T obj); + Task Delete(T obj); /// /// Delete all resources that match the predicate. /// /// A predicate to filter resources to delete. Every resource that match this will be deleted. /// A representing the asynchronous operation. - Task DeleteAll([NotNull] Expression> where); + Task DeleteAll(Expression> where); /// /// Called when a resource has been edited. @@ -202,21 +201,16 @@ namespace Kyoo.Abstractions.Controllers Type RepositoryType { get; } } + /// + /// A repository to handle shows. + /// + public interface IMovieRepository : IRepository { } + /// /// A repository to handle shows. /// public interface IShowRepository : IRepository { - /// - /// Link a show to a collection and/or a library. The given show is now part of those containers. - /// If both a library and a collection are given, the collection is added to the library too. - /// - /// The ID of the show - /// The ID of the library (optional) - /// The ID of the collection (optional) - /// A representing the asynchronous operation. - Task AddShowLink(int showID, int? libraryID, int? collectionID); - /// /// Get a show's slug from it's ID. /// @@ -330,55 +324,16 @@ namespace Kyoo.Abstractions.Controllers Task GetAbsolute(string showSlug, int absoluteNumber); } - /// - /// A repository to handle libraries. - /// - public interface ILibraryRepository : IRepository { } - /// /// A repository to handle library items (A wrapper around shows and collections). /// - public interface ILibraryItemRepository : IRepository - { - /// - /// Get items (A wrapper around shows or collections) from a library. - /// - /// The ID of the library - /// A filter function - /// Sort information (sort order and sort by) - /// How many items to return and where to start - /// No library exist with the given ID. - /// A list of items that match every filters - public Task> GetFromLibrary(int id, - Expression> where = null, - Sort sort = default, - Pagination limit = default); - - /// - /// Get items (A wrapper around shows or collections) from a library. - /// - /// The slug of the library - /// A filter function - /// Sort information (sort order and sort by) - /// How many items to return and where to start - /// No library exist with the given slug. - /// A list of items that match every filters - public Task> GetFromLibrary(string slug, - Expression> where = null, - Sort sort = default, - Pagination limit = default); - } + public interface ILibraryItemRepository : IRepository { } /// /// A repository for collections /// public interface ICollectionRepository : IRepository { } - /// - /// A repository for genres. - /// - public interface IGenreRepository : IRepository { } - /// /// A repository for studios. /// @@ -399,9 +354,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given ID. /// A list of items that match every filters Task> GetFromShow(int showID, - Expression> where = null, - Sort sort = default, - Pagination limit = default); + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get people's roles from a show. @@ -413,9 +368,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given slug. /// A list of items that match every filters Task> GetFromShow(string showSlug, - Expression> where = null, - Sort sort = default, - Pagination limit = default); + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get people's roles from a person. @@ -427,9 +382,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given ID. /// A list of items that match every filters Task> GetFromPeople(int id, - Expression> where = null, - Sort sort = default, - Pagination limit = default); + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); /// /// Get people's roles from a person. @@ -441,28 +396,9 @@ namespace Kyoo.Abstractions.Controllers /// No exist with the given slug. /// A list of items that match every filters Task> GetFromPeople(string slug, - Expression> where = null, - Sort sort = default, - Pagination limit = default); - } - - /// - /// A repository to handle providers. - /// - public interface IProviderRepository : IRepository - { - /// - /// Get a list of external ids that match all filters - /// - /// A predicate to add arbitrary filter - /// Sort information (sort order and 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) - where T : class, IMetadata; + Expression>? where = null, + Sort? sort = default, + Pagination? limit = default); } /// diff --git a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs index 69ffe0bc..8ef426c5 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs @@ -35,22 +35,20 @@ namespace Kyoo.Abstractions.Controllers /// /// The item to cache images. /// - /// - /// true if images should be downloaded even if they already exists locally, false otherwise. - /// /// The type of the item /// true if an image has been downloaded, false otherwise. - Task DownloadImages(T item, bool alwaysDownload = false) + Task DownloadImages(T item) where T : IThumbnails; /// /// Retrieve the local path of an image of the given item. /// /// The item to retrieve the poster from. - /// The ID of the image. See for values. + /// The ID of the image. + /// The quality of the image /// The type of the item /// The path of the image for the given resource or null if it does not exists. - string? GetImagePath(T item, int imageId) + string GetImagePath(T item, string image, ImageQuality quality) where T : IThumbnails; } } diff --git a/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs b/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs index 8e381387..279ec516 100644 --- a/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs +++ b/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs @@ -83,6 +83,7 @@ namespace Kyoo.Abstractions.Controllers /// A dependency that this action will use. /// A new public static StartupAction New(Action action, int priority) + where T : notnull => new(action, priority); /// @@ -94,6 +95,8 @@ namespace Kyoo.Abstractions.Controllers /// A second dependency that this action will use. /// A new public static StartupAction New(Action action, int priority) + where T : notnull + where T2 : notnull => new(action, priority); /// @@ -106,6 +109,9 @@ namespace Kyoo.Abstractions.Controllers /// A third dependency that this action will use. /// A new public static StartupAction New(Action action, int priority) + where T : notnull + where T2 : notnull + where T3 : notnull => new(action, priority); /// @@ -144,6 +150,7 @@ namespace Kyoo.Abstractions.Controllers /// /// The dependency to use. public class StartupAction : IStartupAction + where T : notnull { /// /// The action to execute at startup. @@ -177,6 +184,8 @@ namespace Kyoo.Abstractions.Controllers /// The dependency to use. /// The second dependency to use. public class StartupAction : IStartupAction + where T : notnull + where T2 : notnull { /// /// The action to execute at startup. @@ -214,6 +223,9 @@ namespace Kyoo.Abstractions.Controllers /// The second dependency to use. /// The third dependency to use. public class StartupAction : IStartupAction + where T : notnull + where T2 : notnull + where T3 : notnull { /// /// The action to execute at startup. diff --git a/back/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj b/back/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj index 9d8ddb0c..3433f9f1 100644 --- a/back/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj +++ b/back/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj @@ -3,6 +3,7 @@ Kyoo.Abstractions Base package to create plugins for Kyoo. Kyoo.Abstractions + enable diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs index cd946714..785c8b55 100644 --- a/back/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs +++ b/back/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs @@ -17,7 +17,6 @@ // along with Kyoo. If not, see . using System; -using JetBrains.Annotations; namespace Kyoo.Abstractions.Models.Attributes { @@ -32,23 +31,21 @@ namespace Kyoo.Abstractions.Models.Attributes /// /// The public name of this api. /// - [NotNull] public string Name { get; } + public string Name { get; } /// /// The name of the group in witch this API is. You can also specify a custom sort order using the following /// format: order:name. Everything before the first : will be removed but kept for /// th alphabetical ordering. /// - public string Group { get; set; } + public string? Group { get; set; } /// /// Create a new . /// /// The name of the api that will be used on the documentation page. - public ApiDefinitionAttribute([NotNull] string name) + public ApiDefinitionAttribute(string name) { - if (name == null) - throw new ArgumentNullException(nameof(name)); Name = name; } } diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs index 5bfb9e82..0a0a9672 100644 --- a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs +++ b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs @@ -30,7 +30,7 @@ namespace Kyoo.Abstractions.Models.Attributes /// /// The name of the field containing the related resource's ID. /// - public string RelationID { get; } + public string? RelationID { get; } /// /// Create a new . diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs index bac1edec..80bf585b 100644 --- a/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs +++ b/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs @@ -32,17 +32,17 @@ namespace Kyoo.Abstractions.Models.Permissions /// /// The needed permission type. /// - public string Type { get; } + public string? Type { get; } /// /// The needed permission kind. /// - public Kind Kind { get; } + public Kind? Kind { get; } /// /// The group of this permission. /// - public Group Group { get; set; } + public Group? Group { get; set; } /// /// Ask a permission to run an action. diff --git a/back/src/Kyoo.Abstractions/Models/ConfigurationReference.cs b/back/src/Kyoo.Abstractions/Models/ConfigurationReference.cs deleted file mode 100644 index 635bbb97..00000000 --- a/back/src/Kyoo.Abstractions/Models/ConfigurationReference.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Reflection; -using JetBrains.Annotations; -using Kyoo.Utils; - -namespace Kyoo.Abstractions.Models -{ - /// - /// A class given information about a strongly typed configuration. - /// - public class ConfigurationReference - { - /// - /// The path of the resource (separated by ':') - /// - public string Path { get; } - - /// - /// The type of the resource. - /// - public Type Type { get; } - - /// - /// Create a new using a given path and type. - /// This method does not create sub configuration resources. Please see - /// - /// The path of the resource (separated by ':' or "__") - /// The type of the resource - /// - public ConfigurationReference(string path, Type type) - { - Path = path; - Type = type; - } - - /// - /// Return the list of configuration reference a type has. - /// - /// - /// The base path of the type (separated by ':' or "__". If empty, it will start at root) - /// - /// The type of the object - /// The list of configuration reference a type has. - public static IEnumerable CreateReference(string path, [NotNull] Type type) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - List ret = new() - { - new ConfigurationReference(path, type) - }; - - if (!type.IsClass || type.AssemblyQualifiedName?.StartsWith("System") == true) - return ret; - - Type enumerable = Utility.GetGenericDefinition(type, typeof(IEnumerable<>)); - Type dictionary = Utility.GetGenericDefinition(type, typeof(IDictionary<,>)); - Type dictionaryKey = dictionary?.GetGenericArguments()[0]; - - if (dictionary != null && dictionaryKey == typeof(string)) - ret.AddRange(CreateReference($"{path}:{type.Name}:*", dictionary.GetGenericArguments()[1])); - else if (dictionary != null && dictionaryKey == typeof(int)) - ret.AddRange(CreateReference($"{path}:{type.Name}:", dictionary.GetGenericArguments()[1])); - else if (enumerable != null) - ret.AddRange(CreateReference($"{path}:{type.Name}:", enumerable.GetGenericArguments()[0])); - else - { - foreach (PropertyInfo child in type.GetProperties()) - ret.AddRange(CreateReference($"{path}:{child.Name}", child.PropertyType)); - } - - return ret; - } - - /// - /// Return the list of configuration reference a type has. - /// - /// - /// The base path of the type (separated by ':' or "__". If empty, it will start at root) - /// - /// The type of the object - /// The list of configuration reference a type has. - public static IEnumerable CreateReference(string path) - { - return CreateReference(path, typeof(T)); - } - - /// - /// Return a meaning that the given path is of any type. - /// It means that the type can't be edited. - /// - /// - /// The path that will be untyped (separated by ':' or "__". If empty, it will start at root). - /// - /// A configuration reference representing a path of any type. - public static ConfigurationReference CreateUntyped(string path) - { - return new ConfigurationReference(path, null); - } - } -} diff --git a/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs b/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs index 0cd21975..f0aa4c1c 100644 --- a/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs +++ b/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs @@ -30,13 +30,13 @@ namespace Kyoo.Abstractions.Models.Exceptions /// /// The existing object. /// - public object Existing { get; } + public object? Existing { get; } /// /// Create a new with the default message. /// /// The existing object. - public DuplicatedItemException(object existing = null) + public DuplicatedItemException(object? existing = null) : base("Already exists in the database.") { Existing = existing; diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/ILink.cs b/back/src/Kyoo.Abstractions/Models/Genre.cs similarity index 75% rename from back/src/Kyoo.Abstractions/Models/Resources/Interfaces/ILink.cs rename to back/src/Kyoo.Abstractions/Models/Genre.cs index 5c146786..2b7e16f0 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/ILink.cs +++ b/back/src/Kyoo.Abstractions/Models/Genre.cs @@ -19,13 +19,27 @@ namespace Kyoo.Abstractions.Models { /// - /// An interface to represent resources that should have a link field in their return values (like videos). + /// A genre that allow one to specify categories for shows. /// - public interface ILink + public enum Genre { - /// - /// The link to return, in most cases this should be a string. - /// - public object Link { get; } + Action, + Adventure, + Animation, + Comedy, + Crime, + Documentary, + Drama, + Family, + Fantasy, + History, + Horror, + Music, + Mystery, + Romance, + ScienceFiction, + Thriller, + War, + Western, } } diff --git a/back/src/Kyoo.Abstractions/Models/LibraryItem.cs b/back/src/Kyoo.Abstractions/Models/LibraryItem.cs index fd9a5b85..6190bf6f 100644 --- a/back/src/Kyoo.Abstractions/Models/LibraryItem.cs +++ b/back/src/Kyoo.Abstractions/Models/LibraryItem.cs @@ -18,15 +18,16 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Linq.Expressions; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Kyoo.Utils; namespace Kyoo.Abstractions.Models { /// /// The type of item, ether a show, a movie or a collection. /// - public enum ItemType + public enum ItemKind { /// /// The is a . @@ -34,8 +35,7 @@ namespace Kyoo.Abstractions.Models Show, /// - /// The is a Movie (a with - /// equals to true). + /// The is a Movie. /// Movie, @@ -45,128 +45,135 @@ namespace Kyoo.Abstractions.Models Collection } - /// - /// A type union between and . - /// This is used to list content put inside a library. - /// - public class LibraryItem : CustomTypeDescriptor, IResource, IThumbnails + public class LibraryItem : IResource, ILibraryItem, IThumbnails, IMetadata { /// - public int ID { get; set; } + public int Id { get; set; } /// + [MaxLength(256)] public string Slug { get; set; } /// - /// The title of the show or collection. + /// The title of this show. /// - public string Title { get; set; } + public string Name { get; set; } /// - /// The summary of the show or collection. + /// A catchphrase for this movie. /// - public string Overview { get; set; } + public string? Tagline { get; set; } /// - /// Is this show airing, not aired yet or finished? This is only applicable for shows. + /// The list of alternative titles of this show. /// - public Status? Status { get; set; } + public string[] Aliases { get; set; } = Array.Empty(); /// - /// The date this show or collection started airing. It can be null if this is unknown. + /// The path of the movie video file. + /// + public string? Path { get; set; } + + /// + /// The summary of this show. + /// + public string? Overview { get; set; } + + /// + /// A list of tags that match this movie. + /// + public string[] Tags { get; set; } = Array.Empty(); + + /// + /// The list of genres (themes) this show has. + /// + public Genre[] Genres { get; set; } = Array.Empty(); + + /// + /// Is this show airing, not aired yet or finished? + /// + public Status Status { 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 or collection finished airing. - /// It must be after the but can be the same (example: for movies). + /// The date this show finished airing. /// It can also be null if this is unknown. /// public DateTime? EndAir { get; set; } + /// + /// The date this movie aired. + /// + public DateTime? AirDate { get; set; } + /// - public Dictionary Images { get; set; } + public Image? Poster { get; set; } + + /// + public Image? Thumbnail { get; set; } + + /// + public Image? Logo { get; set; } /// - /// The type of this item (ether a collection, a show or a movie). + /// A video of a few minutes that tease the content. /// - public ItemType Type { get; set; } + public string? Trailer { get; set; } + + /// + public ItemKind Kind { get; set; } + + /// + public Dictionary ExternalId { get; set; } = new(); /// - /// Create a new, empty . + /// Links to watch this movie. /// + public VideoLinks? Links => Kind == ItemKind.Movie ? new() + { + Direct = $"/video/movie/{Slug}/direct", + Hls = $"/video/movie/{Slug}/master.m3u8", + } + : null; + public LibraryItem() { } - /// - /// Create a from a show. - /// - /// The show that this library item should represent. - public LibraryItem(Show show) + [JsonConstructor] + public LibraryItem(string name) { - ID = show.ID; - Slug = show.Slug; - Title = show.Title; - Overview = show.Overview; - Status = show.Status; - StartAir = show.StartAir; - EndAir = show.EndAir; - Images = show.Images; - 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; - Slug = collection.Slug; - Title = collection.Name; - Overview = collection.Overview; - Status = Models.Status.Unknown; - StartAir = null; - EndAir = null; - Images = collection.Images; - Type = ItemType.Collection; - } - - /// - /// An expression to create a representing a show. - /// - public static Expression> FromShow => x => new LibraryItem - { - ID = x.ID, - Slug = x.Slug, - Title = x.Title, - Overview = x.Overview, - Status = x.Status, - StartAir = x.StartAir, - EndAir = x.EndAir, - Images = x.Images, - 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, - Slug = x.Slug, - Title = x.Name, - Overview = x.Overview, - Status = Models.Status.Unknown, - StartAir = null, - EndAir = null, - Images = x.Images, - Type = ItemType.Collection - }; - - /// - public override string GetClassName() - { - return Type.ToString(); + Slug = Utility.ToSlug(name); + Name = name; } } + + /// + /// A type union between and . + /// This is used to list content put inside a library. + /// + public interface ILibraryItem : IResource + { + /// + /// Is the item a collection, a movie or a show? + /// + public ItemKind Kind { get; } + + /// + /// The title of this show. + /// + public string Name { get; } + + /// + /// The summary of this show. + /// + public string? Overview { get; } + + /// + /// The date this movie aired. + /// + public DateTime? AirDate { get; } + } } diff --git a/back/src/Kyoo.Abstractions/Models/MetadataID.cs b/back/src/Kyoo.Abstractions/Models/MetadataID.cs index dbf00a3e..f16c59ae 100644 --- a/back/src/Kyoo.Abstractions/Models/MetadataID.cs +++ b/back/src/Kyoo.Abstractions/Models/MetadataID.cs @@ -16,48 +16,21 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; -using System.Linq.Expressions; -using Kyoo.Abstractions.Models.Attributes; - namespace Kyoo.Abstractions.Models { /// /// ID and link of an item on an external provider. /// - public class MetadataID + public class MetadataId { - /// - /// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs. - /// - public static Expression> PrimaryKey - { - get { return x => new { First = x.ResourceID, Second = x.ProviderID }; } - } - - /// - /// The ID of the resource which possess the metadata. - /// - [SerializeIgnore] public int ResourceID { get; set; } - - /// - /// The ID of the provider. - /// - [SerializeIgnore] public int ProviderID { get; set; } - - /// - /// The provider that can do something with this ID. - /// - public Provider Provider { get; set; } - /// /// The ID of the resource on the external provider. /// - public string DataID { get; set; } + public string DataId { get; set; } /// /// The URL of the resource on the external provider. /// - public string Link { get; set; } + public string? Link { get; set; } } } diff --git a/back/src/Kyoo.Abstractions/Models/Page.cs b/back/src/Kyoo.Abstractions/Models/Page.cs index 6c09e54a..55fc9a5e 100644 --- a/back/src/Kyoo.Abstractions/Models/Page.cs +++ b/back/src/Kyoo.Abstractions/Models/Page.cs @@ -93,14 +93,14 @@ namespace Kyoo.Abstractions.Models if (items.Count > 0 && query.ContainsKey("afterID")) { - query["afterID"] = items.First().ID.ToString(); + query["afterID"] = items.First().Id.ToString(); query["reverse"] = "true"; Previous = url + query.ToQueryString(); } query.Remove("reverse"); if (items.Count == limit && limit > 0) { - query["afterID"] = items.Last().ID.ToString(); + query["afterID"] = items.Last().Id.ToString(); Next = url + query.ToQueryString(); } diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/ProviderTests.cs b/back/src/Kyoo.Abstractions/Models/PartialResource.cs similarity index 50% rename from back/tests/Kyoo.Tests/Database/SpecificTests/ProviderTests.cs rename to back/src/Kyoo.Abstractions/Models/PartialResource.cs index 49340367..7e9b0089 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/ProviderTests.cs +++ b/back/src/Kyoo.Abstractions/Models/PartialResource.cs @@ -16,33 +16,11 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System.Diagnostics.CodeAnalysis; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Xunit; -using Xunit.Abstractions; +namespace Kyoo.Models; -namespace Kyoo.Tests.Database +public class PartialResource { - namespace PostgreSQL - { - [Collection(nameof(Postgresql))] - public class ProviderTests : AProviderTests - { - public ProviderTests(PostgresFixture postgres, ITestOutputHelper output) - : base(new RepositoryActivator(output, postgres)) { } - } - } + public int? Id { get; set; } - public abstract class AProviderTests : RepositoryTests - { - [SuppressMessage("ReSharper", "NotAccessedField.Local")] - private readonly IProviderRepository _repository; - - protected AProviderTests(RepositoryActivator repositories) - : base(repositories) - { - _repository = Repositories.LibraryManager.ProviderRepository; - } - } + public string? Slug { get; set; } } diff --git a/back/src/Kyoo.Abstractions/Models/PeopleRole.cs b/back/src/Kyoo.Abstractions/Models/PeopleRole.cs index 3a416927..c6ac4bf0 100644 --- a/back/src/Kyoo.Abstractions/Models/PeopleRole.cs +++ b/back/src/Kyoo.Abstractions/Models/PeopleRole.cs @@ -29,10 +29,10 @@ namespace Kyoo.Abstractions.Models public class PeopleRole : IResource { /// - public int ID { get; set; } + public int Id { get; set; } /// - public string Slug => ForPeople ? Show.Slug : People.Slug; + public string Slug => ForPeople ? Show!.Slug : People.Slug; /// /// Should this role be used as a Show substitute (the value is true) or @@ -53,12 +53,16 @@ namespace Kyoo.Abstractions.Models /// /// The ID of the Show where the People playing in. /// - public int ShowID { get; set; } + public int? ShowID { get; set; } /// /// The show where the People played in. /// - public Show Show { get; set; } + public Show? Show { get; set; } + + public int? MovieID { get; set; } + + public Movie? Movie { get; set; } /// /// The type of work the person has done for the show. diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs index 079c5eef..63a04eef 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs @@ -17,46 +17,63 @@ // along with Kyoo. If not, see . using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Utils; +using Newtonsoft.Json; namespace Kyoo.Abstractions.Models { /// /// A class representing collections of . - /// A collection can also be stored in a . /// public class Collection : IResource, IMetadata, IThumbnails { /// - public int ID { get; set; } + public int Id { get; set; } /// - public string Slug { get; set; } + [MaxLength(256)] public string Slug { get; set; } /// /// The name of this collection. /// public string Name { get; set; } - /// - public Dictionary Images { get; set; } - /// /// The description of this collection. /// - public string Overview { get; set; } + public string? Overview { get; set; } + + /// + public Image? Poster { get; set; } + + /// + public Image? Thumbnail { get; set; } + + /// + public Image? Logo { get; set; } + + /// + /// The list of movies contained in this collection. + /// + [LoadableRelation] public ICollection? Movies { 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; } + [LoadableRelation] public ICollection? Shows { get; set; } /// - [EditableRelation][LoadableRelation] public ICollection ExternalIDs { get; set; } + public Dictionary ExternalId { get; set; } = new(); + + public Collection() { } + + [JsonConstructor] + public Collection(string name) + { + Slug = Utility.ToSlug(name); + Name = name; + } } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs index f6dcb41e..d5a49da7 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using JetBrains.Annotations; using Kyoo.Abstractions.Controllers; @@ -31,28 +32,23 @@ namespace Kyoo.Abstractions.Models public class Episode : IResource, IMetadata, IThumbnails { /// - public int ID { get; set; } + public int Id { get; set; } /// [Computed] + [MaxLength(256)] public string Slug { get { if (ShowSlug != null || Show?.Slug != null) - return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); - return ShowID != 0 - ? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber) - : null; + return GetSlug(ShowSlug ?? Show!.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber); } [UsedImplicitly] - [NotNull] private set { - if (value == null) - throw new ArgumentNullException(nameof(value)); - Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); if (match.Success) @@ -80,22 +76,22 @@ namespace Kyoo.Abstractions.Models /// /// 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; } + [SerializeIgnore] public string? ShowSlug { private get; set; } /// /// The ID of the Show containing this episode. /// - [SerializeIgnore] public int ShowID { get; set; } + [SerializeIgnore] public int ShowId { 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; } + [LoadableRelation(nameof(ShowId))] public Show? Show { get; set; } /// /// The ID of the Season containing this episode. /// - [SerializeIgnore] public int? SeasonID { get; set; } + [SerializeIgnore] public int? SeasonId { get; set; } /// /// The season that contains this episode. @@ -105,7 +101,7 @@ namespace Kyoo.Abstractions.Models /// 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; } + [LoadableRelation(nameof(SeasonId))] public Season? Season { get; set; } /// /// The season in witch this episode is in. @@ -127,18 +123,15 @@ namespace Kyoo.Abstractions.Models /// public string Path { get; set; } - /// - public Dictionary Images { get; set; } - /// /// The title of this episode. /// - public string Title { get; set; } + public string? Name { get; set; } /// /// The overview of this episode. /// - public string Overview { get; set; } + public string? Overview { get; set; } /// /// The release date of this episode. It can be null if unknown. @@ -146,7 +139,35 @@ namespace Kyoo.Abstractions.Models public DateTime? ReleaseDate { get; set; } /// - [EditableRelation][LoadableRelation] public ICollection ExternalIDs { get; set; } + public Image? Poster { get; set; } + + /// + public Image? Thumbnail { get; set; } + + /// + public Image? Logo { get; set; } + + /// + public Dictionary ExternalId { get; set; } = new(); + + /// + /// The previous episode that should be seen before viewing this one. + /// + [LoadableRelation] public Episode? PreviousEpisode { get; set; } + + /// + /// The next episode to watch after this one. + /// + [LoadableRelation] public Episode? NextEpisode { get; set; } + + /// + /// Links to watch this episode. + /// + public VideoLinks Links => new() + { + Direct = $"/video/episode/{Slug}/direct", + Hls = $"/video/episode/{Slug}/master.m3u8", + }; /// /// Get the slug of an episode. @@ -165,14 +186,11 @@ namespace Kyoo.Abstractions.Models /// 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, + public static string GetSlug(string showSlug, int? seasonNumber, int? episodeNumber, int? absoluteNumber = null) { - if (showSlug == null) - throw new ArgumentNullException(nameof(showSlug)); return seasonNumber switch { null when absoluteNumber == null => showSlug, diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Genre.cs b/back/src/Kyoo.Abstractions/Models/Resources/Genre.cs deleted file mode 100644 index 5bf8ceee..00000000 --- a/back/src/Kyoo.Abstractions/Models/Resources/Genre.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System.Collections.Generic; -using Kyoo.Abstractions.Models.Attributes; -using Kyoo.Utils; - -namespace Kyoo.Abstractions.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; } - - /// - /// The list of shows that have this genre. - /// - [LoadableRelation] public ICollection Shows { get; set; } - - /// - /// 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; - } - } -} diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IMetadata.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IMetadata.cs index 44f072fa..9cfb2595 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IMetadata.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IMetadata.cs @@ -16,11 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models { @@ -30,69 +26,8 @@ namespace Kyoo.Abstractions.Models public interface IMetadata { /// - /// 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; } - } - - /// - /// A static class containing extensions method for every class. - /// This allow one to use metadata more easily. - /// - public static class MetadataExtension - { - /// - /// Retrieve the internal provider's ID of an item using it's provider slug. - /// - /// - /// This method will never return anything if the are not loaded. - /// - /// An instance of to retrieve the ID from. - /// The slug of the provider - /// The field of the asked provider. - [CanBeNull] - public static string GetID(this IMetadata self, string provider) - { - return self.ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID; - } - - /// - /// Retrieve the internal provider's ID of an item using it's provider slug. - /// If the ID could be found, it is converted to the type and true is returned. - /// - /// - /// This method will never succeed if the are not loaded. - /// - /// An instance of to retrieve the ID from. - /// The slug of the provider - /// - /// The field of the asked provider parsed - /// and converted to the type. - /// It is only relevant if this method returns true. - /// - /// The type to convert the to. - /// true if this method succeeded, false otherwise. - public static bool TryGetID(this IMetadata self, string provider, out T id) - { - string dataID = self.ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID; - if (dataID == null) - { - id = default; - return false; - } - - try - { - id = (T)Convert.ChangeType(dataID, typeof(T)); - } - catch - { - id = default; - return false; - } - return true; - } + public Dictionary ExternalId { get; set; } } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs index 509006e5..925e38f5 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Controllers; namespace Kyoo.Abstractions.Models @@ -32,7 +33,7 @@ namespace Kyoo.Abstractions.Models /// 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; } + public int Id { get; set; } /// /// A human-readable identifier that can be used instead of an ID. @@ -42,6 +43,7 @@ namespace Kyoo.Abstractions.Models /// There is no setter for a slug since it can be computed from other fields. /// For example, a season slug is {ShowSlug}-s{SeasonNumber}. /// + [MaxLength(256)] public string Slug { get; } } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs index 071ddc35..72a6d455 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -16,8 +16,11 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System.Collections.Generic; -using Kyoo.Abstractions.Controllers; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models { @@ -27,51 +30,104 @@ namespace Kyoo.Abstractions.Models public interface IThumbnails { /// - /// The list of images mapped to a certain index. + /// A poster is a 2/3 format image with the cover of the resource. /// - /// - /// An arbitrary index should not be used, instead use indexes from - /// - /// {"0": "example.com/dune/poster"} - public Dictionary Images { get; set; } - } - - /// - /// A class containing constant values for images. To be used as index of a . - /// - public static class Images - { - /// - /// A poster is a 9/16 format image with the cover of the resource. - /// - public const int Poster = 0; + public Image? Poster { get; set; } /// /// A thumbnail is a 16/9 format image, it could ether be used as a background or as a preview but it usually /// is not an official image. /// - public const int Thumbnail = 1; + public Image? Thumbnail { get; set; } /// /// A logo is a small image representing the resource. /// - public const int Logo = 2; + public Image? Logo { get; set; } + } + + [TypeConverter(typeof(ImageConvertor))] + public class Image + { + /// + /// The original image from another server. + /// + public string Source { get; set; } /// - /// A video of a few minutes that tease the content. + /// A hash to display as placeholder while the image is loading. /// - public const int Trailer = 3; + [MaxLength(32)] + public string Blurhash { get; set; } + + [SerializeIgnore] + public string Path { private get; set; } /// - /// Retrieve the name of an image using it's ID. It is also used by the serializer to retrieve all named images. - /// If a plugin adds a new image type, it should add it's value and name here to allow the serializer to add it. + /// The url to retrieve the low quality image. /// - public static Dictionary ImageName { get; } = new() + public string Low => $"{Path}?quality=low"; + + /// + /// The url to retrieve the medium quality image. + /// + public string Medium => $"{Path}?quality=medium"; + + /// + /// The url to retrieve the high quality image. + /// + public string High => $"{Path}?quality=high"; + + public Image(string source, string? blurhash = null) { - [Poster] = nameof(Poster), - [Thumbnail] = nameof(Thumbnail), - [Logo] = nameof(Logo), - [Trailer] = nameof(Trailer) - }; + Source = source; + Blurhash = blurhash ?? "00000000000000"; + } + + public class ImageConvertor : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + if (sourceType == typeof(string)) + return true; + return base.CanConvertFrom(context, sourceType); + } + + /// + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is not string source) + return base.ConvertFrom(context, culture, value)!; + return new Image(source); + } + + /// + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return false; + } + } + } + + /// + /// The quality of an image + /// + public enum ImageQuality + { + /// + /// Small + /// + Low, + + /// + /// Medium + /// + Medium, + + /// + /// Large + /// + High, } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Library.cs b/back/src/Kyoo.Abstractions/Models/Resources/Library.cs deleted file mode 100644 index e56bb352..00000000 --- a/back/src/Kyoo.Abstractions/Models/Resources/Library.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System.Collections.Generic; -using Kyoo.Abstractions.Models.Attributes; - -namespace Kyoo.Abstractions.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; } - - /// - /// The list of used for items in this library. - /// - [EditableRelation][LoadableRelation] public ICollection Providers { 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; } - } -} diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs new file mode 100644 index 00000000..850bc3fc --- /dev/null +++ b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs @@ -0,0 +1,152 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Utils; +using Newtonsoft.Json; + +namespace Kyoo.Abstractions.Models +{ + /// + /// A series or a movie. + /// + public class Movie : IResource, IMetadata, IOnMerge, IThumbnails + { + /// + public int Id { get; set; } + + /// + [MaxLength(256)] + public string Slug { get; set; } + + /// + /// The title of this show. + /// + public string Name { get; set; } + + /// + /// A catchphrase for this movie. + /// + public string? Tagline { get; set; } + + /// + /// The list of alternative titles of this show. + /// + public string[] Aliases { get; set; } = Array.Empty(); + + /// + /// The path of the movie video file. + /// + public string Path { get; set; } + + /// + /// The summary of this show. + /// + public string? Overview { get; set; } + + /// + /// A list of tags that match this movie. + /// + public string[] Tags { get; set; } = Array.Empty(); + + /// + /// The list of genres (themes) this show has. + /// + public Genre[] Genres { get; set; } = Array.Empty(); + + /// + /// Is this show airing, not aired yet or finished? + /// + public Status Status { get; set; } + + /// + /// The date this movie aired. + /// + public DateTime? AirDate { get; set; } + + /// + public Image? Poster { get; set; } + + /// + public Image? Thumbnail { get; set; } + + /// + public Image? Logo { get; set; } + + /// + /// A video of a few minutes that tease the content. + /// + public string? Trailer { get; set; } + + /// + public Dictionary ExternalId { get; set; } = new(); + + /// + /// The ID of the Studio that made this show. + /// + [SerializeIgnore] public int? StudioID { 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 people that made this show. + /// + [LoadableRelation][EditableRelation] public ICollection? People { get; set; } + + /// + /// The list of collections that contains this show. + /// + [LoadableRelation] public ICollection? Collections { get; set; } + + /// + /// Links to watch this movie. + /// + public VideoLinks Links => new() + { + Direct = $"/video/movie/{Slug}/direct", + Hls = $"/video/movie/{Slug}/master.m3u8", + }; + + /// + public void OnMerge(object merged) + { + if (People != null) + { + foreach (PeopleRole link in People) + link.Movie = this; + } + } + + public Movie() { } + + [JsonConstructor] + public Movie(string name) + { + Slug = Utility.ToSlug(name); + Name = name; + } + } +} diff --git a/back/src/Kyoo.Abstractions/Models/Resources/People.cs b/back/src/Kyoo.Abstractions/Models/Resources/People.cs index 9ed2cea3..51d70f6c 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/People.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/People.cs @@ -17,7 +17,10 @@ // along with Kyoo. If not, see . using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Utils; +using Newtonsoft.Json; namespace Kyoo.Abstractions.Models { @@ -27,9 +30,10 @@ namespace Kyoo.Abstractions.Models public class People : IResource, IMetadata, IThumbnails { /// - public int ID { get; set; } + public int Id { get; set; } /// + [MaxLength(256)] public string Slug { get; set; } /// @@ -38,14 +42,29 @@ namespace Kyoo.Abstractions.Models public string Name { get; set; } /// - public Dictionary Images { get; set; } + public Image? Poster { get; set; } /// - [EditableRelation][LoadableRelation] public ICollection ExternalIDs { get; set; } + public Image? Thumbnail { get; set; } + + /// + public Image? Logo { get; set; } + + /// + public Dictionary ExternalId { get; set; } = new(); /// /// The list of roles this person has played in. See for more information. /// - [EditableRelation][LoadableRelation] public ICollection Roles { get; set; } + [EditableRelation][LoadableRelation] public ICollection? Roles { get; set; } + + public People() { } + + [JsonConstructor] + public People(string name) + { + Slug = Utility.ToSlug(name); + Name = name; + } } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Provider.cs b/back/src/Kyoo.Abstractions/Models/Resources/Provider.cs deleted file mode 100644 index 4e419027..00000000 --- a/back/src/Kyoo.Abstractions/Models/Resources/Provider.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System.Collections.Generic; -using Kyoo.Abstractions.Models.Attributes; -using Kyoo.Utils; - -namespace Kyoo.Abstractions.Models -{ - /// - /// A dead class that will be removed later. - /// - // TODO: Delete this class - public class Provider : IResource, IThumbnails - { - /// - public int ID { get; set; } - - /// - public string Slug { get; set; } - - /// - /// The name of this provider. - /// - public string Name { get; set; } - - /// - public Dictionary Images { get; set; } - - /// - /// The list of libraries that uses this provider. - /// - [LoadableRelation] public ICollection Libraries { get; set; } - - /// - /// 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; - Images = new Dictionary - { - [Models.Images.Logo] = logo - }; - } - } -} diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs index 91063b8b..6cd21c3c 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using JetBrains.Annotations; using Kyoo.Abstractions.Controllers; @@ -31,16 +32,17 @@ namespace Kyoo.Abstractions.Models public class Season : IResource, IMetadata, IThumbnails { /// - public int ID { get; set; } + public int Id { get; set; } /// [Computed] + [MaxLength(256)] public string Slug { get { if (ShowSlug == null && Show == null) - return $"{ShowID}-s{SeasonNumber}"; + return $"{ShowId}-s{SeasonNumber}"; return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}"; } @@ -48,7 +50,7 @@ namespace Kyoo.Abstractions.Models [NotNull] private set { - Match match = Regex.Match(value ?? string.Empty, @"(?.+)-s(?\d+)"); + Match match = Regex.Match(value, @"(?.+)-s(?\d+)"); if (!match.Success) throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); @@ -60,18 +62,18 @@ namespace Kyoo.Abstractions.Models /// /// 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; } + [SerializeIgnore] public string? ShowSlug { private get; set; } /// /// The ID of the Show containing this season. /// - [SerializeIgnore] public int ShowID { get; set; } + [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; } + [LoadableRelation(nameof(ShowId))] public Show? Show { get; set; } /// /// The number of this season. This can be set to 0 to indicate specials. @@ -81,12 +83,12 @@ namespace Kyoo.Abstractions.Models /// /// The title of this season. /// - public string Title { get; set; } + public string? Name { get; set; } /// /// A quick overview of this season. /// - public string Overview { get; set; } + public string? Overview { get; set; } /// /// The starting air date of this season. @@ -99,14 +101,20 @@ namespace Kyoo.Abstractions.Models public DateTime? EndDate { get; set; } /// - public Dictionary Images { get; set; } + public Image? Poster { get; set; } /// - [EditableRelation][LoadableRelation] public ICollection ExternalIDs { get; set; } + public Image? Thumbnail { get; set; } + + /// + public Image? Logo { get; set; } + + /// + public Dictionary ExternalId { get; set; } = new(); /// /// The list of episodes that this season contains. /// - [LoadableRelation] public ICollection Episodes { get; set; } + [LoadableRelation] public ICollection? Episodes { get; set; } } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs index 646dee9f..aeba5c68 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -18,8 +18,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Utils; +using Newtonsoft.Json; namespace Kyoo.Abstractions.Models { @@ -29,43 +32,47 @@ namespace Kyoo.Abstractions.Models public class Show : IResource, IMetadata, IOnMerge, IThumbnails { /// - public int ID { get; set; } + public int Id { get; set; } /// + [MaxLength(256)] public string Slug { get; set; } /// /// The title of this show. /// - public string Title { get; set; } + public string Name { get; set; } + + /// + /// A catchphrase for this show. + /// + public string? Tagline { 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. - /// - [SerializeIgnore] public string Path { get; set; } + public List Aliases { get; set; } = new(); /// /// The summary of this show. /// - public string Overview { get; set; } + public string? Overview { get; set; } + + /// + /// A list of tags that match this movie. + /// + public List Tags { get; set; } = new(); + + /// + /// The list of genres (themes) this show has. + /// + public List Genres { get; set; } = new(); /// /// Is this show airing, not aired yet or finished? /// public Status Status { get; set; } - /// - /// An URL to a trailer. - /// - /// TODO for now, this is set to a youtube url. It should be cached and converted to a local file. - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string TrailerUrl => Images?.GetValueOrDefault(Models.Images.Trailer); - /// /// The date this show started airing. It can be null if this is unknown. /// @@ -73,64 +80,61 @@ namespace Kyoo.Abstractions.Models /// /// 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 Dictionary Images { get; set; } - - /// - /// True if this show represent a movie, false otherwise. - /// - public bool IsMovie { get; set; } + public Image? Poster { get; set; } /// - [EditableRelation][LoadableRelation] public ICollection ExternalIDs { get; set; } + public Image? Thumbnail { get; set; } + + /// + public Image? Logo { get; set; } + + /// + /// A video of a few minutes that tease the content. + /// + public string? Trailer { get; set; } + + [SerializeIgnore] public DateTime? AirDate => StartAir; + + /// + public Dictionary ExternalId { get; set; } = new(); /// /// The ID of the Studio that made this show. /// - [SerializeIgnore] public int? StudioID { get; set; } + [SerializeIgnore] public int? StudioId { 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; } + [LoadableRelation(nameof(StudioId))][EditableRelation] public Studio? Studio { get; set; } /// /// The list of people that made this show. /// - [LoadableRelation][EditableRelation] public ICollection People { get; set; } + [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; } + [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 null). /// 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; } + [LoadableRelation] public ICollection? Episodes { get; set; } /// /// The list of collections that contains this show. /// - [LoadableRelation] public ICollection Collections { get; set; } + [LoadableRelation] public ICollection? Collections { get; set; } /// public void OnMerge(object merged) @@ -153,6 +157,15 @@ namespace Kyoo.Abstractions.Models episode.Show = this; } } + + public Show() { } + + [JsonConstructor] + public Show(string name) + { + Slug = Utility.ToSlug(name); + Name = name; + } } /// diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs b/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs index 973404d2..c1071766 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs @@ -17,8 +17,10 @@ // along with Kyoo. If not, see . using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; +using Newtonsoft.Json; namespace Kyoo.Abstractions.Models { @@ -28,9 +30,10 @@ namespace Kyoo.Abstractions.Models public class Studio : IResource, IMetadata { /// - public int ID { get; set; } + public int Id { get; set; } /// + [MaxLength(256)] public string Slug { get; set; } /// @@ -41,10 +44,15 @@ namespace Kyoo.Abstractions.Models /// /// The list of shows that are made by this studio. /// - [LoadableRelation] public ICollection Shows { get; set; } + [LoadableRelation] public ICollection? Shows { get; set; } + + /// + /// The list of movies that are made by this studio. + /// + [LoadableRelation] public ICollection? Movies { get; set; } /// - [EditableRelation][LoadableRelation] public ICollection ExternalIDs { get; set; } + public Dictionary ExternalId { get; set; } = new(); /// /// Create a new, empty, . @@ -55,6 +63,7 @@ namespace Kyoo.Abstractions.Models /// Create a new with a specific name, the slug is calculated automatically. /// /// The name of the studio. + [JsonConstructor] public Studio(string name) { Slug = Utility.ToSlug(name); diff --git a/back/src/Kyoo.Abstractions/Models/Resources/User.cs b/back/src/Kyoo.Abstractions/Models/Resources/User.cs index cf9f5368..aa484e3f 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/User.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/User.cs @@ -16,20 +16,25 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Utils; +using Newtonsoft.Json; namespace Kyoo.Abstractions.Models { /// /// A single user of the app. /// - public class User : IResource, IThumbnails + public class User : IResource { /// - public int ID { get; set; } + public int Id { get; set; } /// + [MaxLength(256)] public string Slug { get; set; } /// @@ -51,27 +56,32 @@ namespace Kyoo.Abstractions.Models /// /// The list of permissions of the user. The format of this is implementation dependent. /// - public string[] Permissions { get; set; } + public string[] Permissions { get; set; } = Array.Empty(); /// - /// Arbitrary extra data that can be used by specific authentication implementations. + /// A logo is a small image representing the resource. /// - [SerializeIgnore] - public Dictionary ExtraData { get; set; } - - /// - public Dictionary Images { get; set; } + public Image? Logo { get; set; } /// /// The list of shows the user has finished. /// [SerializeIgnore] - public ICollection Watched { get; set; } + public ICollection? Watched { get; set; } /// /// The list of episodes the user is watching (stopped in progress or the next episode of the show) /// [SerializeIgnore] - public ICollection CurrentlyWatching { get; set; } + public ICollection? CurrentlyWatching { get; set; } + + public User() { } + + [JsonConstructor] + public User(string username) + { + Slug = Utility.ToSlug(username); + Username = username; + } } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/WatchedEpisode.cs b/back/src/Kyoo.Abstractions/Models/Resources/WatchedEpisode.cs index 66a11a3c..b5a76d68 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/WatchedEpisode.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/WatchedEpisode.cs @@ -36,7 +36,7 @@ namespace Kyoo.Abstractions.Models /// /// The started. /// - public Episode Episode { get; set; } + public Episode? Episode { get; set; } /// /// Where the player has stopped watching the episode (between 0 and 100). diff --git a/back/src/Kyoo.Abstractions/Models/SearchResult.cs b/back/src/Kyoo.Abstractions/Models/SearchResult.cs index 0f42a077..54ff93bf 100644 --- a/back/src/Kyoo.Abstractions/Models/SearchResult.cs +++ b/back/src/Kyoo.Abstractions/Models/SearchResult.cs @@ -35,6 +35,16 @@ namespace Kyoo.Abstractions.Models /// public ICollection Collections { get; init; } + /// + /// The items that matched the search. + /// + public ICollection Items { get; init; } + + /// + /// The movies that matched the search. + /// + public ICollection Movies { get; init; } + /// /// The shows that matched the search. /// @@ -50,11 +60,6 @@ namespace Kyoo.Abstractions.Models /// public ICollection People { get; init; } - /// - /// The genres that matched the search. - /// - public ICollection Genres { get; init; } - /// /// The studios that matched the search. /// diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs index 0c56b2b1..c0a1cbdc 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -23,7 +23,6 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using JetBrains.Annotations; namespace Kyoo.Abstractions.Models.Utils { @@ -43,7 +42,7 @@ namespace Kyoo.Abstractions.Models.Utils /// /// The slug of the resource or null if the id is specified. /// - private readonly string _slug; + private readonly string? _slug; /// /// Create a new for the given id. @@ -58,10 +57,8 @@ namespace Kyoo.Abstractions.Models.Utils /// Create a new for the given slug. /// /// The slug of the resource. - public Identifier([NotNull] string slug) + public Identifier(string slug) { - if (slug == null) - throw new ArgumentNullException(nameof(slug)); _slug = slug; } @@ -87,7 +84,7 @@ namespace Kyoo.Abstractions.Models.Utils { return _id.HasValue ? idFunc(_id.Value) - : slugFunc(_slug); + : slugFunc(_slug!); } /// @@ -139,7 +136,7 @@ namespace Kyoo.Abstractions.Models.Utils public bool IsSame(IResource resource) { return Match( - id => resource.ID == id, + id => resource.Id == id, slug => resource.Slug == slug ); } @@ -155,7 +152,7 @@ namespace Kyoo.Abstractions.Models.Utils where T : IResource { return _id.HasValue - ? x => x.ID == _id.Value + ? x => x.Id == _id.Value : x => x.Slug == _slug; } @@ -174,7 +171,7 @@ namespace Kyoo.Abstractions.Models.Utils .Where(x => x.Name == nameof(Enumerable.Any)) .FirstOrDefault(x => x.GetParameters().Length == 2)! .MakeGenericMethod(typeof(T2)); - MethodCallExpression call = Expression.Call(null, method!, listGetter.Body, IsSame()); + MethodCallExpression call = Expression.Call(null, method, listGetter.Body, IsSame()); return Expression.Lambda>(call, listGetter.Parameters); } @@ -183,7 +180,7 @@ namespace Kyoo.Abstractions.Models.Utils { return _id.HasValue ? _id.Value.ToString() - : _slug; + : _slug!; } /// @@ -192,7 +189,7 @@ namespace Kyoo.Abstractions.Models.Utils public class IdentifierConvertor : TypeConverter { /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { if (sourceType == typeof(int) || sourceType == typeof(string)) return true; @@ -200,12 +197,12 @@ namespace Kyoo.Abstractions.Models.Utils } /// - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is int id) return new Identifier(id); if (value is not string slug) - return base.ConvertFrom(context, culture, value); + return base.ConvertFrom(context, culture, value)!; return int.TryParse(slug, out id) ? new Identifier(id) : new Identifier(slug); diff --git a/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs b/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs index fa40fc64..2d55d4ed 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs @@ -31,13 +31,13 @@ namespace Kyoo.Abstractions.Models.Utils /// The list of errors that where made in the request. /// /// ["InvalidFilter: no field 'startYear' on a collection"] - [NotNull] public string[] Errors { get; set; } + public string[] Errors { get; set; } /// /// Create a new with one error. /// /// The error to specify in the response. - public RequestError([NotNull] string error) + public RequestError(string error) { if (error == null) throw new ArgumentNullException(nameof(error)); @@ -48,7 +48,7 @@ namespace Kyoo.Abstractions.Models.Utils /// Create a new with multiple errors. /// /// The errors to specify in the response. - public RequestError([NotNull] string[] errors) + public RequestError(string[] errors) { if (errors == null || !errors.Any()) throw new ArgumentException("Errors must be non null and not empty", nameof(errors)); diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs index 5e8191b0..702e6240 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs @@ -33,11 +33,11 @@ namespace Kyoo.Abstractions.Controllers /// /// Sort by a specific key /// - /// The sort keys. This members will be used to sort the results. - /// + /// The sort keys. This members will be used to sort the results. + /// /// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order. /// - public record By(string key, bool desendant = false) : Sort + public record By(string Key, bool Desendant = false) : Sort { /// /// Sort by a specific key @@ -53,8 +53,8 @@ namespace Kyoo.Abstractions.Controllers /// /// Sort by multiple keys. /// - /// The list of keys to sort by. - public record Conglomerate(params Sort[] list) : Sort; + /// The list of keys to sort by. + public record Conglomerate(params Sort[] List) : Sort; /// The default sort method for the given type. public record Default : Sort; @@ -73,7 +73,7 @@ namespace Kyoo.Abstractions.Controllers return new Conglomerate(sortBy.Split(',').Select(From).ToArray()); string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy; - string order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null; + string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null; bool desendant = order switch { "desc" => true, @@ -81,7 +81,7 @@ namespace Kyoo.Abstractions.Controllers null => false, _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") }; - PropertyInfo property = typeof(T).GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + PropertyInfo? property = typeof(T).GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (property == null) throw new ArgumentException("The given sort key is not valid."); return new By(property.Name, desendant); diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/GenreTests.cs b/back/src/Kyoo.Abstractions/Models/VideoLinks.cs similarity index 51% rename from back/tests/Kyoo.Tests/Database/SpecificTests/GenreTests.cs rename to back/src/Kyoo.Abstractions/Models/VideoLinks.cs index b4456756..b0c2cb3b 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/GenreTests.cs +++ b/back/src/Kyoo.Abstractions/Models/VideoLinks.cs @@ -16,33 +16,21 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System.Diagnostics.CodeAnalysis; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Xunit; -using Xunit.Abstractions; - -namespace Kyoo.Tests.Database +namespace Kyoo.Abstractions.Models { - namespace PostgreSQL + /// + /// The links to see a movie or an episode. + /// + public class VideoLinks { - [Collection(nameof(Postgresql))] - public class GenreTests : AGenreTests - { - public GenreTests(PostgresFixture postgres, ITestOutputHelper output) - : base(new RepositoryActivator(output, postgres)) { } - } - } + /// + /// The direct link to the unprocessed video (pristine quality). + /// + public string Direct { get; set; } - public abstract class AGenreTests : RepositoryTests - { - [SuppressMessage("ReSharper", "NotAccessedField.Local")] - private readonly IGenreRepository _repository; - - protected AGenreTests(RepositoryActivator repositories) - : base(repositories) - { - _repository = Repositories.LibraryManager.GenreRepository; - } + /// + /// The link to an HLS master playlist containing all qualities available for this video. + /// + public string Hls { get; set; } } } diff --git a/back/src/Kyoo.Abstractions/Models/WatchItem.cs b/back/src/Kyoo.Abstractions/Models/WatchItem.cs deleted file mode 100644 index ca87d1e7..00000000 --- a/back/src/Kyoo.Abstractions/Models/WatchItem.cs +++ /dev/null @@ -1,186 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models.Attributes; -using Newtonsoft.Json; - -namespace Kyoo.Abstractions.Models -{ - /// - /// 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 : CustomTypeDescriptor, IThumbnails, ILink - { - /// - /// The ID of the episode associated with this item. - /// - public int EpisodeID { 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. - /// - public int? SeasonNumber { get; set; } - - /// - /// The number of this episode is it's season. - /// - 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. - /// - public int? AbsoluteNumber { get; set; } - - /// - /// The title of this episode. - /// - public string Title { get; set; } - - /// - /// The summary 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; } - - /// - /// 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; } - - /// - public Dictionary Images { get; set; } - - /// - /// The transcoder's info for this item. This include subtitles, fonts, chapters... - /// - public object Info { get; set; } - - [SerializeIgnore] - private string _Type => IsMovie ? "movie" : "episode"; - - /// - public object Link => new - { - Direct = $"/video/{_Type}/{Slug}/direct", - Hls = $"/video/{_Type}/{Slug}/master.m3u8", - }; - - /// - /// Create a from an . - /// - /// The episode to transform. - /// - /// A library manager to retrieve the next and previous episode and load the show and tracks of the episode. - /// - /// A http client to reach the transcoder. - /// A new WatchItem representing the given episode. - public static async Task FromEpisode(Episode ep, ILibraryManager library, HttpClient client) - { - await library.Load(ep, x => x.Show); - - return new WatchItem - { - EpisodeID = ep.ID, - Slug = ep.Slug, - ShowSlug = ep.Show.Slug, - ShowTitle = ep.Show.Title, - SeasonNumber = ep.SeasonNumber, - EpisodeNumber = ep.EpisodeNumber, - AbsoluteNumber = ep.AbsoluteNumber, - Title = ep.Title, - Overview = ep.Overview, - ReleaseDate = ep.ReleaseDate, - Images = ep.Show.Images, - PreviousEpisode = ep.Show.IsMovie - ? null - : (await library.GetAll( - where: x => x.ShowID == ep.ShowID, - limit: new Pagination(1, ep.ID, true) - )).FirstOrDefault(), - NextEpisode = ep.Show.IsMovie - ? null - : (await library.GetAll( - where: x => x.ShowID == ep.ShowID, - limit: new Pagination(1, ep.ID) - )).FirstOrDefault(), - IsMovie = ep.Show.IsMovie, - Info = await _GetInfo(ep, client), - }; - } - - private static async Task _GetInfo(Episode ep, HttpClient client) - { - HttpResponseMessage ret = await client.GetAsync($"http://transcoder:7666/{(ep.Show.IsMovie ? "movie" : "episode")}/{ep.Slug}/info"); - ret.EnsureSuccessStatusCode(); - string content = await ret.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(content); - } - - /// - public override string GetClassName() - { - return nameof(Show); - } - - /// - public override string GetComponentName() - { - return ShowSlug; - } - } -} diff --git a/back/src/Kyoo.Abstractions/Module.cs b/back/src/Kyoo.Abstractions/Module.cs index dc0edc67..1abfd1e9 100644 --- a/back/src/Kyoo.Abstractions/Module.cs +++ b/back/src/Kyoo.Abstractions/Module.cs @@ -43,7 +43,7 @@ namespace Kyoo.Abstractions { return builder.RegisterType() .As() - .As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))) + .As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))!) .InstancePerLifetimeScope(); } @@ -59,6 +59,7 @@ namespace Kyoo.Abstractions /// The initial container. public static IRegistrationBuilder RegisterRepository(this ContainerBuilder builder) + where T : notnull where T2 : IBaseRepository, T { return builder.RegisterRepository().As(); diff --git a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs b/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs index 34ba106a..05e18f49 100644 --- a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs +++ b/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs @@ -17,9 +17,7 @@ // along with Kyoo. If not, see . using System; -using System.Collections; using System.Collections.Generic; -using System.Threading.Tasks; using JetBrains.Annotations; namespace Kyoo.Utils @@ -29,141 +27,16 @@ namespace Kyoo.Utils /// 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. - /// The list or the mapper can't be null - [LinqTunnel] - public static IEnumerable Map([NotNull] this IEnumerable self, - [NotNull] Func mapper) - { - if (self == null) - throw new ArgumentNullException(nameof(self)); - 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. - /// 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. - /// The list or the mapper can't be null. - [LinqTunnel] - public static IAsyncEnumerable MapAsync([NotNull] this IEnumerable self, - [NotNull] Func> mapper) - { - if (self == null) - throw new ArgumentNullException(nameof(self)); - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - static async IAsyncEnumerable Generator(IEnumerable self, Func> mapper) - { - using IEnumerator enumerator = self.GetEnumerator(); - int index = 0; - - while (enumerator.MoveNext()) - { - yield return await mapper(enumerator.Current, index); - index++; - } - } - - return Generator(self, mapper); - } - - /// - /// 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 - /// The list or the mapper can't be null - [LinqTunnel] - public static IAsyncEnumerable SelectAsync([NotNull] this IEnumerable self, - [NotNull] Func> mapper) - { - if (self == null) - throw new ArgumentNullException(nameof(self)); - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - 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); - } - - /// - /// 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 - [LinqTunnel] - public static Task> ToListAsync([NotNull] this IAsyncEnumerable self) - { - if (self == null) - throw new ArgumentNullException(nameof(self)); - - static async Task> ToList(IAsyncEnumerable self) - { - List ret = new(); - await foreach (T i in self) - ret.Add(i); - return ret; - } - - return ToList(self); - } - /// /// 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 - /// 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) + public static IEnumerable IfEmpty(this IEnumerable self, Action action) { - if (self == null) - throw new ArgumentNullException(nameof(self)); - if (action == null) - throw new ArgumentNullException(nameof(action)); - static IEnumerable Generator(IEnumerable self, Action action) { using IEnumerator enumerator = self.GetEnumerator(); @@ -190,111 +63,12 @@ namespace Kyoo.Utils /// 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) + public static void ForEach(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 - /// A representing the asynchronous operation. - 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. - /// A representing the asynchronous operation. - 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. - /// A representing the asynchronous operation. - 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 - [LinqTunnel] - 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 - [LinqTunnel] - 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; - } } } diff --git a/back/src/Kyoo.Abstractions/Utility/Merger.cs b/back/src/Kyoo.Abstractions/Utility/Merger.cs index 237eff06..ccbe2b48 100644 --- a/back/src/Kyoo.Abstractions/Utility/Merger.cs +++ b/back/src/Kyoo.Abstractions/Utility/Merger.cs @@ -17,13 +17,10 @@ // along with Kyoo. If not, see . using System; -using System.Collections; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Reflection; using JetBrains.Annotations; -using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Utils @@ -33,99 +30,9 @@ namespace Kyoo.Utils /// 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 type of items in the lists to merge. - /// The two list merged as an array - [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] - public static T[] MergeLists([CanBeNull] IEnumerable first, - [CanBeNull] IEnumerable second, - [CanBeNull] 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(); - } - - /// - /// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept. - /// - /// The first dictionary to merge - /// The second dictionary to merge - /// The type of the keys in dictionaries - /// The type of values in the dictionaries - /// The first dictionary with the missing elements of . - /// - [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] - public static IDictionary MergeDictionaries([CanBeNull] IDictionary first, - [CanBeNull] IDictionary second) - { - return MergeDictionaries(first, second, out bool _); - } - - /// - /// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept. - /// - /// The first dictionary to merge - /// The second dictionary to merge - /// - /// true if a new items has been added to the dictionary, false otherwise. - /// - /// The type of the keys in dictionaries - /// The type of values in the dictionaries - /// The first dictionary with the missing elements of . - [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] - public static IDictionary MergeDictionaries([CanBeNull] IDictionary first, - [CanBeNull] IDictionary second, - out bool hasChanged) - { - if (first == null) - { - hasChanged = true; - return second; - } - - hasChanged = false; - if (second == null) - return first; - foreach ((T key, T2 value) in second) - { - bool success = first.TryAdd(key, value); - hasChanged |= success; - - if (success || first[key]?.Equals(default) == false || value?.Equals(default) != false) - continue; - first[key] = value; - hasChanged = true; - } - - return first; - } - /// /// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept. /// - /// - /// The only difference in this function compared to - /// - /// is the way is calculated and the order of the arguments. - /// - /// MergeDictionaries(first, second); - /// - /// will do the same thing as - /// - /// CompleteDictionaries(second, first, out bool _); - /// - /// /// The first dictionary to merge /// The second dictionary to merge /// @@ -138,8 +45,8 @@ namespace Kyoo.Utils /// set to those of . /// [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] - public static IDictionary CompleteDictionaries([CanBeNull] IDictionary first, - [CanBeNull] IDictionary second, + public static IDictionary? CompleteDictionaries(IDictionary? first, + IDictionary? second, out bool hasChanged) { if (first == null) @@ -151,49 +58,17 @@ namespace Kyoo.Utils hasChanged = false; if (second == null) return first; - hasChanged = second.Any(x => x.Value?.Equals(first[x.Key]) == false); + hasChanged = second.Any(x => !first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false); foreach ((T key, T2 value) in first) second.TryAdd(key, value); return second; } - /// - /// 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 non-default values of seconds to the corresponding property of second. /// Dictionaries are handled like anonymous objects with a property per key/pair value - /// (see - /// - /// for more details). /// At the end, the OnMerge method of first will be called if first is a /// - /// - /// This does the opposite of . - /// /// /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"} /// @@ -208,19 +83,16 @@ namespace Kyoo.Utils /// /// Fields of T will be completed /// - /// If first is null - public static T Complete([NotNull] T first, - [CanBeNull] T second, - [InstantHandle] Func where = null) + public static T Complete(T first, + T? second, + [InstantHandle] 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 + .Where(x => x is { CanRead: true, CanWrite: true } && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); if (where != null) @@ -228,17 +100,16 @@ namespace Kyoo.Utils foreach (PropertyInfo property in properties) { - object value = property.GetValue(second); - object defaultValue = property.GetCustomAttribute()?.Value - ?? property.PropertyType.GetClrDefault(); + object? value = property.GetValue(second); - if (value?.Equals(defaultValue) != false || value.Equals(property.GetValue(first))) + if (value?.Equals(property.GetValue(first)) == true) continue; + if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) { - Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>)) + Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))! .GenericTypeArguments; - object[] parameters = + object?[] parameters = { property.GetValue(first), value, @@ -248,8 +119,8 @@ namespace Kyoo.Utils typeof(Merger), nameof(CompleteDictionaries), dictionaryTypes, - parameters); - if ((bool)parameters[2]) + parameters)!; + if ((bool)parameters[2]!) property.SetValue(first, newDictionary); } else @@ -260,109 +131,5 @@ namespace Kyoo.Utils merge.OnMerge(second); return first; } - - /// - /// This will set missing values of to the corresponding values of . - /// Enumerable will be merged (concatenated) and Dictionaries too. - /// At the end, the OnMerge method of first will be called if first is a . - /// - /// - /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"} - /// - /// - /// 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 merged - /// - [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] - public static T Merge([CanBeNull] T first, - [CanBeNull] T second, - [InstantHandle] Func where = null) - { - 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); - - if (where != null) - properties = properties.Where(where); - - foreach (PropertyInfo property in properties) - { - object oldValue = property.GetValue(first); - object newValue = property.GetValue(second); - object defaultValue = property.PropertyType.GetClrDefault(); - - if (oldValue?.Equals(defaultValue) != false) - property.SetValue(first, newValue); - else if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) - { - Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>)) - .GenericTypeArguments; - object[] parameters = - { - oldValue, - newValue, - false - }; - object newDictionary = Utility.RunGenericMethod( - typeof(Merger), - nameof(MergeDictionaries), - dictionaryTypes, - parameters); - if ((bool)parameters[2]) - property.SetValue(first, newDictionary); - } - else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) - && property.PropertyType != typeof(string)) - { - Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>)) - .GenericTypeArguments - .First(); - Func equalityComparer = enumerableType.IsAssignableTo(typeof(IResource)) - ? (x, y) => x.Slug == y.Slug - : null; - property.SetValue(first, Utility.RunGenericMethod( - typeof(Merger), - nameof(MergeLists), - enumerableType, - oldValue, newValue, equalityComparer)); - } - } - - 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 || property.GetCustomAttribute() != null) - continue; - property.SetValue(obj, property.PropertyType.GetClrDefault()); - } - - return obj; - } } } diff --git a/back/src/Kyoo.Abstractions/Utility/TaskUtils.cs b/back/src/Kyoo.Abstractions/Utility/TaskUtils.cs index 69da84cc..973ec3ea 100644 --- a/back/src/Kyoo.Abstractions/Utility/TaskUtils.cs +++ b/back/src/Kyoo.Abstractions/Utility/TaskUtils.cs @@ -18,7 +18,6 @@ using System; using System.Threading.Tasks; -using JetBrains.Annotations; namespace Kyoo.Utils { @@ -49,38 +48,5 @@ namespace Kyoo.Utils 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); - } } } diff --git a/back/src/Kyoo.Abstractions/Utility/Utility.cs b/back/src/Kyoo.Abstractions/Utility/Utility.cs index bce40f4e..e3e91694 100644 --- a/back/src/Kyoo.Abstractions/Utility/Utility.cs +++ b/back/src/Kyoo.Abstractions/Utility/Utility.cs @@ -41,8 +41,6 @@ namespace Kyoo.Utils /// 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); } @@ -57,44 +55,19 @@ namespace Kyoo.Utils { if (!IsPropertyExpression(ex)) throw new ArgumentException($"{ex} is not a property expression."); - MemberExpression member = ex.Body.NodeType == ExpressionType.Convert + 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) /// /// The string to slugify /// The slug version of the given string - public static string ToSlug([CanBeNull] string str) + public static string ToSlug(string str) { - if (str == null) - return null; - str = str.ToLowerInvariant(); string normalizedString = str.Normalize(NormalizationForm.FormD); @@ -114,59 +87,25 @@ namespace Kyoo.Utils 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) /// - /// The starting type + /// The starting type /// A list of types - /// can't be null - public static IEnumerable GetInheritanceTree([NotNull] this Type type) + public static IEnumerable GetInheritanceTree(this Type self) { - if (type == null) - throw new ArgumentNullException(nameof(type)); - for (; type != null; type = type.BaseType) + for (Type? type = self; 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) + public static bool IsOfGenericType(Type type, 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."); @@ -184,14 +123,9 @@ namespace Kyoo.Utils /// 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) + public static Type? GetGenericDefinition(Type type, 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."); @@ -224,20 +158,12 @@ namespace Kyoo.Utils /// No method match the given constraints. /// The method handle of the matching method. [PublicAPI] - [NotNull] - public static MethodInfo GetMethod([NotNull] Type type, + public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, - [NotNull] Type[] generics, - [NotNull] object[] args) + Type[] generics, + object?[] args) { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (generics == null) - throw new ArgumentNullException(nameof(generics)); - if (args == null) - throw new ArgumentNullException(nameof(args)); - MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) .Where(x => x.Name == name) .Where(x => x.GetGenericArguments().Length == generics.Length) @@ -275,7 +201,7 @@ namespace Kyoo.Utils /// Run a generic static method for a runtime . /// /// - /// To run for a List where you don't know the type at compile type, + /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type, /// you could do: /// /// Utility.RunGenericMethod<object>( @@ -294,12 +220,11 @@ namespace Kyoo.Utils /// /// No method match the given constraints. /// The return of the method you wanted to run. - /// /// - public static T RunGenericMethod( - [NotNull] Type owner, - [NotNull] string methodName, - [NotNull] Type type, + public static T? RunGenericMethod( + Type owner, + string methodName, + Type type, params object[] args) { return RunGenericMethod(owner, methodName, new[] { type }, args); @@ -311,7 +236,7 @@ namespace Kyoo.Utils /// /// /// - /// To run for a List where you don't know the type at compile type, + /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type, /// you could do: /// /// Utility.RunGenericMethod<object>( @@ -330,102 +255,17 @@ namespace Kyoo.Utils /// /// No method match the given constraints. /// The return of the method you wanted to run. - /// /// - [PublicAPI] - public static T RunGenericMethod( - [NotNull] Type owner, - [NotNull] string methodName, - [NotNull] Type[] types, - params object[] args) + public static T? RunGenericMethod( + Type owner, + string methodName, + 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); - } - - /// - /// Run a generic method for a runtime . - /// - /// - /// To run for a List where you don't know the type at compile type, - /// you could do: - /// - /// Utility.RunGenericMethod<object>( - /// typeof(Utility), - /// nameof(MergeLists), - /// enumerableType, - /// oldValue, newValue, equalityComparer) - /// - /// - /// The this of the method to run. - /// The name of the method. You should use the nameof keyword. - /// The generic type to run the method with. - /// The list of arguments of the method - /// - /// The return type of the method. You can put for an unknown one. - /// - /// No method match the given constraints. - /// The return of the method you wanted to run. - /// - /// - public static T RunGenericMethod( - [NotNull] object instance, - [NotNull] string methodName, - [NotNull] Type type, - params object[] args) - { - return RunGenericMethod(instance, methodName, new[] { type }, args); - } - - /// - /// Run a generic method for a multiple runtime . - /// If your generic method only needs one type, see - /// - /// - /// - /// To run for a List where you don't know the type at compile type, - /// you could do: - /// - /// Utility.RunGenericMethod<object>( - /// typeof(Utility), - /// nameof(MergeLists), - /// enumerableType, - /// oldValue, newValue, equalityComparer) - /// - /// - /// The this of the method to run. - /// The name of the method. You should use the nameof keyword. - /// The list of generic types to run the method with. - /// The list of arguments of the method - /// - /// The return type of the method. You can put for an unknown one. - /// - /// No method match the given constraints. - /// The return of the method you wanted to run. - /// - /// - 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()); + return (T?)method.MakeGenericMethod(types).Invoke(null, args); } /// @@ -446,25 +286,11 @@ namespace Kyoo.Utils /// /// The exception to rethrow. [System.Diagnostics.CodeAnalysis.DoesNotReturn] - public static void ReThrow([NotNull] this Exception ex) + public static void ReThrow(this Exception ex) { if (ex == null) throw new ArgumentNullException(nameof(ex)); ExceptionDispatchInfo.Capture(ex).Throw(); } - - /// - /// 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}>"; - } } } diff --git a/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs b/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs index 9e238045..8b137e78 100644 --- a/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs +++ b/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs @@ -63,7 +63,7 @@ namespace Kyoo.Authentication /// public IFilterMetadata Create(PartialPermissionAttribute attribute) { - return new PermissionValidatorFilter((object)attribute.Type ?? attribute.Kind, attribute.Group, _options); + return new PermissionValidatorFilter(((object?)attribute.Type ?? attribute.Kind)!, attribute.Group, _options); } /// diff --git a/back/src/Kyoo.Authentication/Controllers/TokenController.cs b/back/src/Kyoo.Authentication/Controllers/TokenController.cs index 524b5a91..64a9f9cd 100644 --- a/back/src/Kyoo.Authentication/Controllers/TokenController.cs +++ b/back/src/Kyoo.Authentication/Controllers/TokenController.cs @@ -61,7 +61,7 @@ namespace Kyoo.Authentication : string.Empty; List claims = new() { - new Claim(Claims.Id, user.ID.ToString(CultureInfo.InvariantCulture)), + new Claim(Claims.Id, user.Id.ToString(CultureInfo.InvariantCulture)), new Claim(Claims.Name, user.Username), new Claim(Claims.Permissions, permissions), new Claim(Claims.Type, "access") @@ -85,7 +85,7 @@ namespace Kyoo.Authentication signingCredentials: credential, claims: new[] { - new Claim(Claims.Id, user.ID.ToString(CultureInfo.InvariantCulture)), + new Claim(Claims.Id, user.Id.ToString(CultureInfo.InvariantCulture)), new Claim(Claims.Guid, Guid.NewGuid().ToString()), new Claim(Claims.Type, "refresh") }, diff --git a/back/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs b/back/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs index e8e76750..7bb524e7 100644 --- a/back/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs +++ b/back/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Models; using Kyoo.Utils; @@ -72,7 +71,6 @@ namespace Kyoo.Authentication.Models.DTO Username = Username, Password = BCryptNet.HashPassword(Password), Email = Email, - ExtraData = new Dictionary() }; } } diff --git a/back/src/Kyoo.Authentication/Views/AuthApi.cs b/back/src/Kyoo.Authentication/Views/AuthApi.cs index 3a97190b..2040ac13 100644 --- a/back/src/Kyoo.Authentication/Views/AuthApi.cs +++ b/back/src/Kyoo.Authentication/Views/AuthApi.cs @@ -27,6 +27,7 @@ using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Authentication.Models; using Kyoo.Authentication.Models.DTO; +using Kyoo.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; @@ -228,8 +229,8 @@ namespace Kyoo.Authentication.Views return Unauthorized(new RequestError("User not authenticated or token invalid.")); try { - user.ID = userID; - return await _users.Edit(user, true); + user.Id = userID; + return await _users.Edit(user); } catch (ItemNotFoundException) { @@ -252,14 +253,15 @@ namespace Kyoo.Authentication.Views [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] - public async Task> PatchMe(User user) + public async Task> PatchMe(PartialResource user) { if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID)) return Unauthorized(new RequestError("User not authenticated or token invalid.")); try { - user.ID = userID; - return await _users.Edit(user, false); + if (user.Id.HasValue && user.Id != userID) + throw new ArgumentException("Can't edit your user id."); + return await _users.Patch(userID, TryUpdateModelAsync); } catch (ItemNotFoundException) { diff --git a/back/src/Kyoo.Core/Controllers/LibraryManager.cs b/back/src/Kyoo.Core/Controllers/LibraryManager.cs index 453d6b10..318e7aff 100644 --- a/back/src/Kyoo.Core/Controllers/LibraryManager.cs +++ b/back/src/Kyoo.Core/Controllers/LibraryManager.cs @@ -39,15 +39,15 @@ namespace Kyoo.Core.Controllers /// private readonly IBaseRepository[] _repositories; - /// - public ILibraryRepository LibraryRepository { get; } - /// public ILibraryItemRepository LibraryItemRepository { get; } /// public ICollectionRepository CollectionRepository { get; } + /// + public IMovieRepository MovieRepository { get; } + /// public IShowRepository ShowRepository { get; } @@ -63,12 +63,6 @@ namespace Kyoo.Core.Controllers /// public IStudioRepository StudioRepository { get; } - /// - public IGenreRepository GenreRepository { get; } - - /// - public IProviderRepository ProviderRepository { get; } - /// public IUserRepository UserRepository { get; } @@ -80,16 +74,14 @@ namespace Kyoo.Core.Controllers public LibraryManager(IEnumerable repositories) { _repositories = repositories.ToArray(); - LibraryRepository = GetRepository() as ILibraryRepository; - LibraryItemRepository = GetRepository() as ILibraryItemRepository; + LibraryItemRepository = GetRepository() as ILibraryItemRepository; CollectionRepository = GetRepository() as ICollectionRepository; + MovieRepository = GetRepository() as IMovieRepository; ShowRepository = GetRepository() as IShowRepository; SeasonRepository = GetRepository() as ISeasonRepository; EpisodeRepository = GetRepository() as IEpisodeRepository; PeopleRepository = GetRepository() as IPeopleRepository; StudioRepository = GetRepository() as IStudioRepository; - GenreRepository = GetRepository() as IGenreRepository; - ProviderRepository = GetRepository() as IProviderRepository; UserRepository = GetRepository() as IUserRepository; } @@ -217,8 +209,6 @@ namespace Kyoo.Core.Controllers where T : class, IResource where T2 : class, IResource { - if (member == null) - throw new ArgumentNullException(nameof(member)); return Load(obj, Utility.GetPropertyName(member), force); } @@ -227,8 +217,6 @@ namespace Kyoo.Core.Controllers where T : class, IResource where T2 : class { - if (member == null) - throw new ArgumentNullException(nameof(member)); return Load(obj, Utility.GetPropertyName(member), force); } @@ -259,166 +247,119 @@ namespace Kyoo.Core.Controllers return (obj, member: memberName) switch { - (Library l, nameof(Library.Providers)) => ProviderRepository - .GetAll(x => x.Libraries.Any(y => y.ID == obj.ID)) - .Then(x => l.Providers = x), - - (Library l, nameof(Library.Shows)) => ShowRepository - .GetAll(x => x.Libraries.Any(y => y.ID == obj.ID)) - .Then(x => l.Shows = x), - - (Library l, nameof(Library.Collections)) => CollectionRepository - .GetAll(x => x.Libraries.Any(y => y.ID == obj.ID)) - .Then(x => l.Collections = x), - - - (Collection c, nameof(Collection.ExternalIDs)) => _SetRelation(c, - ProviderRepository.GetMetadataID(x => x.ResourceID == obj.ID), - (x, y) => x.ExternalIDs = y, - (x, y) => { x.ResourceID = y.ID; }), - (Collection c, nameof(Collection.Shows)) => ShowRepository - .GetAll(x => x.Collections.Any(y => y.ID == obj.ID)) + .GetAll(x => x.Collections.Any(y => y.Id == obj.Id)) .Then(x => c.Shows = x), - (Collection c, nameof(Collection.Libraries)) => LibraryRepository - .GetAll(x => x.Collections.Any(y => y.ID == obj.ID)) - .Then(x => c.Libraries = x), + (Collection c, nameof(Collection.Movies)) => MovieRepository + .GetAll(x => x.Collections.Any(y => y.Id == obj.Id)) + .Then(x => c.Movies = x), - (Show s, nameof(Show.ExternalIDs)) => _SetRelation(s, - ProviderRepository.GetMetadataID(x => x.ResourceID == obj.ID), - (x, y) => x.ExternalIDs = y, - (x, y) => { x.ResourceID = y.ID; }), + (Movie m, nameof(Movie.People)) => PeopleRepository + .GetFromShow(obj.Id) + .Then(x => m.People = x), + + (Movie m, nameof(Movie.Collections)) => CollectionRepository + .GetAll(x => x.Movies.Any(y => y.Id == obj.Id)) + .Then(x => m.Collections = x), + + (Movie m, nameof(Movie.Studio)) => StudioRepository + .GetOrDefault(x => x.Movies.Any(y => y.Id == obj.Id)) + .Then(x => + { + m.Studio = x; + m.StudioID = x?.Id ?? 0; + }), - (Show s, nameof(Show.Genres)) => GenreRepository - .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) - .Then(x => s.Genres = x), (Show s, nameof(Show.People)) => PeopleRepository - .GetFromShow(obj.ID) + .GetFromShow(obj.Id) .Then(x => s.People = x), (Show s, nameof(Show.Seasons)) => _SetRelation(s, - SeasonRepository.GetAll(x => x.Show.ID == obj.ID), + SeasonRepository.GetAll(x => x.Show.Id == obj.Id), (x, y) => x.Seasons = y, - (x, y) => { x.Show = y; x.ShowID = y.ID; }), + (x, y) => { x.Show = y; x.ShowId = y.Id; }), (Show s, nameof(Show.Episodes)) => _SetRelation(s, - EpisodeRepository.GetAll(x => x.Show.ID == obj.ID), + EpisodeRepository.GetAll(x => x.Show.Id == obj.Id), (x, y) => x.Episodes = y, - (x, y) => { x.Show = y; x.ShowID = y.ID; }), - - (Show s, nameof(Show.Libraries)) => LibraryRepository - .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) - .Then(x => s.Libraries = x), + (x, y) => { x.Show = y; x.ShowId = y.Id; }), (Show s, nameof(Show.Collections)) => CollectionRepository - .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) + .GetAll(x => x.Shows.Any(y => y.Id == obj.Id)) .Then(x => s.Collections = x), (Show s, nameof(Show.Studio)) => StudioRepository - .GetOrDefault(x => x.Shows.Any(y => y.ID == obj.ID)) + .GetOrDefault(x => x.Shows.Any(y => y.Id == obj.Id)) .Then(x => { s.Studio = x; - s.StudioID = x?.ID ?? 0; + s.StudioId = x?.Id ?? 0; }), - (Season s, nameof(Season.ExternalIDs)) => _SetRelation(s, - ProviderRepository.GetMetadataID(x => x.ResourceID == obj.ID), - (x, y) => x.ExternalIDs = y, - (x, y) => { x.ResourceID = y.ID; }), - (Season s, nameof(Season.Episodes)) => _SetRelation(s, - EpisodeRepository.GetAll(x => x.Season.ID == obj.ID), + EpisodeRepository.GetAll(x => x.Season.Id == obj.Id), (x, y) => x.Episodes = y, - (x, y) => { x.Season = y; x.SeasonID = y.ID; }), + (x, y) => { x.Season = y; x.SeasonId = y.Id; }), (Season s, nameof(Season.Show)) => ShowRepository - .GetOrDefault(x => x.Seasons.Any(y => y.ID == obj.ID)) + .GetOrDefault(x => x.Seasons.Any(y => y.Id == obj.Id)) .Then(x => { s.Show = x; - s.ShowID = x?.ID ?? 0; + s.ShowId = x?.Id ?? 0; }), - (Episode e, nameof(Episode.ExternalIDs)) => _SetRelation(e, - ProviderRepository.GetMetadataID(x => x.ResourceID == obj.ID), - (x, y) => x.ExternalIDs = y, - (x, y) => { x.ResourceID = y.ID; }), - (Episode e, nameof(Episode.Show)) => ShowRepository - .GetOrDefault(x => x.Episodes.Any(y => y.ID == obj.ID)) + .GetOrDefault(x => x.Episodes.Any(y => y.Id == obj.Id)) .Then(x => { e.Show = x; - e.ShowID = x?.ID ?? 0; + e.ShowId = x?.Id ?? 0; }), (Episode e, nameof(Episode.Season)) => SeasonRepository - .GetOrDefault(x => x.Episodes.Any(y => y.ID == e.ID)) + .GetOrDefault(x => x.Episodes.Any(y => y.Id == e.Id)) .Then(x => { e.Season = x; - e.SeasonID = x?.ID ?? 0; + e.SeasonId = x?.Id ?? 0; }), + (Episode e, nameof(Episode.PreviousEpisode)) => EpisodeRepository + .GetAll( + where: x => x.ShowId == e.ShowId, + limit: new Pagination(1, e.Id, true) + ).Then(x => e.PreviousEpisode = x.FirstOrDefault()), - (Genre g, nameof(Genre.Shows)) => ShowRepository - .GetAll(x => x.Genres.Any(y => y.ID == obj.ID)) - .Then(x => g.Shows = x), + (Episode e, nameof(Episode.NextEpisode)) => EpisodeRepository + .GetAll( + where: x => x.ShowId == e.ShowId, + limit: new Pagination(1, e.Id) + ).Then(x => e.NextEpisode = x.FirstOrDefault()), (Studio s, nameof(Studio.Shows)) => ShowRepository - .GetAll(x => x.Studio.ID == obj.ID) + .GetAll(x => x.Studio.Id == obj.Id) .Then(x => s.Shows = x), - (Studio s, nameof(Studio.ExternalIDs)) => _SetRelation(s, - ProviderRepository.GetMetadataID(x => x.ResourceID == obj.ID), - (x, y) => x.ExternalIDs = y, - (x, y) => { x.ResourceID = y.ID; }), + (Studio s, nameof(Studio.Movies)) => MovieRepository + .GetAll(x => x.Studio.Id == obj.Id) + .Then(x => s.Movies = x), - (People p, nameof(People.ExternalIDs)) => _SetRelation(p, - ProviderRepository.GetMetadataID(x => x.ResourceID == obj.ID), - (x, y) => x.ExternalIDs = y, - (x, y) => { x.ResourceID = y.ID; }), - (People p, nameof(People.Roles)) => PeopleRepository - .GetFromPeople(obj.ID) + .GetFromPeople(obj.Id) .Then(x => p.Roles = x), - - (Provider p, nameof(Provider.Libraries)) => LibraryRepository - .GetAll(x => x.Providers.Any(y => y.ID == obj.ID)) - .Then(x => p.Libraries = x), - - _ => throw new ArgumentException($"Couldn't find a way to load {memberName} of {obj.Slug}.") }; } - /// - public Task> GetItemsFromLibrary(int id, - Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - return LibraryItemRepository.GetFromLibrary(id, where, sort, limit); - } - - /// - public Task> GetItemsFromLibrary(string slug, - Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - return LibraryItemRepository.GetFromLibrary(slug, where, sort, limit); - } - /// public Task> GetPeopleFromShow(int showID, Expression> where = null, @@ -455,20 +396,6 @@ namespace Kyoo.Core.Controllers return PeopleRepository.GetFromPeople(slug, where, sort, limit); } - /// - public Task AddShowLink(int showID, int? libraryID, int? collectionID) - { - return ShowRepository.AddShowLink(showID, libraryID, collectionID); - } - - /// - public Task AddShowLink(Show show, Library library, Collection collection) - { - if (show == null) - throw new ArgumentNullException(nameof(show)); - return ShowRepository.AddShowLink(show.ID, library?.ID, collection?.ID); - } - /// public Task> GetAll(Expression> where = null, Sort sort = default, @@ -507,10 +434,17 @@ namespace Kyoo.Core.Controllers } /// - public Task Edit(T item, bool resetOld) + public Task Edit(T item) where T : class, IResource { - return GetRepository().Edit(item, resetOld); + return GetRepository().Edit(item); + } + + /// + public Task Patch(int id, Func> patch) + where T : class, IResource + { + return GetRepository().Patch(id, patch); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index 53f8df1b..7b7a1388 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -37,11 +37,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - /// protected override Sort DefaultSort => new Sort.By(nameof(Collection.Name)); @@ -49,22 +44,22 @@ namespace Kyoo.Core.Controllers /// Create a new . /// /// The database handle to use - /// /// A provider repository - public CollectionRepository(DatabaseContext database, IProviderRepository providers) - : base(database) + /// The thumbnail manager used to store images. + public CollectionRepository(DatabaseContext database, IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; - _providers = providers; } /// public override async Task> Search(string query) { - return await Sort( + return (await Sort( _database.Collections .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) .Take(20) - ).ToListAsync(); + ).ToListAsync()) + .Select(SetBackingImageSelf).ToList(); } /// @@ -82,41 +77,13 @@ namespace Kyoo.Core.Controllers { await base.Validate(resource); - if (string.IsNullOrEmpty(resource.Slug)) - throw new ArgumentException("The collection's slug must be set and not empty"); if (string.IsNullOrEmpty(resource.Name)) throw new ArgumentException("The collection's name must be set and not empty"); - - if (resource.ExternalIDs != null) - { - foreach (MetadataID id in resource.ExternalIDs) - { - id.Provider = _database.LocalEntity(id.Provider.Slug) - ?? await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - } - _database.MetadataIds().AttachRange(resource.ExternalIDs); - } - } - - /// - protected override async Task EditRelations(Collection resource, Collection changed, bool resetOld) - { - await Validate(changed); - - if (changed.ExternalIDs != null || resetOld) - { - await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); - resource.ExternalIDs = changed.ExternalIDs; - } } /// public override async Task Delete(Collection obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - _database.Entry(obj).State = EntityState.Deleted; await _database.SaveChangesAsync(); await base.Delete(obj); diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index c3fc9ec8..933c2bb7 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - private readonly IShowRepository _shows; /// @@ -59,20 +54,19 @@ namespace Kyoo.Core.Controllers /// /// The database handle to use. /// A show repository - /// A provider repository + /// The thumbnail manager used to store images. public EpisodeRepository(DatabaseContext database, IShowRepository shows, - IProviderRepository providers) - : base(database) + IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; - _providers = providers; _shows = shows; // Edit episode slugs when the show's slug changes. shows.OnEdited += (show) => { - List episodes = _database.Episodes.AsTracking().Where(x => x.ShowID == show.ID).ToList(); + List episodes = _database.Episodes.AsTracking().Where(x => x.ShowId == show.Id).ToList(); foreach (Episode ep in episodes) { ep.ShowSlug = show.Slug; @@ -85,9 +79,9 @@ namespace Kyoo.Core.Controllers /// public Task GetOrDefault(int showID, int seasonNumber, int episodeNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); + && x.EpisodeNumber == episodeNumber).Then(SetBackingImage); } /// @@ -95,7 +89,7 @@ namespace Kyoo.Core.Controllers { return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); + && x.EpisodeNumber == episodeNumber).Then(SetBackingImage); } /// @@ -119,15 +113,15 @@ namespace Kyoo.Core.Controllers /// public Task GetAbsolute(int showID, int absoluteNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID - && x.AbsoluteNumber == absoluteNumber); + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID + && x.AbsoluteNumber == absoluteNumber).Then(SetBackingImage); } /// public Task GetAbsolute(string showSlug, int absoluteNumber) { return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.AbsoluteNumber == absoluteNumber); + && x.AbsoluteNumber == absoluteNumber).Then(SetBackingImage); } /// @@ -137,80 +131,56 @@ namespace Kyoo.Core.Controllers _database.Episodes .Include(x => x.Show) .Where(x => x.EpisodeNumber != null || x.AbsoluteNumber != null) - .Where(_database.Like(x => x.Title, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) .ToListAsync(); foreach (Episode ep in ret) + { ep.Show.Episodes = null; + SetBackingImage(ep); + } return ret; } /// public override async Task Create(Episode obj) { + obj.ShowSlug = obj.Show?.Slug ?? _database.Shows.First(x => x.Id == obj.ShowId).Slug; await base.Create(obj); - obj.ShowSlug = obj.Show?.Slug ?? _database.Shows.First(x => x.ID == obj.ShowID).Slug; _database.Entry(obj).State = EntityState.Added; await _database.SaveChangesAsync(() => obj.SeasonNumber != null && obj.EpisodeNumber != null - ? Get(obj.ShowID, obj.SeasonNumber.Value, obj.EpisodeNumber.Value) - : GetAbsolute(obj.ShowID, obj.AbsoluteNumber.Value)); + ? Get(obj.ShowId, obj.SeasonNumber.Value, obj.EpisodeNumber.Value) + : GetAbsolute(obj.ShowId, obj.AbsoluteNumber.Value)); OnResourceCreated(obj); return obj; } - /// - protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld) - { - await Validate(changed); - - if (changed.ExternalIDs != null || resetOld) - { - await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); - resource.ExternalIDs = changed.ExternalIDs; - } - } - /// protected override async Task Validate(Episode resource) { await base.Validate(resource); - if (resource.ShowID <= 0) + if (resource.ShowId <= 0) { if (resource.Show == null) { throw new ArgumentException($"Can't store an episode not related " + - $"to any show (showID: {resource.ShowID})."); + $"to any show (showID: {resource.ShowId})."); } - resource.ShowID = resource.Show.ID; - } - - if (resource.ExternalIDs != null) - { - foreach (MetadataID id in resource.ExternalIDs) - { - id.Provider = _database.LocalEntity(id.Provider.Slug) - ?? await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - } - _database.MetadataIds().AttachRange(resource.ExternalIDs); + resource.ShowId = resource.Show.Id; } } /// public override async Task Delete(Episode obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - int epCount = await _database.Episodes.Where(x => x.ShowID == obj.ShowID).Take(2).CountAsync(); + int epCount = await _database.Episodes.Where(x => x.ShowId == obj.ShowId).Take(2).CountAsync(); _database.Entry(obj).State = EntityState.Deleted; - obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); await base.Delete(obj); if (epCount == 1) - await _shows.Delete(obj.ShowID); + await _shows.Delete(obj.ShowId); } } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/GenreRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/GenreRepository.cs deleted file mode 100644 index 73c05a68..00000000 --- a/back/src/Kyoo.Core/Controllers/Repositories/GenreRepository.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Postgresql; -using Kyoo.Utils; -using Microsoft.EntityFrameworkCore; - -namespace Kyoo.Core.Controllers -{ - /// - /// A local repository for genres. - /// - public class GenreRepository : LocalRepository, IGenreRepository - { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - - /// - protected override Sort DefaultSort => new Sort.By(x => x.Slug); - - /// - /// Create a new . - /// - /// The database handle - public GenreRepository(DatabaseContext database) - : base(database) - { - _database = database; - } - - /// - public override async Task> Search(string query) - { - return await Sort( - _database.Genres - .Where(_database.Like(x => x.Name, $"%{query}%")) - ) - .Take(20) - .ToListAsync(); - } - - /// - protected override async Task Validate(Genre resource) - { - resource.Slug ??= Utility.ToSlug(resource.Name); - await base.Validate(resource); - } - - /// - public override async Task Create(Genre obj) - { - await base.Create(obj); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.Slug)); - OnResourceCreated(obj); - return obj; - } - - /// - public override async Task Delete(Genre obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - _database.Entry(obj).State = EntityState.Deleted; - await _database.SaveChangesAsync(); - await base.Delete(obj); - } - } -} diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index a9b15fb5..0b5da1e7 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -23,8 +23,8 @@ using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Postgresql; +using Kyoo.Utils; using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers @@ -32,84 +32,90 @@ namespace Kyoo.Core.Controllers /// /// A local repository to handle library items. /// - public class LibraryItemRepository : LocalRepository, ILibraryItemRepository + public class LibraryItemRepository : LocalRepository, ILibraryItemRepository { /// /// The database handle /// private readonly DatabaseContext _database; - /// - /// A lazy loaded library repository to validate queries (check if a library does exist) - /// - private readonly Lazy _libraries; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Title); + protected override Sort DefaultSort => new Sort.By(x => x.Name); /// - /// Create a new . + /// Create a new . /// /// The database instance - /// A lazy loaded library repository - public LibraryItemRepository(DatabaseContext database, - Lazy libraries) - : base(database) + /// The thumbnail manager used to store images. + public LibraryItemRepository(DatabaseContext database, IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; - _libraries = libraries; } /// - public override Task GetOrDefault(int id) + public override async Task GetOrDefault(int id) { - return _database.LibraryItems.FirstOrDefaultAsync(x => x.ID == id); + return await _database.LibraryItems.SingleOrDefaultAsync(x => x.Id == id).Then(SetBackingImage); } /// - public override Task GetOrDefault(string slug) + public override async Task GetOrDefault(string slug) { - return _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug); + return await _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug).Then(SetBackingImage); } /// - public override Task> GetAll(Expression> where = null, - Sort sort = default, + public override async Task GetOrDefault(Expression> where, Sort sortBy = default) + { + return await Sort(_database.LibraryItems, sortBy).FirstOrDefaultAsync(where).Then(SetBackingImage); + } + + /// + public override async Task> GetAll(Expression> where = null, + Sort sort = default, Pagination limit = default) { - return ApplyFilters(_database.LibraryItems, where, sort, limit); + return (await ApplyFilters(_database.LibraryItems, where, sort, limit)) + .Select(SetBackingImageSelf).ToList(); } /// - public override Task GetCount(Expression> where = null) + public override Task GetCount(Expression> where = null) { - IQueryable query = _database.LibraryItems; + IQueryable query = _database.LibraryItems; if (where != null) query = query.Where(where); return query.CountAsync(); } /// - public override async Task> Search(string query) + public override async Task> Search(string query) { - return await Sort( - _database.LibraryItems - .Where(_database.Like(x => x.Title, $"%{query}%")) + return (await Sort( + _database.LibraryItems + .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) - .ToListAsync(); + .ToListAsync()) + .Select(SetBackingImageSelf) + .ToList(); } /// - public override Task Create(LibraryItem obj) + public override Task Create(ILibraryItem obj) => throw new InvalidOperationException(); /// - public override Task CreateIfNotExists(LibraryItem obj) + public override Task CreateIfNotExists(ILibraryItem obj) => throw new InvalidOperationException(); /// - public override Task Edit(LibraryItem obj, bool resetOld) + public override Task Edit(ILibraryItem edited) + => throw new InvalidOperationException(); + + /// + public override Task Patch(int id, Func> patch) => throw new InvalidOperationException(); /// @@ -121,58 +127,7 @@ namespace Kyoo.Core.Controllers => throw new InvalidOperationException(); /// - public override Task Delete(LibraryItem obj) + public override Task Delete(ILibraryItem obj) => throw new InvalidOperationException(); - - /// - /// Get a basic queryable for a library with the right mapping from shows and collections. - /// Shows contained in a collection are excluded. - /// - /// Only items that are part of a library that match this predicate will be returned. - /// A queryable containing items that are part of a library matching the selector. - private IQueryable _LibraryRelatedQuery(Expression> selector) - => _database.Libraries - .Where(selector) - .SelectMany(x => x.Shows) - .Where(x => !x.Collections.Any()) - .Select(LibraryItem.FromShow) - .Concat(_database.Libraries - .Where(selector) - .SelectMany(x => x.Collections) - .Select(LibraryItem.FromCollection)); - - /// - public async Task> GetFromLibrary(int id, - Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - ICollection items = await ApplyFilters( - _LibraryRelatedQuery(x => x.ID == id), - where, - sort, - limit - ); - if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null) - throw new ItemNotFoundException(); - return items; - } - - /// - public async Task> GetFromLibrary(string slug, - Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - ICollection items = await ApplyFilters( - _LibraryRelatedQuery(x => x.Slug == slug), - where, - sort, - limit - ); - if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null) - throw new ItemNotFoundException(); - return items; - } } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryRepository.cs deleted file mode 100644 index dfc76442..00000000 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryRepository.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Postgresql; -using Kyoo.Utils; -using Microsoft.EntityFrameworkCore; - -namespace Kyoo.Core.Controllers -{ - /// - /// A local repository to handle libraries. - /// - public class LibraryRepository : LocalRepository, ILibraryRepository - { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - - /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - - /// - protected override Sort DefaultSort => new Sort.By(x => x.ID); - - /// - /// Create a new instance. - /// - /// The database handle - /// The provider repository - public LibraryRepository(DatabaseContext database, IProviderRepository providers) - : base(database) - { - _database = database; - _providers = providers; - } - - /// - public override async Task> Search(string query) - { - return await Sort( - _database.Libraries - .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) - ) - .Take(20) - .ToListAsync(); - } - - /// - public override async Task Create(Library obj) - { - await base.Create(obj); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.Slug)); - OnResourceCreated(obj); - return obj; - } - - /// - protected override async Task Validate(Library resource) - { - await base.Validate(resource); - - if (string.IsNullOrEmpty(resource.Slug)) - throw new ArgumentException("The library's slug must be set and not empty"); - if (string.IsNullOrEmpty(resource.Name)) - throw new ArgumentException("The library's name must be set and not empty"); - if (resource.Paths == null || !resource.Paths.Any()) - throw new ArgumentException("The library should have a least one path."); - - if (resource.Providers != null) - { - resource.Providers = await resource.Providers - .SelectAsync(async x => - _database.LocalEntity(x.Slug) - ?? await _providers.CreateIfNotExists(x) - ) - .ToListAsync(); - _database.AttachRange(resource.Providers); - } - } - - /// - protected override async Task EditRelations(Library resource, Library changed, bool resetOld) - { - await Validate(changed); - - if (changed.Providers != null || resetOld) - { - await Database.Entry(resource).Collection(x => x.Providers).LoadAsync(); - resource.Providers = changed.Providers; - } - } - - /// - public override async Task Delete(Library obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - _database.Entry(obj).State = EntityState.Deleted; - await _database.SaveChangesAsync(); - await base.Delete(obj); - } - } -} diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index 85c3b6a3..81f2f57b 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -45,6 +45,11 @@ namespace Kyoo.Core.Controllers /// protected DbContext Database { get; } + /// + /// The thumbnail manager used to store images. + /// + private readonly IThumbnailsManager _thumbs; + /// /// The default sort order that will be used for this resource's type. /// @@ -54,9 +59,11 @@ namespace Kyoo.Core.Controllers /// Create a new base with the given database handle. /// /// A database connection to load resources of type - protected LocalRepository(DbContext database) + /// The thumbnail manager used to store images. + protected LocalRepository(DbContext database, IThumbnailsManager thumbs) { Database = database; + _thumbs = thumbs; } /// @@ -112,7 +119,7 @@ namespace Kyoo.Core.Controllers throw new SwitchExpressionException(); } } - return _Sort(query, sortBy, false).ThenBy(x => x.ID); + return _Sort(query, sortBy, false).ThenBy(x => x.Id); } private static Func _GetComparisonExpression( @@ -144,7 +151,7 @@ namespace Kyoo.Core.Controllers /// The reference item (the AfterID query) /// True if the following page should be returned, false for the previous. /// An expression ready to be added to a Where close of a sorted query to handle the AfterID - protected Expression> KeysetPaginatate( + protected Expression> KeysetPaginate( Sort sort, T reference, bool next = true) @@ -155,20 +162,20 @@ namespace Kyoo.Core.Controllers ParameterExpression x = Expression.Parameter(typeof(T), "x"); ConstantExpression referenceC = Expression.Constant(reference, typeof(T)); - IEnumerable.By> _GetSortsBy(Sort sort) + IEnumerable.By> GetSortsBy(Sort sort) { return sort switch { - Sort.Default => _GetSortsBy(DefaultSort), + Sort.Default => GetSortsBy(DefaultSort), Sort.By @sortBy => new[] { sortBy }, - Sort.Conglomerate(var list) => list.SelectMany(_GetSortsBy), + Sort.Conglomerate(var list) => list.SelectMany(GetSortsBy), _ => Array.Empty.By>(), }; } - // Don't forget that every sorts must end with a ID sort (to differenciate equalities). - Sort.By id = new(x => x.ID); - IEnumerable.By> sorts = _GetSortsBy(sort).Append(id); + // Don't forget that every sorts must end with a ID sort (to differentiate equalities). + Sort.By id = new(x => x.Id); + IEnumerable.By> sorts = GetSortsBy(sort).Append(id); BinaryExpression filter = null; List.By> previousSteps = new(); @@ -180,9 +187,9 @@ namespace Kyoo.Core.Controllers PropertyInfo property = typeof(T).GetProperty(key); // Comparing a value with null always return false so we short opt < > comparisons with null. - if (property.GetValue(reference) == null) + if (property!.GetValue(reference) == null) { - previousSteps.Add(new(key, desc)); + previousSteps.Add(new Sort.By(key, desc)); continue; } @@ -206,7 +213,7 @@ namespace Kyoo.Core.Controllers // Comparing a value with null always return false for nulls so we must add nulls to the results manually. // Postgres sorts them after values so we will do the same - // We only add this condition if the collumn type is nullable + // We only add this condition if the column type is nullable if (Nullable.GetUnderlyingType(property.PropertyType) != null) { BinaryExpression equalNull = Expression.Equal(xkey, Expression.Constant(null)); @@ -223,7 +230,29 @@ namespace Kyoo.Core.Controllers previousSteps.Add(new(key, desc)); } - return Expression.Lambda>(filter, x); + return Expression.Lambda>(filter!, x); + } + + protected void SetBackingImage(T obj) + { + if (obj is not IThumbnails thumbs) + return; + string type = obj is ILibraryItem item + ? item.Kind.ToString().ToLowerInvariant() + : typeof(T).Name.ToLowerInvariant(); + + if (thumbs.Poster != null) + thumbs.Poster.Path = $"/{type}/{obj.Slug}/poster"; + if (thumbs.Thumbnail != null) + thumbs.Thumbnail.Path = $"/{type}/{obj.Slug}/thumbnail"; + if (thumbs.Logo != null) + thumbs.Logo.Path = $"/{type}/{obj.Slug}/logo"; + } + + protected T SetBackingImageSelf(T obj) + { + SetBackingImage(obj); + return obj; } /// @@ -234,7 +263,8 @@ namespace Kyoo.Core.Controllers /// The tracked resource with the given ID protected virtual async Task GetWithTracking(int id) { - T ret = await Database.Set().AsTracking().FirstOrDefaultAsync(x => x.ID == id); + T ret = await Database.Set().AsTracking().FirstOrDefaultAsync(x => x.Id == id); + SetBackingImage(ret); if (ret == null) throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}"); return ret; @@ -270,30 +300,31 @@ namespace Kyoo.Core.Controllers /// public virtual Task GetOrDefault(int id) { - return Database.Set().FirstOrDefaultAsync(x => x.ID == id); + return Database.Set().FirstOrDefaultAsync(x => x.Id == id).Then(SetBackingImage); } /// public virtual Task GetOrDefault(string slug) { - return Database.Set().FirstOrDefaultAsync(x => x.Slug == slug); + return Database.Set().FirstOrDefaultAsync(x => x.Slug == slug).Then(SetBackingImage); } /// public virtual Task GetOrDefault(Expression> where, Sort sortBy = default) { - return Sort(Database.Set(), sortBy).FirstOrDefaultAsync(where); + return Sort(Database.Set(), sortBy).FirstOrDefaultAsync(where).Then(SetBackingImage); } /// public abstract Task> Search(string query); /// - public virtual Task> GetAll(Expression> where = null, + public virtual async Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) { - return ApplyFilters(Database.Set(), where, sort, limit); + return (await ApplyFilters(Database.Set(), where, sort, limit)) + .Select(SetBackingImageSelf).ToList(); } /// @@ -316,7 +347,7 @@ namespace Kyoo.Core.Controllers if (limit?.AfterID != null) { T reference = await Get(limit.AfterID.Value); - query = query.Where(KeysetPaginatate(sort, reference, !limit.Reverse)); + query = query.Where(KeysetPaginate(sort, reference, !limit.Reverse)); } if (limit?.Reverse == true) query = query.Reverse(); @@ -338,9 +369,18 @@ namespace Kyoo.Core.Controllers /// public virtual async Task Create(T obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); await Validate(obj); + if (obj is IThumbnails thumbs) + { + await _thumbs.DownloadImages(thumbs); + if (thumbs.Poster != null) + Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry.State = EntityState.Added; + if (thumbs.Thumbnail != null) + Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry.State = EntityState.Added; + if (thumbs.Logo != null) + Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry.State = EntityState.Added; + } + SetBackingImage(obj); return obj; } @@ -367,9 +407,6 @@ namespace Kyoo.Core.Controllers { try { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - T old = await GetOrDefault(obj.Slug); if (old != null) return old; @@ -383,23 +420,19 @@ namespace Kyoo.Core.Controllers } /// - public virtual async Task Edit(T edited, bool resetOld) + public virtual async Task Edit(T edited) { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; Database.ChangeTracker.LazyLoadingEnabled = false; try { - T old = await GetWithTracking(edited.ID); + T old = await GetWithTracking(edited.Id); - if (resetOld) - old = Merger.Nullify(old); Merger.Complete(old, edited, x => x.GetCustomAttribute() == null); - await EditRelations(old, edited, resetOld); + await EditRelations(old, edited); await Database.SaveChangesAsync(); OnEdited?.Invoke(old); + SetBackingImage(old); return old; } finally @@ -409,6 +442,30 @@ namespace Kyoo.Core.Controllers } } + /// + public virtual async Task Patch(int id, Func> patch) + { + bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; + Database.ChangeTracker.LazyLoadingEnabled = false; + try + { + T resource = await GetWithTracking(id); + + if (!await patch(resource)) + throw new ArgumentException("Could not patch resource"); + + await Database.SaveChangesAsync(); + OnEdited?.Invoke(resource); + SetBackingImage(resource); + return resource; + } + finally + { + Database.ChangeTracker.LazyLoadingEnabled = lazyLoading; + Database.ChangeTracker.Clear(); + } + } + /// /// An overridable method to edit relation of a resource. /// @@ -419,12 +476,15 @@ namespace Kyoo.Core.Controllers /// The new version of . /// This item will be saved on the database and replace /// - /// - /// A boolean to indicate if all values of resource should be discarded or not. - /// /// A representing the asynchronous operation. - protected virtual Task EditRelations(T resource, T changed, bool resetOld) + protected virtual Task EditRelations(T resource, T changed) { + if (resource is IThumbnails thumbs && changed is IThumbnails chng) + { + Database.Entry(thumbs).Reference(x => x.Poster).IsModified = thumbs.Poster != chng.Poster; + Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = thumbs.Thumbnail != chng.Thumbnail; + Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != chng.Logo; + } return Validate(resource); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs new file mode 100644 index 00000000..02d0ba84 --- /dev/null +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -0,0 +1,142 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Postgresql; +using Microsoft.EntityFrameworkCore; + +namespace Kyoo.Core.Controllers +{ + /// + /// A local repository to handle shows + /// + public class MovieRepository : LocalRepository, IMovieRepository + { + /// + /// The database handle + /// + private readonly DatabaseContext _database; + + /// + /// A studio repository to handle creation/validation of related studios. + /// + private readonly IStudioRepository _studios; + + /// + /// A people repository to handle creation/validation of related people. + /// + private readonly IPeopleRepository _people; + + /// + protected override Sort DefaultSort => new Sort.By(x => x.Name); + + /// + /// Create a new . + /// + /// The database handle to use + /// A studio repository + /// A people repository + /// The thumbnail manager used to store images. + public MovieRepository(DatabaseContext database, + IStudioRepository studios, + IPeopleRepository people, + IThumbnailsManager thumbs) + : base(database, thumbs) + { + _database = database; + _studios = studios; + _people = people; + } + + /// + public override async Task> Search(string query) + { + query = $"%{query}%"; + return (await Sort( + _database.Movies + .Where(_database.Like(x => x.Name + " " + x.Slug, query)) + ) + .Take(20) + .ToListAsync()) + .Select(SetBackingImageSelf) + .ToList(); + } + + /// + public override async Task Create(Movie obj) + { + await base.Create(obj); + _database.Entry(obj).State = EntityState.Added; + await _database.SaveChangesAsync(() => Get(obj.Slug)); + OnResourceCreated(obj); + return obj; + } + + /// + protected override async Task Validate(Movie resource) + { + await base.Validate(resource); + if (resource.Studio != null) + { + resource.Studio = await _studios.CreateIfNotExists(resource.Studio); + resource.StudioID = resource.Studio.Id; + } + + if (resource.People != null) + { + foreach (PeopleRole role in resource.People) + { + role.People = _database.LocalEntity(role.People.Slug) + ?? await _people.CreateIfNotExists(role.People); + role.PeopleID = role.People.Id; + _database.Entry(role).State = EntityState.Added; + } + } + } + + /// + protected override async Task EditRelations(Movie resource, Movie changed) + { + await Validate(changed); + + if (changed.Studio != null || changed.StudioID == null) + { + await Database.Entry(resource).Reference(x => x.Studio).LoadAsync(); + resource.Studio = changed.Studio; + } + + if (changed.People != null) + { + await Database.Entry(resource).Collection(x => x.People).LoadAsync(); + resource.People = changed.People; + } + } + + /// + public override async Task Delete(Movie obj) + { + _database.Remove(obj); + await _database.SaveChangesAsync(); + await base.Delete(obj); + } + } +} diff --git a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs index bd631ed1..7c53b609 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs @@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - /// /// A lazy loaded show repository to validate requests from shows. /// @@ -56,27 +51,28 @@ namespace Kyoo.Core.Controllers /// Create a new /// /// The database handle - /// A provider repository /// A lazy loaded show repository + /// The thumbnail manager used to store images. public PeopleRepository(DatabaseContext database, - IProviderRepository providers, - Lazy shows) - : base(database) + Lazy shows, + IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; - _providers = providers; _shows = shows; } /// public override async Task> Search(string query) { - return await Sort( + return (await Sort( _database.People .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) - .ToListAsync(); + .ToListAsync()) + .Select(SetBackingImageSelf) + .ToList(); } /// @@ -94,55 +90,34 @@ namespace Kyoo.Core.Controllers { await base.Validate(resource); - if (resource.ExternalIDs != null) - { - foreach (MetadataID id in resource.ExternalIDs) - { - id.Provider = _database.LocalEntity(id.Provider.Slug) - ?? await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - } - _database.MetadataIds().AttachRange(resource.ExternalIDs); - } - if (resource.Roles != null) { foreach (PeopleRole role in resource.Roles) { role.Show = _database.LocalEntity(role.Show.Slug) ?? await _shows.Value.CreateIfNotExists(role.Show); - role.ShowID = role.Show.ID; + role.ShowID = role.Show.Id; _database.Entry(role).State = EntityState.Added; } } } /// - protected override async Task EditRelations(People resource, People changed, bool resetOld) + protected override async Task EditRelations(People resource, People changed) { await Validate(changed); - if (changed.Roles != null || resetOld) + if (changed.Roles != null) { await Database.Entry(resource).Collection(x => x.Roles).LoadAsync(); resource.Roles = changed.Roles; } - - if (changed.ExternalIDs != null || resetOld) - { - await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); - resource.ExternalIDs = changed.ExternalIDs; - } } /// public override async Task Delete(People obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - _database.Entry(obj).State = EntityState.Deleted; - obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); obj.Roles.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); await base.Delete(obj); diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ProviderRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ProviderRepository.cs deleted file mode 100644 index 7435e0fa..00000000 --- a/back/src/Kyoo.Core/Controllers/Repositories/ProviderRepository.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Postgresql; -using Microsoft.EntityFrameworkCore; - -namespace Kyoo.Core.Controllers -{ - /// - /// A local repository to handle providers. - /// - public class ProviderRepository : LocalRepository, IProviderRepository - { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - - /// - /// Create a new . - /// - /// The database handle - public ProviderRepository(DatabaseContext database) - : base(database) - { - _database = database; - } - - /// - protected override Sort DefaultSort => new Sort.By(x => x.Slug); - - /// - public override async Task> Search(string query) - { - return await Sort( - _database.Providers - .Where(_database.Like(x => x.Name, $"%{query}%")) - ) - .Take(20) - .ToListAsync(); - } - - /// - public override async Task Create(Provider obj) - { - await base.Create(obj); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.Slug)); - OnResourceCreated(obj); - return obj; - } - - /// - public override async Task Delete(Provider obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - _database.Entry(obj).State = EntityState.Deleted; - await _database.SaveChangesAsync(); - await base.Delete(obj); - } - - /// - public async Task> GetMetadataID(Expression> where = null, - Sort sort = default, - Pagination limit = default) - where T : class, IMetadata - { - return await _database.MetadataIds() - .Include(y => y.Provider) - .Where(where) - .ToListAsync(); - } - } -} diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index de070572..801b4876 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -24,6 +24,7 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Postgresql; +using Kyoo.Utils; using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers @@ -38,11 +39,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - /// protected override Sort DefaultSort => new Sort.By(x => x.SeasonNumber); @@ -51,19 +47,18 @@ namespace Kyoo.Core.Controllers /// /// The database handle that will be used /// A shows repository - /// A provider repository + /// The thumbnail manager used to store images. public SeasonRepository(DatabaseContext database, IShowRepository shows, - IProviderRepository providers) - : base(database) + IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; - _providers = providers; // Edit seasons slugs when the show's slug changes. shows.OnEdited += (show) => { - List seasons = _database.Seasons.AsTracking().Where(x => x.ShowID == show.ID).ToList(); + List seasons = _database.Seasons.AsTracking().Where(x => x.ShowId == show.Id).ToList(); foreach (Season season in seasons) { season.ShowSlug = show.Slug; @@ -94,35 +89,37 @@ namespace Kyoo.Core.Controllers /// public Task GetOrDefault(int showID, int seasonNumber) { - return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber); + return _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == showID + && x.SeasonNumber == seasonNumber).Then(SetBackingImage); } /// public Task GetOrDefault(string showSlug, int seasonNumber) { return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.SeasonNumber == seasonNumber); + && x.SeasonNumber == seasonNumber).Then(SetBackingImage); } /// public override async Task> Search(string query) { - return await Sort( + return (await Sort( _database.Seasons - .Where(_database.Like(x => x.Title, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) - .ToListAsync(); + .ToListAsync()) + .Select(SetBackingImageSelf) + .ToList(); } /// public override async Task Create(Season obj) { await base.Create(obj); - obj.ShowSlug = _database.Shows.First(x => x.ID == obj.ShowID).Slug; + obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowId).Slug; _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.ShowID, obj.SeasonNumber)); + await _database.SaveChangesAsync(() => Get(obj.ShowId, obj.SeasonNumber)); OnResourceCreated(obj); return obj; } @@ -131,46 +128,20 @@ namespace Kyoo.Core.Controllers protected override async Task Validate(Season resource) { await base.Validate(resource); - if (resource.ShowID <= 0) + if (resource.ShowId <= 0) { if (resource.Show == null) { throw new ArgumentException($"Can't store a season not related to any show " + - $"(showID: {resource.ShowID})."); + $"(showID: {resource.ShowId})."); } - resource.ShowID = resource.Show.ID; - } - - if (resource.ExternalIDs != null) - { - foreach (MetadataID id in resource.ExternalIDs) - { - id.Provider = _database.LocalEntity(id.Provider.Slug) - ?? await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - } - _database.MetadataIds().AttachRange(resource.ExternalIDs); - } - } - - /// - protected override async Task EditRelations(Season resource, Season changed, bool resetOld) - { - await Validate(changed); - - if (changed.ExternalIDs != null || resetOld) - { - await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); - resource.ExternalIDs = changed.ExternalIDs; + resource.ShowId = resource.Show.Id; } } /// public override async Task Delete(Season obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - _database.Remove(obj); await _database.SaveChangesAsync(); await base.Delete(obj); diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index e9ea389e..11887286 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -47,18 +47,8 @@ namespace Kyoo.Core.Controllers /// private readonly IPeopleRepository _people; - /// - /// A genres repository to handle creation/validation of related genres. - /// - private readonly IGenreRepository _genres; - - /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Title); + protected override Sort DefaultSort => new Sort.By(x => x.Name); /// /// Create a new . @@ -66,32 +56,30 @@ namespace Kyoo.Core.Controllers /// The database handle to use /// A studio repository /// A people repository - /// A genres repository - /// A provider repository + /// The thumbnail manager used to store images. public ShowRepository(DatabaseContext database, IStudioRepository studios, IPeopleRepository people, - IGenreRepository genres, - IProviderRepository providers) - : base(database) + IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; _studios = studios; _people = people; - _genres = genres; - _providers = providers; } /// public override async Task> Search(string query) { query = $"%{query}%"; - return await Sort( + return (await Sort( _database.Shows - .Where(_database.Like(x => x.Title + " " + x.Slug, query)) + .Where(_database.Like(x => x.Name + " " + x.Slug, query)) ) .Take(20) - .ToListAsync(); + .ToListAsync()) + .Select(SetBackingImageSelf) + .ToList(); } /// @@ -107,32 +95,13 @@ namespace Kyoo.Core.Controllers /// protected override async Task Validate(Show resource) { - resource.Slug ??= Utility.ToSlug(resource.Title); + resource.Slug ??= Utility.ToSlug(resource.Name); await base.Validate(resource); if (resource.Studio != null) { resource.Studio = await _studios.CreateIfNotExists(resource.Studio); - resource.StudioID = resource.Studio.ID; - } - - if (resource.Genres != null) - { - resource.Genres = await resource.Genres - .SelectAsync(x => _genres.CreateIfNotExists(x)) - .ToListAsync(); - _database.AttachRange(resource.Genres); - } - - if (resource.ExternalIDs != null) - { - foreach (MetadataID id in resource.ExternalIDs) - { - id.Provider = _database.LocalEntity(id.Provider.Slug) - ?? await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - } - _database.MetadataIds().AttachRange(resource.ExternalIDs); + resource.StudioId = resource.Studio.Id; } if (resource.People != null) @@ -141,70 +110,34 @@ namespace Kyoo.Core.Controllers { role.People = _database.LocalEntity(role.People.Slug) ?? await _people.CreateIfNotExists(role.People); - role.PeopleID = role.People.ID; + role.PeopleID = role.People.Id; _database.Entry(role).State = EntityState.Added; } } } /// - protected override async Task EditRelations(Show resource, Show changed, bool resetOld) + protected override async Task EditRelations(Show resource, Show changed) { await Validate(changed); - if (changed.Aliases != null || resetOld) - resource.Aliases = changed.Aliases; - - if (changed.Studio != null || resetOld) + if (changed.Studio != null || changed.StudioId == null) { 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.Genres).LoadAsync(); - resource.Genres = changed.Genres; - } - - if (changed.People != null || resetOld) + if (changed.People != null) { await Database.Entry(resource).Collection(x => x.People).LoadAsync(); resource.People = changed.People; } - - if (changed.ExternalIDs != null || resetOld) - { - await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); - resource.ExternalIDs = changed.ExternalIDs; - } - } - - /// - public async Task AddShowLink(int showID, int? libraryID, int? collectionID) - { - if (collectionID != null) - { - await _database.AddLinks(collectionID.Value, showID); - await _database.SaveIfNoDuplicates(); - - if (libraryID != null) - { - await _database.AddLinks(libraryID.Value, collectionID.Value); - await _database.SaveIfNoDuplicates(); - } - } - if (libraryID != null) - { - await _database.AddLinks(libraryID.Value, showID); - await _database.SaveIfNoDuplicates(); - } } /// public Task GetSlug(int showID) { - return _database.Shows.Where(x => x.ID == showID) + return _database.Shows.Where(x => x.Id == showID) .Select(x => x.Slug) .FirstOrDefaultAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs index 138c7ae1..21b16576 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -38,11 +37,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - /// protected override Sort DefaultSort => new Sort.By(x => x.Name); @@ -50,23 +44,24 @@ namespace Kyoo.Core.Controllers /// Create a new . /// /// The database handle - /// A provider repository - public StudioRepository(DatabaseContext database, IProviderRepository providers) - : base(database) + /// The thumbnail manager used to store images. + public StudioRepository(DatabaseContext database, IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; - _providers = providers; } /// public override async Task> Search(string query) { - return await Sort( + return (await Sort( _database.Studios .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) - .ToListAsync(); + .ToListAsync()) + .Select(SetBackingImageSelf) + .ToList(); } /// @@ -83,38 +78,12 @@ namespace Kyoo.Core.Controllers protected override async Task Validate(Studio resource) { resource.Slug ??= Utility.ToSlug(resource.Name); - await base.Validate(resource); - if (resource.ExternalIDs != null) - { - foreach (MetadataID id in resource.ExternalIDs) - { - id.Provider = _database.LocalEntity(id.Provider.Slug) - ?? await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - } - _database.MetadataIds().AttachRange(resource.ExternalIDs); - } - } - - /// - protected override async Task EditRelations(Studio resource, Studio changed, bool resetOld) - { - if (changed.ExternalIDs != null || resetOld) - { - await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); - resource.ExternalIDs = changed.ExternalIDs; - } - - await base.EditRelations(resource, changed, resetOld); } /// public override async Task Delete(Studio obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - _database.Entry(obj).State = EntityState.Deleted; await _database.SaveChangesAsync(); await base.Delete(obj); diff --git a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs index 89c00fdd..6e4ea323 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -44,8 +43,9 @@ namespace Kyoo.Core.Controllers /// Create a new /// /// The database handle to use - public UserRepository(DatabaseContext database) - : base(database) + /// The thumbnail manager used to store images. + public UserRepository(DatabaseContext database, IThumbnailsManager thumbs) + : base(database, thumbs) { _database = database; } @@ -53,12 +53,14 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - return await Sort( + return (await Sort( _database.Users .Where(_database.Like(x => x.Username, $"%{query}%")) ) .Take(20) - .ToListAsync(); + .ToListAsync()) + .Select(SetBackingImageSelf) + .ToList(); } /// @@ -66,6 +68,8 @@ namespace Kyoo.Core.Controllers { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; + if (obj.Logo != null) + _database.Entry(obj).Reference(x => x.Logo).TargetEntry.State = EntityState.Added; await _database.SaveChangesAsync(() => Get(obj.Slug)); OnResourceCreated(obj); return obj; @@ -74,9 +78,6 @@ namespace Kyoo.Core.Controllers /// public override async Task Delete(User obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - _database.Entry(obj).State = EntityState.Deleted; await _database.SaveChangesAsync(); await base.Delete(obj); diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index 15ba7def..f2ecf5a2 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -18,13 +18,13 @@ using System; using System.IO; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using Blurhash.SkiaSharp; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; -using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Logging; +using SkiaSharp; #nullable enable @@ -54,105 +54,77 @@ namespace Kyoo.Core.Controllers _logger = logger; } - /// - /// An helper function to download an image. - /// - /// The distant url of the image - /// The local path of the image - /// What is currently downloaded (used for errors) - /// true if an image has been downloaded, false otherwise. - private async Task _DownloadImage(string url, string localPath, string what) + private static async Task _WriteTo(SKBitmap bitmap, string path, int quality) { - if (url == localPath) - return false; + SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, quality); + await using Stream reader = data.AsStream(); + await using Stream file = File.Create(path); + await reader.CopyToAsync(file); + } + private async Task _DownloadImage(Image? image, string localPath, string what) + { + if (image == null) + return; try { _logger.LogInformation("Downloading image {What}", what); HttpClient client = _clientFactory.CreateClient(); - HttpResponseMessage response = await client.GetAsync(url); + HttpResponseMessage response = await client.GetAsync(image.Source); response.EnsureSuccessStatusCode(); - string mime = response.Content.Headers.ContentType?.MediaType!; await using Stream reader = await response.Content.ReadAsStreamAsync(); + using SKCodec codec = SKCodec.Create(reader); + SKImageInfo info = codec.Info; + info.ColorType = SKColorType.Rgba8888; + using SKBitmap original = SKBitmap.Decode(codec, info); - string extension = new FileExtensionContentTypeProvider() - .Mappings.FirstOrDefault(x => x.Value == mime) - .Key; - await using Stream local = File.Create(localPath + extension); - await reader.CopyToAsync(local); - return true; + using SKBitmap high = original.Resize(new SKSizeI(original.Width, original.Height), SKFilterQuality.High); + await _WriteTo(original, $"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp", 80); + + using SKBitmap medium = high.Resize(new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)), SKFilterQuality.Medium); + await _WriteTo(medium, $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp", 55); + + using SKBitmap low = medium.Resize(new SKSizeI(original.Width / 2, original.Height / 2), SKFilterQuality.Low); + await _WriteTo(low, $"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp", 25); + + image.Blurhash = Blurhasher.Encode(low, 4, 3); } catch (Exception ex) { _logger.LogError(ex, "{What} could not be downloaded", what); - return false; } } /// - public async Task DownloadImages(T item, bool alwaysDownload = false) + public async Task DownloadImages(T item) where T : IThumbnails { if (item == null) throw new ArgumentNullException(nameof(item)); - if (item.Images == null) - return false; - string name = item is IResource res ? res.Slug : "???"; - bool ret = false; - - foreach ((int id, string image) in item.Images.Where(x => x.Value != null)) - { - string localPath = _GetPrivateImagePath(item, id); - if (alwaysDownload || !Path.Exists(localPath)) - ret |= await _DownloadImage(image, localPath, $"The image n {id} of {name}"); - } - - return ret; + await _DownloadImage(item.Poster, _GetBaseImagePath(item, "poster"), $"The poster of {name}"); + await _DownloadImage(item.Thumbnail, _GetBaseImagePath(item, "thumbnail"), $"The poster of {name}"); + await _DownloadImage(item.Logo, _GetBaseImagePath(item, "logo"), $"The poster of {name}"); } - /// - /// Retrieve the local path of an image of the given item without an extension. - /// - /// The item to retrieve the poster from. - /// The ID of the image. See for values. - /// The type of the item - /// The path of the image for the given resource, even if it does not exists - private static string _GetPrivateImagePath(T item, int imageId) + private static string _GetBaseImagePath(T item, string image) { - if (item == null) - throw new ArgumentNullException(nameof(item)); - string directory = item switch { - IResource res => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant(), res.Slug), + IResource res => Path.Combine("./metadata", item.GetType().Name.ToLowerInvariant(), res.Slug), _ => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant()) }; Directory.CreateDirectory(directory); - string imageName = imageId switch - { - Images.Poster => "poster", - Images.Logo => "logo", - Images.Thumbnail => "thumbnail", - Images.Trailer => "trailer", - _ => $"{imageId}" - }; - return Path.Combine(directory, imageName); + return Path.Combine(directory, image); } /// - public string? GetImagePath(T item, int imageId) + public string GetImagePath(T item, string image, ImageQuality quality) where T : IThumbnails { - string basePath = _GetPrivateImagePath(item, imageId); - string directory = Path.GetDirectoryName(basePath)!; - string baseFile = Path.GetFileName(basePath); - if (!Directory.Exists(directory)) - return null; - return Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly) - .FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == baseFile); + return $"{_GetBaseImagePath(item, image)}.{quality.ToString().ToLowerInvariant()}.webp"; } } } diff --git a/back/src/Kyoo.Core/CoreModule.cs b/back/src/Kyoo.Core/CoreModule.cs index 5c55c01b..ff6e9535 100644 --- a/back/src/Kyoo.Core/CoreModule.cs +++ b/back/src/Kyoo.Core/CoreModule.cs @@ -30,6 +30,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using JsonOptions = Kyoo.Core.Api.JsonOptions; namespace Kyoo.Core @@ -48,16 +49,14 @@ namespace Kyoo.Core builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); - builder.RegisterRepository(); builder.RegisterRepository(); builder.RegisterRepository(); + builder.RegisterRepository(); builder.RegisterRepository(); builder.RegisterRepository(); builder.RegisterRepository(); builder.RegisterRepository(); builder.RegisterRepository(); - builder.RegisterRepository(); - builder.RegisterRepository(); builder.RegisterRepository(); } @@ -74,6 +73,7 @@ namespace Kyoo.Core .AddNewtonsoftJson(x => { x.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + x.SerializerSettings.Converters.Add(new StringEnumConverter()); }) .AddDataAnnotations() .AddControllersAsServices() diff --git a/back/src/Kyoo.Core/Kyoo.Core.csproj b/back/src/Kyoo.Core/Kyoo.Core.csproj index 917b6986..b4061a1c 100644 --- a/back/src/Kyoo.Core/Kyoo.Core.csproj +++ b/back/src/Kyoo.Core/Kyoo.Core.csproj @@ -6,9 +6,12 @@ + + + diff --git a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs index cb941c43..a593ac9a 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -16,12 +16,14 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using System; using System.Collections.Generic; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; +using Kyoo.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -161,12 +163,12 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Edit([FromBody] T resource) { - if (resource.ID > 0) - return await Repository.Edit(resource, true); + if (resource.Id > 0) + return await Repository.Edit(resource); T old = await Repository.Get(resource.Slug); - resource.ID = old.ID; - return await Repository.Edit(resource, true); + resource.Id = old.Id; + return await Repository.Edit(resource); } /// @@ -185,14 +187,15 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Patch([FromBody] T resource) + public async Task> Patch([FromBody] PartialResource resource) { - if (resource.ID > 0) - return await Repository.Edit(resource, false); + if (resource.Id.HasValue) + return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync); + if (resource.Slug == null) + throw new ArgumentException("Either the Id or the slug of the resource has to be defined to edit it."); T old = await Repository.Get(resource.Slug); - resource.ID = old.ID; - return await Repository.Edit(resource, false); + return await Repository.Patch(old.Id, TryUpdateModelAsync); } /// diff --git a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 20a9715c..4674b6ff 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.IO; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; @@ -25,7 +24,6 @@ using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.StaticFiles; using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api @@ -59,37 +57,7 @@ namespace Kyoo.Core.Api _thumbs = thumbs; } - /// - /// Get the content type of a file using it's extension. - /// - /// The path of the file - /// The extension of the file is not known. - /// The content type of the file - private static string _GetContentType(string path) - { - FileExtensionContentTypeProvider provider = new(); - if (provider.TryGetContentType(path, out string contentType)) - return contentType; - throw new NotImplementedException($"Can't get the content type of the file at: {path}"); - } - - /// - /// Get image - /// - /// - /// Get an image for the specified item. - /// List of commonly available images:
- /// - Poster: Image 0, also available at /poster
- /// - Thumbnail: Image 1, also available at /thumbnail
- /// - Logo: Image 3, also available at /logo
- ///
- /// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint. - ///
- /// The ID or slug of the resource to get the image for. - /// The number of the image to retrieve. - /// The image asked. - /// No item exist with the specific identifier or the image does not exists on kyoo. - private async Task _GetImage(Identifier identifier, int image) + private async Task _GetImage(Identifier identifier, string image, ImageQuality? quality) { T resource = await identifier.Match( id => Repository.GetOrDefault(id), @@ -97,10 +65,10 @@ namespace Kyoo.Core.Api ); if (resource == null) return NotFound(); - string path = _thumbs.GetImagePath(resource, image); + string path = _thumbs.GetImagePath(resource, image, quality ?? ImageQuality.High); if (path == null || !System.IO.File.Exists(path)) return NotFound(); - return PhysicalFile(Path.GetFullPath(path), _GetContentType(path), true); + return PhysicalFile(Path.GetFullPath(path), "image/webp", true); } /// @@ -110,17 +78,18 @@ namespace Kyoo.Core.Api /// Get the poster for the specified item. /// /// The ID or slug of the resource to get the image for. + /// The quality of the image to retrieve. /// The image asked. /// /// No item exist with the specific identifier or the image does not exists on kyoo. /// - [HttpGet("{identifier:id}/poster", Order = AlternativeRoute)] + [HttpGet("{identifier:id}/poster")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public Task GetPoster(Identifier identifier) + public Task GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality) { - return _GetImage(identifier, Images.Poster); + return _GetImage(identifier, "poster", quality); } /// @@ -130,17 +99,18 @@ namespace Kyoo.Core.Api /// Get the logo for the specified item. /// /// The ID or slug of the resource to get the image for. + /// The quality of the image to retrieve. /// The image asked. /// /// No item exist with the specific identifier or the image does not exists on kyoo. /// - [HttpGet("{identifier:id}/logo", Order = AlternativeRoute)] + [HttpGet("{identifier:id}/logo")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public Task GetLogo(Identifier identifier) + public Task GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality) { - return _GetImage(identifier, Images.Logo); + return _GetImage(identifier, "logo", quality); } /// @@ -150,24 +120,16 @@ namespace Kyoo.Core.Api /// Get the thumbnail for the specified item. /// /// The ID or slug of the resource to get the image for. + /// The quality of the image to retrieve. /// The image asked. /// /// No item exist with the specific identifier or the image does not exists on kyoo. /// + [HttpGet("{identifier:id}/thumbnail")] [HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)] - [HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)] - public Task GetBackdrop(Identifier identifier) + public Task GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality) { - return _GetImage(identifier, Images.Thumbnail); - } - - /// - public override async Task> Create([FromBody] T resource) - { - // TODO: Remove this method and use a websocket API to do that. - ActionResult ret = await base.Create(resource); - await _thumbs.DownloadImages(ret.Value); - return ret; + return _GetImage(identifier, "thumbnail", quality); } } } diff --git a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index a1394b87..a5daeec4 100644 --- a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -16,9 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; -using System.ComponentModel; using System.Reflection; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; @@ -74,78 +72,5 @@ namespace Kyoo.Core.Api property.ShouldDeserialize = _ => false; return property; } - - /// - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) - { - IList properties = base.CreateProperties(type, memberSerialization); - if (!type.IsAssignableTo(typeof(IThumbnails))) - return properties; - foreach ((int id, string image) in Images.ImageName) - { - properties.Add(new JsonProperty - { - DeclaringType = type, - PropertyName = image.ToLower(), - UnderlyingName = image, - PropertyType = typeof(string), - Readable = true, - Writable = false, - ItemIsReference = false, - TypeNameHandling = TypeNameHandling.None, - ShouldSerialize = x => - { - IThumbnails thumb = (IThumbnails)x; - return thumb?.Images?.ContainsKey(id) == true; - }, - ValueProvider = new ThumbnailProvider(id) - }); - } - - return properties; - } - - /// - /// A custom that uses the - /// . as a value. - /// - private class ThumbnailProvider : IValueProvider - { - /// - /// The index/ID of the image to retrieve/set. - /// - private readonly int _imageIndex; - - /// - /// Create a new . - /// - /// The index/ID of the image to retrieve/set. - public ThumbnailProvider(int imageIndex) - { - _imageIndex = imageIndex; - } - - /// - public void SetValue(object target, object value) - { - if (target is not IThumbnails thumb) - throw new ArgumentException($"The given object is not an Thumbnail."); - thumb.Images[_imageIndex] = value as string; - } - - /// - public object GetValue(object target) - { - string slug = (target as IResource)?.Slug ?? (target as ICustomTypeDescriptor)?.GetComponentName(); - if (target is not IThumbnails thumb - || slug == null - || string.IsNullOrEmpty(thumb.Images?.GetValueOrDefault(_imageIndex))) - return null; - string type = target is ICustomTypeDescriptor descriptor - ? descriptor.GetClassName() - : target.GetType().Name; - return $"/{type}/{slug}/{Images.ImageName[_imageIndex]}".ToLowerInvariant(); - } - } } } diff --git a/back/src/Kyoo.Core/Views/Metadata/GenreApi.cs b/back/src/Kyoo.Core/Views/Metadata/GenreApi.cs deleted file mode 100644 index eb72b284..00000000 --- a/back/src/Kyoo.Core/Views/Metadata/GenreApi.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Attributes; -using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Abstractions.Models.Utils; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using static Kyoo.Abstractions.Models.Utils.Constants; - -namespace Kyoo.Core.Api -{ - /// - /// Information about one or multiple . - /// - [Route("genres")] - [Route("genre", Order = AlternativeRoute)] - [ApiController] - [PartialPermission(nameof(Genre))] - [ApiDefinition("Genres", Group = MetadataGroup)] - public class GenreApi : CrudApi - { - /// - /// The library manager used to modify or retrieve information about the data store. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// Create a new . - /// - /// - /// The library manager used to modify or retrieve information about the data store. - /// - public GenreApi(ILibraryManager libraryManager) - : base(libraryManager.GenreRepository) - { - _libraryManager = libraryManager; - } - - /// - /// Get shows with genre - /// - /// - /// Lists the shows that have the selected genre. - /// - /// The ID or slug of the . - /// A key to sort shows by. - /// An optional list of filters. - /// The number of shows to return and where to start. - /// A page of shows. - /// The filters or the sort parameters are invalid. - /// No genre with the given ID could be found. - [HttpGet("{identifier:id}/shows")] - [HttpGet("{identifier:id}/show", Order = AlternativeRoute)] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetShows(Identifier identifier, - [FromQuery] string sortBy, - [FromQuery] Dictionary where, - [FromQuery] Pagination pagination) - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Genres)), - Sort.From(sortBy), - pagination - ); - - if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) - return NotFound(); - return Page(resources, pagination.Limit); - } - } -} diff --git a/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs b/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs index 873c398f..90c72539 100644 --- a/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs +++ b/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs @@ -83,7 +83,7 @@ namespace Kyoo.Core.Api [FromQuery] Pagination pagination) { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.Matcher(x => x.StudioID, x => x.Studio.Slug)), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.StudioId, x => x.Studio.Slug)), Sort.From(sortBy), pagination ); diff --git a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs index ffd13752..bc15368a 100644 --- a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -93,40 +93,5 @@ namespace Kyoo.Core.Api return NotFound(); return Page(resources, pagination.Limit); } - - /// - /// Get libraries containing this collection - /// - /// - /// Lists the libraries that contain the collection with the given id or slug. - /// - /// The ID or slug of the . - /// A key to sort libraries by. - /// An optional list of filters. - /// The number of libraries to return. - /// A page of libraries. - /// The filters or the sort parameters are invalid. - /// No collection with the given ID or slug could be found. - [HttpGet("{identifier:id}/libraries")] - [HttpGet("{identifier:id}/library", Order = AlternativeRoute)] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLibraries(Identifier identifier, - [FromQuery] string sortBy, - [FromQuery] Dictionary where, - [FromQuery] Pagination pagination) - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Collections)), - Sort.From(sortBy), - pagination - ); - - if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) - return NotFound(); - return Page(resources, pagination.Limit); - } } } diff --git a/back/src/Kyoo.Core/Views/Resources/LibraryApi.cs b/back/src/Kyoo.Core/Views/Resources/LibraryApi.cs deleted file mode 100644 index 6551a99a..00000000 --- a/back/src/Kyoo.Core/Views/Resources/LibraryApi.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Attributes; -using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Abstractions.Models.Utils; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using static Kyoo.Abstractions.Models.Utils.Constants; - -namespace Kyoo.Core.Api -{ - /// - /// Information about one or multiple . - /// - [Route("libraries")] - [Route("library", Order = AlternativeRoute)] - [ApiController] - [ResourceView] - [PartialPermission(nameof(Library), Group = Group.Admin)] - [ApiDefinition("Library", Group = ResourcesGroup)] - public class LibraryApi : CrudApi - { - /// - /// The library manager used to modify or retrieve information in the data store. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// Create a new . - /// - /// - /// The library manager used to modify or retrieve information in the data store. - /// - public LibraryApi(ILibraryManager libraryManager) - : base(libraryManager.LibraryRepository) - { - _libraryManager = libraryManager; - } - - /// - /// Get shows - /// - /// - /// List the shows that are part of this library. - /// - /// The ID or slug of the . - /// A key to sort shows by. - /// An optional list of filters. - /// The number of shows to return. - /// A page of shows. - /// The filters or the sort parameters are invalid. - /// No library with the given ID or slug could be found. - [HttpGet("{identifier:id}/shows")] - [HttpGet("{identifier:id}/show", Order = AlternativeRoute)] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetShows(Identifier identifier, - [FromQuery] string sortBy, - [FromQuery] Dictionary where, - [FromQuery] Pagination pagination) - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Libraries)), - Sort.From(sortBy), - pagination - ); - - if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) - return NotFound(); - return Page(resources, pagination.Limit); - } - - /// - /// Get collections - /// - /// - /// List the collections that are part of this library. - /// - /// The ID or slug of the . - /// A key to sort collections by. - /// An optional list of filters. - /// The number of collections to return. - /// A page of collections. - /// The filters or the sort parameters are invalid. - /// No library with the given ID or slug could be found. - [HttpGet("{identifier:id}/collections")] - [HttpGet("{identifier:id}/collection", Order = AlternativeRoute)] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetCollections(Identifier identifier, - [FromQuery] string sortBy, - [FromQuery] Dictionary where, - [FromQuery] Pagination pagination) - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Libraries)), - Sort.From(sortBy), - pagination - ); - - if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) - return NotFound(); - return Page(resources, pagination.Limit); - } - - /// - /// Get items - /// - /// - /// List all items of this library. - /// An item can ether represent a collection or a show. - /// This endpoint allow one to retrieve all collections and shows that are not contained in a collection. - /// This is what is displayed on the /browse/library page of the webapp. - /// - /// The ID or slug of the . - /// A key to sort items by. - /// An optional list of filters. - /// The number of items to return. - /// A page of items. - /// The filters or the sort parameters are invalid. - /// No library with the given ID or slug could be found. - [HttpGet("{identifier:id}/items")] - [HttpGet("{identifier:id}/item", Order = AlternativeRoute)] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetItems(Identifier identifier, - [FromQuery] string sortBy, - [FromQuery] Dictionary where, - [FromQuery] Pagination pagination) - { - Expression> whereQuery = ApiHelper.ParseWhere(where); - Sort sort = Sort.From(sortBy); - - ICollection resources = await identifier.Match( - id => _libraryManager.GetItemsFromLibrary(id, whereQuery, sort, pagination), - slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination) - ); - - return Page(resources, pagination.Limit); - } - } -} diff --git a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs index f1b6747d..8046a942 100644 --- a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; @@ -38,7 +37,7 @@ namespace Kyoo.Core.Api [Route("item", Order = AlternativeRoute)] [ApiController] [ResourceView] - [PartialPermission(nameof(LibraryItem))] + [PartialPermission("LibraryItem")] [ApiDefinition("Items", Group = ResourcesGroup)] public class LibraryItemApi : BaseApi { @@ -78,14 +77,14 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetAll( + public async Task>> GetAll( [FromQuery] string sortBy, [FromQuery] Dictionary where, [FromQuery] Pagination pagination) { - ICollection resources = await _libraryItems.GetAll( - ApiHelper.ParseWhere(where), - Sort.From(sortBy), + ICollection resources = await _libraryItems.GetAll( + ApiHelper.ParseWhere(where), + Sort.From(sortBy), pagination ); diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs new file mode 100644 index 00000000..a1825517 --- /dev/null +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -0,0 +1,149 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Permissions; +using Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using static Kyoo.Abstractions.Models.Utils.Constants; + +namespace Kyoo.Core.Api +{ + /// + /// Information about one or multiple . + /// + [Route("movies")] + [Route("movie", Order = AlternativeRoute)] + [ApiController] + [PartialPermission(nameof(Show))] + [ApiDefinition("Shows", Group = ResourcesGroup)] + public class MovieApi : CrudThumbsApi + { + /// + /// The library manager used to modify or retrieve information in the data store. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information about the data store. + /// + /// The thumbnail manager used to retrieve images paths. + public MovieApi(ILibraryManager libraryManager, + IThumbnailsManager thumbs) + : base(libraryManager.MovieRepository, thumbs) + { + _libraryManager = libraryManager; + } + + // /// + // /// Get staff + // /// + // /// + // /// List staff members that made this show. + // /// + // /// The ID or slug of the . + // /// A key to sort staff members by. + // /// An optional list of filters. + // /// The number of people to return. + // /// A page of people. + // /// The filters or the sort parameters are invalid. + // /// No show with the given ID or slug could be found. + // [HttpGet("{identifier:id}/staff")] + // [HttpGet("{identifier:id}/people", Order = AlternativeRoute)] + // [PartialPermission(Kind.Read)] + // [ProducesResponseType(StatusCodes.Status200OK)] + // [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + // [ProducesResponseType(StatusCodes.Status404NotFound)] + // public async Task>> GetPeople(Identifier identifier, + // [FromQuery] string sortBy, + // [FromQuery] Dictionary where, + // [FromQuery] Pagination pagination) + // { + // Expression> whereQuery = ApiHelper.ParseWhere(where); + // Sort sort = Sort.From(sortBy); + // + // ICollection resources = await identifier.Match( + // id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination), + // slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination) + // ); + // return Page(resources, pagination.Limit); + // } + + /// + /// Get studio that made the show + /// + /// + /// Get the studio that made the show. + /// + /// The ID or slug of the . + /// The studio that made the show. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/studio")] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetStudio(Identifier identifier) + { + return await _libraryManager.Get(identifier.IsContainedIn(x => x.Movies)); + } + + /// + /// Get collections containing this show + /// + /// + /// List the collections that contain this show. + /// + /// The ID or slug of the . + /// A key to sort collections by. + /// An optional list of filters. + /// The number of collections to return. + /// A page of collections. + /// The filters or the sort parameters are invalid. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/collections")] + [HttpGet("{identifier:id}/collection", Order = AlternativeRoute)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetCollections(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] Pagination pagination) + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Movies)), + Sort.From(sortBy), + pagination + ); + + if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) + return NotFound(); + return Page(resources, pagination.Limit); + } + } +} diff --git a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs index 79e9ec2b..4779cb20 100644 --- a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs @@ -76,10 +76,11 @@ namespace Kyoo.Core.Api { Query = query, Collections = await _libraryManager.Search(query), + Items = await _libraryManager.Search(query), + Movies = await _libraryManager.Search(query), Shows = await _libraryManager.Search(query), Episodes = await _libraryManager.Search(query), People = await _libraryManager.Search(query), - Genres = await _libraryManager.Search(query), Studios = await _libraryManager.Search(query) }; } @@ -133,9 +134,9 @@ namespace Kyoo.Core.Api [Permission(nameof(Show), Kind.Read)] [ApiDefinition("Items")] [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchItems(string query) + public Task> SearchItems(string query) { - return _libraryManager.Search(query); + return _libraryManager.Search(query); } /// @@ -175,24 +176,6 @@ namespace Kyoo.Core.Api return _libraryManager.Search(query); } - /// - /// Search genres - /// - /// - /// Search for genres - /// - /// The query to search for. - /// A list of genres found for the specified query. - [HttpGet("genres")] - [HttpGet("genre", Order = AlternativeRoute)] - [Permission(nameof(Genre), Kind.Read)] - [ApiDefinition("Genres")] - [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchGenres(string query) - { - return _libraryManager.Search(query); - } - /// /// Search studios /// diff --git a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs index d336e22c..f26bbe30 100644 --- a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs @@ -84,7 +84,7 @@ namespace Kyoo.Core.Api [FromQuery] Pagination pagination) { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.Matcher(x => x.SeasonID, x => x.Season.Slug)), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.SeasonId, x => x.Season.Slug)), Sort.From(sortBy), pagination ); diff --git a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs index 95a9492e..2084291e 100644 --- a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -37,8 +37,6 @@ namespace Kyoo.Core.Api /// [Route("shows")] [Route("show", Order = AlternativeRoute)] - [Route("movie", Order = AlternativeRoute)] - [Route("movies", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(Show))] [ApiDefinition("Shows", Group = ResourcesGroup)] @@ -63,24 +61,6 @@ namespace Kyoo.Core.Api _libraryManager = libraryManager; } - /// - public override async Task> Create([FromBody] Show resource) - { - ActionResult ret = await base.Create(resource); - if (ret.Value.IsMovie) - { - Episode episode = new() - { - Show = ret.Value, - Title = ret.Value.Title, - Path = ret.Value.Path - }; - - await _libraryManager.Create(episode); - } - return ret; - } - /// /// Get seasons of this show /// @@ -106,7 +86,7 @@ namespace Kyoo.Core.Api [FromQuery] Pagination pagination) { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowID, x => x.Show.Slug)), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowId, x => x.Show.Slug)), Sort.From(sortBy), pagination ); @@ -141,7 +121,7 @@ namespace Kyoo.Core.Api [FromQuery] Pagination pagination) { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowID, x => x.Show.Slug)), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowId, x => x.Show.Slug)), Sort.From(sortBy), pagination ); @@ -185,41 +165,6 @@ namespace Kyoo.Core.Api return Page(resources, pagination.Limit); } - /// - /// Get genres of this show - /// - /// - /// List the genres that represent this show. - /// - /// The ID or slug of the . - /// A key to sort genres by. - /// An optional list of filters. - /// The number of genres to return. - /// A page of genres. - /// The filters or the sort parameters are invalid. - /// No show with the given ID or slug could be found. - [HttpGet("{identifier:id}/genres")] - [HttpGet("{identifier:id}/genre", Order = AlternativeRoute)] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetGenres(Identifier identifier, - [FromQuery] string sortBy, - [FromQuery] Dictionary where, - [FromQuery] Pagination pagination) - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), - Sort.From(sortBy), - pagination - ); - - if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) - return NotFound(); - return Page(resources, pagination.Limit); - } - /// /// Get studio that made the show /// @@ -238,42 +183,6 @@ namespace Kyoo.Core.Api return await _libraryManager.Get(identifier.IsContainedIn(x => x.Shows)); } - /// - /// Get libraries containing this show - /// - /// - /// List the libraries that contain this show. If this show is contained in a collection that is contained in - /// a library, this library will be returned too. - /// - /// The ID or slug of the . - /// A key to sort libraries by. - /// An optional list of filters. - /// The number of libraries to return. - /// A page of libraries. - /// The filters or the sort parameters are invalid. - /// No show with the given ID or slug could be found. - [HttpGet("{identifier:id}/libraries")] - [HttpGet("{identifier:id}/library", Order = AlternativeRoute)] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLibraries(Identifier identifier, - [FromQuery] string sortBy, - [FromQuery] Dictionary where, - [FromQuery] Pagination pagination) - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), - Sort.From(sortBy), - pagination - ); - - if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) - return NotFound(); - return Page(resources, pagination.Limit); - } - /// /// Get collections containing this show /// diff --git a/back/src/Kyoo.Core/Views/Watch/WatchApi.cs b/back/src/Kyoo.Core/Views/Watch/WatchApi.cs deleted file mode 100644 index 24f53174..00000000 --- a/back/src/Kyoo.Core/Views/Watch/WatchApi.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System.Net.Http; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Attributes; -using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Abstractions.Models.Utils; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using static Kyoo.Abstractions.Models.Utils.Constants; - -namespace Kyoo.Core.Api -{ - /// - /// Retrieve information of an as a . - /// A watch item is another representation of an episode in a form easier to read and display for playback. - /// It contains streams (video, audio, subtitles) information, chapters, next and previous episodes and a bit of - /// information of the show. - /// - [Route("watch")] - [Route("watchitem", Order = AlternativeRoute)] - [ApiController] - [ApiDefinition("Watch Items", Group = WatchGroup)] - public class WatchApi : ControllerBase - { - /// - /// The library manager used to modify or retrieve information in the data store. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// The http client to reach transcoder. - /// - private readonly HttpClient _client; - - /// - /// Create a new . - /// - /// - /// The library manager used to modify or retrieve information in the data store. - /// - /// The http client to reach transcoder. - public WatchApi(ILibraryManager libraryManager, HttpClient client) - { - _libraryManager = libraryManager; - _client = client; - } - - /// - /// Get a watch item - /// - /// - /// Retrieve a watch item of an episode. - /// - /// The ID or slug of the . - /// A page of items. - /// No episode with the given ID or slug could be found. - [HttpGet("{identifier:id}")] - [Permission("watch", Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetWatchItem(Identifier identifier) - { - Episode item = await identifier.Match( - id => _libraryManager.GetOrDefault(id), - slug => _libraryManager.GetOrDefault(slug) - ); - if (item == null) - return NotFound(); - return await WatchItem.FromEpisode(item, _libraryManager, _client); - } - } -} diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs index 19303e75..46605074 100644 --- a/back/src/Kyoo.Postgresql/DatabaseContext.cs +++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -40,16 +41,16 @@ namespace Kyoo.Postgresql /// public abstract class DatabaseContext : DbContext { - /// - /// All libraries of Kyoo. See . - /// - public DbSet Libraries { get; set; } - /// /// All collections of Kyoo. See . /// public DbSet Collections { get; set; } + /// + /// All movies of Kyoo. See . + /// + public DbSet Movies { get; set; } + /// /// All shows of Kyoo. See . /// @@ -65,11 +66,6 @@ namespace Kyoo.Postgresql /// public DbSet Episodes { get; set; } - /// - /// All genres of Kyoo. See . - /// - public DbSet Genres { get; set; } - /// /// All people of Kyoo. See . /// @@ -80,11 +76,6 @@ namespace Kyoo.Postgresql /// public DbSet Studios { get; set; } - /// - /// All providers of Kyoo. See . - /// - public DbSet Providers { get; set; } - /// /// The list of registered users. /// @@ -95,11 +86,6 @@ namespace Kyoo.Postgresql /// public DbSet PeopleRoles { get; set; } - /// - /// Episodes with a watch percentage. See . - /// - public DbSet WatchedEpisodes { get; set; } - /// /// The list of library items (shows and collections that are part of a library - or the global one). /// @@ -108,17 +94,6 @@ namespace Kyoo.Postgresql /// public DbSet LibraryItems { get; set; } - /// - /// Get all metadataIDs (ExternalIDs) of a given resource. See . - /// - /// The metadata of this type will be returned. - /// A queryable of metadata ids for a type. - public DbSet MetadataIds() - where T : class, IMetadata - { - return Set(MetadataName()); - } - /// /// Add a many to many link between two resources. /// @@ -153,14 +128,6 @@ namespace Kyoo.Postgresql : base(options) { } - /// - /// Get the name of the metadata table of the given type. - /// - /// The type related to the metadata - /// The name of the table containing the metadata. - protected abstract string MetadataName() - where T : IMetadata; - /// /// Get the name of the link table of the two given types. /// @@ -194,17 +161,33 @@ namespace Kyoo.Postgresql ///
/// The database model builder /// The type to add metadata to. - private void _HasMetadata(ModelBuilder modelBuilder) + private static void _HasMetadata(ModelBuilder modelBuilder) where T : class, IMetadata { - modelBuilder.SharedTypeEntity(MetadataName()) - .HasKey(MetadataID.PrimaryKey); + // TODO: Waiting for https://github.com/dotnet/efcore/issues/29825 + // modelBuilder.Entity() + // .OwnsOne(x => x.ExternalIDs, x => + // { + // x.ToJson(); + // }); + modelBuilder.Entity() + .Property(x => x.ExternalId) + .HasConversion( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), + v => JsonSerializer.Deserialize>(v, (JsonSerializerOptions)null) + ) + .HasColumnType("json"); + } - modelBuilder.SharedTypeEntity(MetadataName()) - .HasOne() - .WithMany(x => x.ExternalIDs) - .HasForeignKey(x => x.ResourceID) - .OnDelete(DeleteBehavior.Cascade); + private static void _HasImages(ModelBuilder modelBuilder) + where T : class, IThumbnails + { + modelBuilder.Entity() + .OwnsOne(x => x.Poster); + modelBuilder.Entity() + .OwnsOne(x => x.Thumbnail); + modelBuilder.Entity() + .OwnsOne(x => x.Logo); } /// @@ -248,6 +231,10 @@ namespace Kyoo.Postgresql { base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .Ignore(x => x.PreviousEpisode) + .Ignore(x => x.NextEpisode); + modelBuilder.Entity() .Ignore(x => x.ForPeople); @@ -264,72 +251,68 @@ namespace Kyoo.Postgresql .WithOne(x => x.Season) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasOne(x => x.Studio) + .WithMany(x => x.Movies) + .OnDelete(DeleteBehavior.SetNull); modelBuilder.Entity() .HasOne(x => x.Studio) .WithMany(x => x.Shows) .OnDelete(DeleteBehavior.SetNull); - _HasManyToMany(modelBuilder, x => x.Providers, x => x.Libraries); - _HasManyToMany(modelBuilder, x => x.Collections, x => x.Libraries); - _HasManyToMany(modelBuilder, x => x.Shows, x => x.Libraries); + _HasManyToMany(modelBuilder, x => x.Movies, x => x.Collections); _HasManyToMany(modelBuilder, x => x.Shows, x => x.Collections); - _HasManyToMany(modelBuilder, x => x.Genres, x => x.Shows); modelBuilder.Entity() .HasMany(x => x.Watched) .WithMany("Users") .UsingEntity(x => x.ToTable(LinkName())); + _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); + _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); + _HasImages(modelBuilder); + _HasImages(modelBuilder); + _HasImages(modelBuilder); + _HasImages(modelBuilder); + _HasImages(modelBuilder); + _HasImages(modelBuilder); + _HasImages(modelBuilder); + + modelBuilder.Entity().OwnsOne(x => x.Logo); + modelBuilder.Entity() .HasKey(x => new { User = x.UserID, Episode = x.EpisodeID }); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => x.Slug) + .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); modelBuilder.Entity() - .HasIndex(x => new { x.ShowID, x.SeasonNumber }) + .HasIndex(x => new { ShowID = 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 }) + .HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber }) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) @@ -338,8 +321,10 @@ namespace Kyoo.Postgresql .HasIndex(x => x.Slug) .IsUnique(); + modelBuilder.Entity() + .Ignore(x => x.Links); modelBuilder.Entity() - .ToView("library_items"); + .Ignore(x => x.Links); } /// @@ -352,7 +337,7 @@ namespace Kyoo.Postgresql public T GetTemporaryObject(T model) where T : class, IResource { - T tmp = Set().Local.FirstOrDefault(x => x.ID == model.ID); + T tmp = Set().Local.FirstOrDefault(x => x.Id == model.Id); if (tmp != null) return tmp; Entry(model).State = EntityState.Unchanged; diff --git a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 2eb64ec0..44ff1a9d 100644 --- a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/back/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.cs b/back/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.cs deleted file mode 100644 index 5321a0a1..00000000 --- a/back/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.cs +++ /dev/null @@ -1,872 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using Kyoo.Abstractions.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Postgresql.Migrations -{ - /// - /// The initial migration that build most of the database. - /// - [DbContext(typeof(PostgresContext))] - [Migration("20210801171613_Initial")] - 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", "unknown,finished,airing,planned") - .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), - images = table.Column>(type: "jsonb", 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), - images = table.Column>(type: "jsonb", 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), - images = table.Column>(type: "jsonb", 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), - images = table.Column>(type: "jsonb", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_users", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "link_library_collection", - columns: table => new - { - collection_id = table.Column(type: "integer", nullable: false), - library_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_link_library_collection", x => new { x.collection_id, x.library_id }); - table.ForeignKey( - name: "fk_link_library_collection_collections_collection_id", - column: x => x.collection_id, - principalTable: "collections", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_link_library_collection_libraries_library_id", - column: x => x.library_id, - principalTable: "libraries", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "collection_metadata_id", - columns: table => new - { - resource_id = table.Column(type: "integer", nullable: false), - provider_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_collection_metadata_id", x => new { x.resource_id, x.provider_id }); - table.ForeignKey( - name: "fk_collection_metadata_id_collections_collection_id", - column: x => x.resource_id, - principalTable: "collections", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_collection_metadata_id_providers_provider_id", - column: x => x.provider_id, - principalTable: "providers", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "link_library_provider", - columns: table => new - { - library_id = table.Column(type: "integer", nullable: false), - provider_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_link_library_provider", x => new { x.library_id, x.provider_id }); - table.ForeignKey( - name: "fk_link_library_provider_libraries_library_id", - column: x => x.library_id, - principalTable: "libraries", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_link_library_provider_providers_provider_id", - column: x => x.provider_id, - principalTable: "providers", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "people_metadata_id", - columns: table => new - { - resource_id = table.Column(type: "integer", nullable: false), - provider_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_people_metadata_id", x => new { x.resource_id, x.provider_id }); - table.ForeignKey( - name: "fk_people_metadata_id_people_people_id", - column: x => x.resource_id, - principalTable: "people", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_people_metadata_id_providers_provider_id", - column: x => x.provider_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: false), - start_air = table.Column(type: "timestamp without time zone", nullable: true), - end_air = table.Column(type: "timestamp without time zone", nullable: true), - images = table.Column>(type: "jsonb", 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.SetNull); - }); - - migrationBuilder.CreateTable( - name: "studio_metadata_id", - columns: table => new - { - resource_id = table.Column(type: "integer", nullable: false), - provider_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_studio_metadata_id", x => new { x.resource_id, x.provider_id }); - table.ForeignKey( - name: "fk_studio_metadata_id_providers_provider_id", - column: x => x.provider_id, - principalTable: "providers", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_studio_metadata_id_studios_studio_id", - column: x => x.resource_id, - principalTable: "studios", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "link_collection_show", - columns: table => new - { - collection_id = table.Column(type: "integer", nullable: false), - show_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_link_collection_show", x => new { x.collection_id, x.show_id }); - table.ForeignKey( - name: "fk_link_collection_show_collections_collection_id", - column: x => x.collection_id, - principalTable: "collections", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_link_collection_show_shows_show_id", - column: x => x.show_id, - principalTable: "shows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "link_library_show", - columns: table => new - { - library_id = table.Column(type: "integer", nullable: false), - show_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_link_library_show", x => new { x.library_id, x.show_id }); - table.ForeignKey( - name: "fk_link_library_show_libraries_library_id", - column: x => x.library_id, - principalTable: "libraries", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_link_library_show_shows_show_id", - column: x => x.show_id, - principalTable: "shows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "link_show_genre", - columns: table => new - { - genre_id = table.Column(type: "integer", nullable: false), - show_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_link_show_genre", x => new { x.genre_id, x.show_id }); - table.ForeignKey( - name: "fk_link_show_genre_genres_genre_id", - column: x => x.genre_id, - principalTable: "genres", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_link_show_genre_shows_show_id", - column: x => x.show_id, - principalTable: "shows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "link_user_show", - columns: table => new - { - users_id = table.Column(type: "integer", nullable: false), - watched_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_link_user_show", x => new { x.users_id, x.watched_id }); - table.ForeignKey( - name: "fk_link_user_show_shows_watched_id", - column: x => x.watched_id, - principalTable: "shows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_link_user_show_users_users_id", - column: x => x.users_id, - principalTable: "users", - 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), - 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), - images = table.Column>(type: "jsonb", 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: "show_metadata_id", - columns: table => new - { - resource_id = table.Column(type: "integer", nullable: false), - provider_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_show_metadata_id", x => new { x.resource_id, x.provider_id }); - table.ForeignKey( - name: "fk_show_metadata_id_providers_provider_id", - column: x => x.provider_id, - principalTable: "providers", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_show_metadata_id_shows_show_id", - column: x => x.resource_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: true), - episode_number = table.Column(type: "integer", nullable: true), - absolute_number = table.Column(type: "integer", nullable: true), - path = table.Column(type: "text", nullable: true), - images = table.Column>(type: "jsonb", 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: "season_metadata_id", - columns: table => new - { - resource_id = table.Column(type: "integer", nullable: false), - provider_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_season_metadata_id", x => new { x.resource_id, x.provider_id }); - table.ForeignKey( - name: "fk_season_metadata_id_providers_provider_id", - column: x => x.provider_id, - principalTable: "providers", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_season_metadata_id_seasons_season_id", - column: x => x.resource_id, - principalTable: "seasons", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "episode_metadata_id", - columns: table => new - { - resource_id = table.Column(type: "integer", nullable: false), - provider_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_episode_metadata_id", x => new { x.resource_id, x.provider_id }); - table.ForeignKey( - name: "fk_episode_metadata_id_episodes_episode_id", - column: x => x.resource_id, - principalTable: "episodes", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_episode_metadata_id_providers_provider_id", - column: x => x.provider_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 - { - user_id = table.Column(type: "integer", nullable: false), - episode_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.user_id, x.episode_id }); - table.ForeignKey( - name: "fk_watched_episodes_episodes_episode_id", - column: x => x.episode_id, - principalTable: "episodes", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_watched_episodes_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "ix_collection_metadata_id_provider_id", - table: "collection_metadata_id", - column: "provider_id"); - - migrationBuilder.CreateIndex( - name: "ix_collections_slug", - table: "collections", - column: "slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_episode_metadata_id_provider_id", - table: "episode_metadata_id", - column: "provider_id"); - - 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_show_id", - table: "link_collection_show", - column: "show_id"); - - migrationBuilder.CreateIndex( - name: "ix_link_library_collection_library_id", - table: "link_library_collection", - column: "library_id"); - - migrationBuilder.CreateIndex( - name: "ix_link_library_provider_provider_id", - table: "link_library_provider", - column: "provider_id"); - - migrationBuilder.CreateIndex( - name: "ix_link_library_show_show_id", - table: "link_library_show", - column: "show_id"); - - migrationBuilder.CreateIndex( - name: "ix_link_show_genre_show_id", - table: "link_show_genre", - column: "show_id"); - - migrationBuilder.CreateIndex( - name: "ix_link_user_show_watched_id", - table: "link_user_show", - column: "watched_id"); - - migrationBuilder.CreateIndex( - name: "ix_people_slug", - table: "people", - column: "slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_people_metadata_id_provider_id", - table: "people_metadata_id", - column: "provider_id"); - - 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_season_metadata_id_provider_id", - table: "season_metadata_id", - column: "provider_id"); - - 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_show_metadata_id_provider_id", - table: "show_metadata_id", - column: "provider_id"); - - 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_studio_metadata_id_provider_id", - table: "studio_metadata_id", - column: "provider_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_episode_id", - table: "watched_episodes", - column: "episode_id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "collection_metadata_id"); - - migrationBuilder.DropTable( - name: "episode_metadata_id"); - - 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: "people_metadata_id"); - - migrationBuilder.DropTable( - name: "people_roles"); - - migrationBuilder.DropTable( - name: "season_metadata_id"); - - migrationBuilder.DropTable( - name: "show_metadata_id"); - - migrationBuilder.DropTable( - name: "studio_metadata_id"); - - migrationBuilder.DropTable( - name: "tracks"); - - migrationBuilder.DropTable( - name: "watched_episodes"); - - migrationBuilder.DropTable( - name: "collections"); - - migrationBuilder.DropTable( - name: "libraries"); - - migrationBuilder.DropTable( - name: "genres"); - - migrationBuilder.DropTable( - name: "people"); - - migrationBuilder.DropTable( - name: "providers"); - - migrationBuilder.DropTable( - name: "episodes"); - - migrationBuilder.DropTable( - name: "users"); - - migrationBuilder.DropTable( - name: "seasons"); - - migrationBuilder.DropTable( - name: "shows"); - - migrationBuilder.DropTable( - name: "studios"); - } - } -} diff --git a/back/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.cs b/back/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.cs deleted file mode 100644 index cc0468a5..00000000 --- a/back/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Kyoo.Postgresql.Migrations -{ - /// - /// A migration that adds postgres triggers to update slugs. - /// - [DbContext(typeof(PostgresContext))] - [Migration("20210801171641_Triggers")] - public partial class Triggers : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // language=PostgreSQL - migrationBuilder.Sql(@" - CREATE FUNCTION season_slug_update() - RETURNS TRIGGER - LANGUAGE PLPGSQL - AS $$ - BEGIN - NEW.slug := CONCAT( - (SELECT slug FROM shows WHERE id = NEW.show_id), - '-s', - NEW.season_number - ); - RETURN NEW; - END - $$;"); - - // language=PostgreSQL - 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), - 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 - ); - RETURN NEW; - END - $$;"); - - // language=PostgreSQL - migrationBuilder.Sql(@" - 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();"); - - // language=PostgreSQL - migrationBuilder.Sql(@" - CREATE FUNCTION show_slug_update() - RETURNS TRIGGER - LANGUAGE PLPGSQL - AS $$ - 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; - 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), - '.', COALESCE(NEW.language, 'und'), - 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();"); - - MigrationHelper.CreateLibraryItemsView(migrationBuilder); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - // 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;"); - // 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;"); - MigrationHelper.DropLibraryItemsView(migrationBuilder); - } - } -} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230724144449_RemoveTriggers.cs b/back/src/Kyoo.Postgresql/Migrations/20230724144449_RemoveTriggers.cs deleted file mode 100644 index f235cdf6..00000000 --- a/back/src/Kyoo.Postgresql/Migrations/20230724144449_RemoveTriggers.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Kyoo.Postgresql.Migrations -{ - /// - /// Remove triggers - /// - [DbContext(typeof(PostgresContext))] - [Migration("20230724144449_RemoveTriggers")] - public partial class RemoveTriggers : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // 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;"); - // 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;"); - - migrationBuilder.AlterColumn( - name: "slug", - table: "tracks", - type: "text", - nullable: false, - defaultValue: string.Empty, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "slug", - table: "seasons", - type: "text", - nullable: false, - defaultValue: string.Empty, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "slug", - table: "episodes", - type: "text", - nullable: false, - defaultValue: string.Empty, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "slug", - table: "tracks", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "slug", - table: "seasons", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "slug", - table: "episodes", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text"); - } - } -} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230726100747_Timestamp.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20230726100747_Timestamp.Designer.cs deleted file mode 100644 index 2a80f33b..00000000 --- a/back/src/Kyoo.Postgresql/Migrations/20230726100747_Timestamp.Designer.cs +++ /dev/null @@ -1,1273 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Kyoo.Abstractions.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; - -#nullable disable - -namespace Kyoo.Postgresql.Migrations -{ - partial class Timestamp - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_type", new[] { "show", "movie", "collection" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "stream_type", new[] { "unknown", "video", "audio", "subtitle" }); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_collections"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_collections_slug"); - - b.ToTable("collections", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("AbsoluteNumber") - .HasColumnType("integer") - .HasColumnName("absolute_number"); - - b.Property("EpisodeNumber") - .HasColumnType("integer") - .HasColumnName("episode_number"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); - - b.Property("ReleaseDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("release_date"); - - b.Property("SeasonID") - .HasColumnType("integer") - .HasColumnName("season_id"); - - b.Property("SeasonNumber") - .HasColumnType("integer") - .HasColumnName("season_number"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_episodes"); - - b.HasIndex("SeasonID") - .HasDatabaseName("ix_episodes_season_id"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_episodes_slug"); - - b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique() - .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); - - b.ToTable("episodes", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Genre", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_genres"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_genres_slug"); - - b.ToTable("genres", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Library", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Paths") - .HasColumnType("text[]") - .HasColumnName("paths"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_libraries"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_libraries_slug"); - - b.ToTable("libraries", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => - { - b.Property("ID") - .HasColumnType("integer") - .HasColumnName("id"); - - b.Property("EndAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_air"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Slug") - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_air"); - - b.Property("Status") - .HasColumnType("status") - .HasColumnName("status"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.Property("Type") - .HasColumnType("item_type") - .HasColumnName("type"); - - b.HasKey("ID") - .HasName("pk_library_items"); - - b.ToTable((string)null); - - b.ToView("library_items", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_people"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_people_slug"); - - b.ToTable("people", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("PeopleID") - .HasColumnType("integer") - .HasColumnName("people_id"); - - b.Property("Role") - .HasColumnType("text") - .HasColumnName("role"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Type") - .HasColumnType("text") - .HasColumnName("type"); - - b.HasKey("ID") - .HasName("pk_people_roles"); - - b.HasIndex("PeopleID") - .HasDatabaseName("ix_people_roles_people_id"); - - b.HasIndex("ShowID") - .HasDatabaseName("ix_people_roles_show_id"); - - b.ToTable("people_roles", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Provider", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_providers"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_providers_slug"); - - b.ToTable("providers", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_date"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("SeasonNumber") - .HasColumnType("integer") - .HasColumnName("season_number"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_date"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_seasons"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_seasons_slug"); - - b.HasIndex("ShowID", "SeasonNumber") - .IsUnique() - .HasDatabaseName("ix_seasons_show_id_season_number"); - - b.ToTable("seasons", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Aliases") - .HasColumnType("text[]") - .HasColumnName("aliases"); - - b.Property("EndAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_air"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("IsMovie") - .HasColumnType("boolean") - .HasColumnName("is_movie"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_air"); - - b.Property("Status") - .HasColumnType("status") - .HasColumnName("status"); - - b.Property("StudioID") - .HasColumnType("integer") - .HasColumnName("studio_id"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_shows"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_shows_slug"); - - b.HasIndex("StudioID") - .HasDatabaseName("ix_shows_studio_id"); - - b.ToTable("shows", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_studios"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_studios_slug"); - - b.ToTable("studios", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Track", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Codec") - .HasColumnType("text") - .HasColumnName("codec"); - - b.Property("EpisodeID") - .HasColumnType("integer") - .HasColumnName("episode_id"); - - b.Property("IsDefault") - .HasColumnType("boolean") - .HasColumnName("is_default"); - - b.Property("IsExternal") - .HasColumnType("boolean") - .HasColumnName("is_external"); - - b.Property("IsForced") - .HasColumnType("boolean") - .HasColumnName("is_forced"); - - b.Property("Language") - .HasColumnType("text") - .HasColumnName("language"); - - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.Property("TrackIndex") - .HasColumnType("integer") - .HasColumnName("track_index"); - - b.Property("Type") - .HasColumnType("stream_type") - .HasColumnName("type"); - - b.HasKey("ID") - .HasName("pk_tracks"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_tracks_slug"); - - b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") - .IsUnique() - .HasDatabaseName("ix_tracks_episode_id_type_language_track_index_is_forced"); - - b.ToTable("tracks", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Email") - .HasColumnType("text") - .HasColumnName("email"); - - b.Property>("ExtraData") - .HasColumnType("jsonb") - .HasColumnName("extra_data"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); - - b.Property("Permissions") - .HasColumnType("text[]") - .HasColumnName("permissions"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Username") - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("ID") - .HasName("pk_users"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_users_slug"); - - b.ToTable("users", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => - { - b.Property("UserID") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("EpisodeID") - .HasColumnType("integer") - .HasColumnName("episode_id"); - - b.Property("WatchedPercentage") - .HasColumnType("integer") - .HasColumnName("watched_percentage"); - - b.HasKey("UserID", "EpisodeID") - .HasName("pk_watched_episodes"); - - b.HasIndex("EpisodeID") - .HasDatabaseName("ix_watched_episodes_episode_id"); - - b.ToTable("watched_episodes", (string)null); - }); - - modelBuilder.Entity("ShowUser", b => - { - b.Property("UsersID") - .HasColumnType("integer") - .HasColumnName("users_id"); - - b.Property("WatchedID") - .HasColumnType("integer") - .HasColumnName("watched_id"); - - b.HasKey("UsersID", "WatchedID") - .HasName("pk_link_user_show"); - - b.HasIndex("WatchedID") - .HasDatabaseName("ix_link_user_show_watched_id"); - - b.ToTable("link_user_show", (string)null); - }); - - modelBuilder.Entity("collection_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_collection_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_collection_metadata_id_provider_id"); - - b.ToTable("collection_metadata_id", (string)null); - }); - - modelBuilder.Entity("episode_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_episode_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_episode_metadata_id_provider_id"); - - b.ToTable("episode_metadata_id", (string)null); - }); - - modelBuilder.Entity("link_collection_show", b => - { - b.Property("collection_id") - .HasColumnType("integer") - .HasColumnName("collection_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("collection_id", "show_id") - .HasName("pk_link_collection_show"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_collection_show_show_id"); - - b.ToTable("link_collection_show", (string)null); - }); - - modelBuilder.Entity("link_library_collection", b => - { - b.Property("collection_id") - .HasColumnType("integer") - .HasColumnName("collection_id"); - - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.HasKey("collection_id", "library_id") - .HasName("pk_link_library_collection"); - - b.HasIndex("library_id") - .HasDatabaseName("ix_link_library_collection_library_id"); - - b.ToTable("link_library_collection", (string)null); - }); - - modelBuilder.Entity("link_library_provider", b => - { - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.Property("provider_id") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.HasKey("library_id", "provider_id") - .HasName("pk_link_library_provider"); - - b.HasIndex("provider_id") - .HasDatabaseName("ix_link_library_provider_provider_id"); - - b.ToTable("link_library_provider", (string)null); - }); - - modelBuilder.Entity("link_library_show", b => - { - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("library_id", "show_id") - .HasName("pk_link_library_show"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_library_show_show_id"); - - b.ToTable("link_library_show", (string)null); - }); - - modelBuilder.Entity("link_show_genre", b => - { - b.Property("genre_id") - .HasColumnType("integer") - .HasColumnName("genre_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("genre_id", "show_id") - .HasName("pk_link_show_genre"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_show_genre_show_id"); - - b.ToTable("link_show_genre", (string)null); - }); - - modelBuilder.Entity("people_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_people_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_people_metadata_id_provider_id"); - - b.ToTable("people_metadata_id", (string)null); - }); - - modelBuilder.Entity("season_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_season_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_season_metadata_id_provider_id"); - - b.ToTable("season_metadata_id", (string)null); - }); - - modelBuilder.Entity("show_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_show_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_show_metadata_id_provider_id"); - - b.ToTable("show_metadata_id", (string)null); - }); - - modelBuilder.Entity("studio_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_studio_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_studio_metadata_id_provider_id"); - - b.ToTable("studio_metadata_id", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.HasOne("Kyoo.Abstractions.Models.Season", "Season") - .WithMany("Episodes") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade) - .HasConstraintName("fk_episodes_seasons_season_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("Episodes") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episodes_shows_show_id"); - - b.Navigation("Season"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => - { - b.HasOne("Kyoo.Abstractions.Models.People", "People") - .WithMany("Roles") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_roles_people_people_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("People") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_roles_shows_show_id"); - - b.Navigation("People"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("Seasons") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_seasons_shows_show_id"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") - .WithMany("Shows") - .HasForeignKey("StudioID") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("fk_shows_studios_studio_id"); - - b.Navigation("Studio"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Track", b => - { - b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") - .WithMany("Tracks") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_tracks_episodes_episode_id"); - - b.Navigation("Episode"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => - { - b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") - .WithMany() - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_watched_episodes_episodes_episode_id"); - - b.HasOne("Kyoo.Abstractions.Models.User", null) - .WithMany("CurrentlyWatching") - .HasForeignKey("UserID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_watched_episodes_users_user_id"); - - b.Navigation("Episode"); - }); - - modelBuilder.Entity("ShowUser", b => - { - b.HasOne("Kyoo.Abstractions.Models.User", null) - .WithMany() - .HasForeignKey("UsersID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_user_show_users_users_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("WatchedID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_user_show_shows_watched_id"); - }); - - modelBuilder.Entity("collection_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_collection_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_collection_metadata_id_collections_collection_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("episode_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episode_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Episode", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episode_metadata_id_episodes_episode_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("link_collection_show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany() - .HasForeignKey("collection_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_collection_show_collections_collection_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_collection_show_shows_show_id"); - }); - - modelBuilder.Entity("link_library_collection", b => - { - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany() - .HasForeignKey("collection_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_collection_collections_collection_id"); - - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_collection_libraries_library_id"); - }); - - modelBuilder.Entity("link_library_provider", b => - { - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_provider_libraries_library_id"); - - b.HasOne("Kyoo.Abstractions.Models.Provider", null) - .WithMany() - .HasForeignKey("provider_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_provider_providers_provider_id"); - }); - - modelBuilder.Entity("link_library_show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_show_libraries_library_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_show_shows_show_id"); - }); - - modelBuilder.Entity("link_show_genre", b => - { - b.HasOne("Kyoo.Abstractions.Models.Genre", null) - .WithMany() - .HasForeignKey("genre_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_show_genre_genres_genre_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_show_genre_shows_show_id"); - }); - - modelBuilder.Entity("people_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.People", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_metadata_id_people_people_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("season_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_season_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Season", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_season_metadata_id_seasons_season_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("show_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_show_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_show_metadata_id_shows_show_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("studio_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_studio_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Studio", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_studio_metadata_id_studios_studio_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => - { - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Tracks"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Roles"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - - b.Navigation("People"); - - b.Navigation("Seasons"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Shows"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => - { - b.Navigation("CurrentlyWatching"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230726100747_Timestamp.cs b/back/src/Kyoo.Postgresql/Migrations/20230726100747_Timestamp.cs deleted file mode 100644 index 3fd9cd6b..00000000 --- a/back/src/Kyoo.Postgresql/Migrations/20230726100747_Timestamp.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Kyoo.Postgresql.Migrations -{ - /// - [DbContext(typeof(PostgresContext))] - [Migration("20230726100747_Timestamp")] - public partial class Timestamp : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - MigrationHelper.DropLibraryItemsView(migrationBuilder); - - migrationBuilder.AlterColumn( - name: "start_air", - table: "shows", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp without time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "end_air", - table: "shows", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp without time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "start_date", - table: "seasons", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp without time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "end_date", - table: "seasons", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp without time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "release_date", - table: "episodes", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp without time zone", - oldNullable: true); - - MigrationHelper.CreateLibraryItemsView(migrationBuilder); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - MigrationHelper.DropLibraryItemsView(migrationBuilder); - - migrationBuilder.AlterColumn( - name: "start_air", - table: "shows", - type: "timestamp without time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "end_air", - table: "shows", - type: "timestamp without time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "start_date", - table: "seasons", - type: "timestamp without time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "end_date", - table: "seasons", - type: "timestamp without time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "release_date", - table: "episodes", - type: "timestamp without time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - MigrationHelper.CreateLibraryItemsView(migrationBuilder); - } - } -} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230731065523_RemoveTracks.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20230731065523_RemoveTracks.Designer.cs deleted file mode 100644 index e86a0b3f..00000000 --- a/back/src/Kyoo.Postgresql/Migrations/20230731065523_RemoveTracks.Designer.cs +++ /dev/null @@ -1,1189 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Kyoo.Abstractions.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Kyoo.Postgresql.Migrations -{ - [DbContext(typeof(PostgresContext))] - [Migration("20230731065523_RemoveTracks")] - partial class RemoveTracks - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_type", new[] { "show", "movie", "collection" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" }); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_collections"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_collections_slug"); - - b.ToTable("collections", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("AbsoluteNumber") - .HasColumnType("integer") - .HasColumnName("absolute_number"); - - b.Property("EpisodeNumber") - .HasColumnType("integer") - .HasColumnName("episode_number"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); - - b.Property("ReleaseDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("release_date"); - - b.Property("SeasonID") - .HasColumnType("integer") - .HasColumnName("season_id"); - - b.Property("SeasonNumber") - .HasColumnType("integer") - .HasColumnName("season_number"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_episodes"); - - b.HasIndex("SeasonID") - .HasDatabaseName("ix_episodes_season_id"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_episodes_slug"); - - b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique() - .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); - - b.ToTable("episodes", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Genre", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_genres"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_genres_slug"); - - b.ToTable("genres", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Library", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Paths") - .HasColumnType("text[]") - .HasColumnName("paths"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_libraries"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_libraries_slug"); - - b.ToTable("libraries", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => - { - b.Property("ID") - .HasColumnType("integer") - .HasColumnName("id"); - - b.Property("EndAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_air"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Slug") - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_air"); - - b.Property("Status") - .HasColumnType("status") - .HasColumnName("status"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.Property("Type") - .HasColumnType("item_type") - .HasColumnName("type"); - - b.HasKey("ID") - .HasName("pk_library_items"); - - b.ToTable((string)null); - - b.ToView("library_items", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_people"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_people_slug"); - - b.ToTable("people", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("PeopleID") - .HasColumnType("integer") - .HasColumnName("people_id"); - - b.Property("Role") - .HasColumnType("text") - .HasColumnName("role"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Type") - .HasColumnType("text") - .HasColumnName("type"); - - b.HasKey("ID") - .HasName("pk_people_roles"); - - b.HasIndex("PeopleID") - .HasDatabaseName("ix_people_roles_people_id"); - - b.HasIndex("ShowID") - .HasDatabaseName("ix_people_roles_show_id"); - - b.ToTable("people_roles", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Provider", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_providers"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_providers_slug"); - - b.ToTable("providers", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_date"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("SeasonNumber") - .HasColumnType("integer") - .HasColumnName("season_number"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_date"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_seasons"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_seasons_slug"); - - b.HasIndex("ShowID", "SeasonNumber") - .IsUnique() - .HasDatabaseName("ix_seasons_show_id_season_number"); - - b.ToTable("seasons", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Aliases") - .HasColumnType("text[]") - .HasColumnName("aliases"); - - b.Property("EndAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_air"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("IsMovie") - .HasColumnType("boolean") - .HasColumnName("is_movie"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_air"); - - b.Property("Status") - .HasColumnType("status") - .HasColumnName("status"); - - b.Property("StudioID") - .HasColumnType("integer") - .HasColumnName("studio_id"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_shows"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_shows_slug"); - - b.HasIndex("StudioID") - .HasDatabaseName("ix_shows_studio_id"); - - b.ToTable("shows", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_studios"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_studios_slug"); - - b.ToTable("studios", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Email") - .HasColumnType("text") - .HasColumnName("email"); - - b.Property>("ExtraData") - .HasColumnType("jsonb") - .HasColumnName("extra_data"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); - - b.Property("Permissions") - .HasColumnType("text[]") - .HasColumnName("permissions"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Username") - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("ID") - .HasName("pk_users"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_users_slug"); - - b.ToTable("users", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => - { - b.Property("UserID") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("EpisodeID") - .HasColumnType("integer") - .HasColumnName("episode_id"); - - b.Property("WatchedPercentage") - .HasColumnType("integer") - .HasColumnName("watched_percentage"); - - b.HasKey("UserID", "EpisodeID") - .HasName("pk_watched_episodes"); - - b.HasIndex("EpisodeID") - .HasDatabaseName("ix_watched_episodes_episode_id"); - - b.ToTable("watched_episodes", (string)null); - }); - - modelBuilder.Entity("ShowUser", b => - { - b.Property("UsersID") - .HasColumnType("integer") - .HasColumnName("users_id"); - - b.Property("WatchedID") - .HasColumnType("integer") - .HasColumnName("watched_id"); - - b.HasKey("UsersID", "WatchedID") - .HasName("pk_link_user_show"); - - b.HasIndex("WatchedID") - .HasDatabaseName("ix_link_user_show_watched_id"); - - b.ToTable("link_user_show", (string)null); - }); - - modelBuilder.Entity("collection_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_collection_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_collection_metadata_id_provider_id"); - - b.ToTable("collection_metadata_id", (string)null); - }); - - modelBuilder.Entity("episode_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_episode_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_episode_metadata_id_provider_id"); - - b.ToTable("episode_metadata_id", (string)null); - }); - - modelBuilder.Entity("link_collection_show", b => - { - b.Property("collection_id") - .HasColumnType("integer") - .HasColumnName("collection_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("collection_id", "show_id") - .HasName("pk_link_collection_show"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_collection_show_show_id"); - - b.ToTable("link_collection_show", (string)null); - }); - - modelBuilder.Entity("link_library_collection", b => - { - b.Property("collection_id") - .HasColumnType("integer") - .HasColumnName("collection_id"); - - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.HasKey("collection_id", "library_id") - .HasName("pk_link_library_collection"); - - b.HasIndex("library_id") - .HasDatabaseName("ix_link_library_collection_library_id"); - - b.ToTable("link_library_collection", (string)null); - }); - - modelBuilder.Entity("link_library_provider", b => - { - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.Property("provider_id") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.HasKey("library_id", "provider_id") - .HasName("pk_link_library_provider"); - - b.HasIndex("provider_id") - .HasDatabaseName("ix_link_library_provider_provider_id"); - - b.ToTable("link_library_provider", (string)null); - }); - - modelBuilder.Entity("link_library_show", b => - { - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("library_id", "show_id") - .HasName("pk_link_library_show"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_library_show_show_id"); - - b.ToTable("link_library_show", (string)null); - }); - - modelBuilder.Entity("link_show_genre", b => - { - b.Property("genre_id") - .HasColumnType("integer") - .HasColumnName("genre_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("genre_id", "show_id") - .HasName("pk_link_show_genre"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_show_genre_show_id"); - - b.ToTable("link_show_genre", (string)null); - }); - - modelBuilder.Entity("people_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_people_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_people_metadata_id_provider_id"); - - b.ToTable("people_metadata_id", (string)null); - }); - - modelBuilder.Entity("season_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_season_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_season_metadata_id_provider_id"); - - b.ToTable("season_metadata_id", (string)null); - }); - - modelBuilder.Entity("show_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_show_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_show_metadata_id_provider_id"); - - b.ToTable("show_metadata_id", (string)null); - }); - - modelBuilder.Entity("studio_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_studio_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_studio_metadata_id_provider_id"); - - b.ToTable("studio_metadata_id", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.HasOne("Kyoo.Abstractions.Models.Season", "Season") - .WithMany("Episodes") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade) - .HasConstraintName("fk_episodes_seasons_season_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("Episodes") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episodes_shows_show_id"); - - b.Navigation("Season"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => - { - b.HasOne("Kyoo.Abstractions.Models.People", "People") - .WithMany("Roles") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_roles_people_people_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("People") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_roles_shows_show_id"); - - b.Navigation("People"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("Seasons") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_seasons_shows_show_id"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") - .WithMany("Shows") - .HasForeignKey("StudioID") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("fk_shows_studios_studio_id"); - - b.Navigation("Studio"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => - { - b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") - .WithMany() - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_watched_episodes_episodes_episode_id"); - - b.HasOne("Kyoo.Abstractions.Models.User", null) - .WithMany("CurrentlyWatching") - .HasForeignKey("UserID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_watched_episodes_users_user_id"); - - b.Navigation("Episode"); - }); - - modelBuilder.Entity("ShowUser", b => - { - b.HasOne("Kyoo.Abstractions.Models.User", null) - .WithMany() - .HasForeignKey("UsersID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_user_show_users_users_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("WatchedID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_user_show_shows_watched_id"); - }); - - modelBuilder.Entity("collection_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_collection_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_collection_metadata_id_collections_collection_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("episode_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episode_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Episode", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episode_metadata_id_episodes_episode_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("link_collection_show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany() - .HasForeignKey("collection_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_collection_show_collections_collection_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_collection_show_shows_show_id"); - }); - - modelBuilder.Entity("link_library_collection", b => - { - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany() - .HasForeignKey("collection_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_collection_collections_collection_id"); - - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_collection_libraries_library_id"); - }); - - modelBuilder.Entity("link_library_provider", b => - { - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_provider_libraries_library_id"); - - b.HasOne("Kyoo.Abstractions.Models.Provider", null) - .WithMany() - .HasForeignKey("provider_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_provider_providers_provider_id"); - }); - - modelBuilder.Entity("link_library_show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_show_libraries_library_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_show_shows_show_id"); - }); - - modelBuilder.Entity("link_show_genre", b => - { - b.HasOne("Kyoo.Abstractions.Models.Genre", null) - .WithMany() - .HasForeignKey("genre_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_show_genre_genres_genre_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_show_genre_shows_show_id"); - }); - - modelBuilder.Entity("people_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.People", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_metadata_id_people_people_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("season_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_season_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Season", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_season_metadata_id_seasons_season_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("show_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_show_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_show_metadata_id_shows_show_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("studio_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_studio_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Studio", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_studio_metadata_id_studios_studio_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => - { - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Roles"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - - b.Navigation("People"); - - b.Navigation("Seasons"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Shows"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => - { - b.Navigation("CurrentlyWatching"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230731065523_RemoveTracks.cs b/back/src/Kyoo.Postgresql/Migrations/20230731065523_RemoveTracks.cs deleted file mode 100644 index a9e58f16..00000000 --- a/back/src/Kyoo.Postgresql/Migrations/20230731065523_RemoveTracks.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Kyoo.Postgresql.Migrations -{ - /// - public partial class RemoveTracks : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "tracks"); - - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:item_type", "show,movie,collection") - .Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned") - .OldAnnotation("Npgsql:Enum:item_type", "show,movie,collection") - .OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned") - .OldAnnotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:item_type", "show,movie,collection") - .Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned") - .Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle") - .OldAnnotation("Npgsql:Enum:item_type", "show,movie,collection") - .OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned"); - - migrationBuilder.CreateTable( - name: "tracks", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - episode_id = table.Column(type: "integer", nullable: false), - codec = table.Column(type: "text", nullable: true), - is_default = table.Column(type: "boolean", nullable: false), - is_external = table.Column(type: "boolean", nullable: false), - is_forced = table.Column(type: "boolean", nullable: false), - language = table.Column(type: "text", nullable: true), - path = table.Column(type: "text", nullable: true), - slug = table.Column(type: "text", nullable: false), - title = table.Column(type: "text", nullable: true), - track_index = table.Column(type: "integer", nullable: false), - type = table.Column(type: "stream_type", nullable: true) - }, - 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.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); - } - } -} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.Designer.cs new file mode 100644 index 00000000..78e47d38 --- /dev/null +++ b/back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.Designer.cs @@ -0,0 +1,1502 @@ +// +using System; +using Kyoo.Abstractions.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; + +#nullable disable + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20230806025737_initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "action", "adventure", "animation", "comedy", "crime", "documentary", "drama", "family", "fantasy", "history", "horror", "music", "mystery", "romance", "science_fiction", "thriller", "war", "western" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_kind", new[] { "show", "movie", "collection" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_collections"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_collections_slug"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsoluteNumber") + .HasColumnType("integer") + .HasColumnName("absolute_number"); + + b.Property("EpisodeNumber") + .HasColumnType("integer") + .HasColumnName("episode_number"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("release_date"); + + b.Property("SeasonID") + .HasColumnType("integer") + .HasColumnName("season_id"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_episodes"); + + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); + + b.ToTable("episodes", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AirDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date"); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("EndAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_air"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_library_items"); + + b.ToTable("library_items", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AirDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date"); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_movies"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_movies_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_movies_studio_id"); + + b.ToTable("movies", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_people"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_people_slug"); + + b.ToTable("people", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MovieID") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("PeopleID") + .HasColumnType("integer") + .HasColumnName("people_id"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_people_roles"); + + b.HasIndex("MovieID") + .HasDatabaseName("ix_people_roles_movie_id"); + + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); + + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); + + b.ToTable("people_roles", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_date"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("pk_seasons"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); + + b.ToTable("seasons", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("EndAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_air"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_shows"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_shows_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); + + b.ToTable("shows", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_studios"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_studios_slug"); + + b.ToTable("studios", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("permissions"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_users_slug"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("EpisodeID") + .HasColumnType("integer") + .HasColumnName("episode_id"); + + b.Property("WatchedPercentage") + .HasColumnType("integer") + .HasColumnName("watched_percentage"); + + b.HasKey("UserID", "EpisodeID") + .HasName("pk_watched_episode"); + + b.HasIndex("EpisodeID") + .HasDatabaseName("ix_watched_episode_episode_id"); + + b.ToTable("watched_episode", (string)null); + }); + + modelBuilder.Entity("ShowUser", b => + { + b.Property("UsersId") + .HasColumnType("integer") + .HasColumnName("users_id"); + + b.Property("WatchedId") + .HasColumnType("integer") + .HasColumnName("watched_id"); + + b.HasKey("UsersId", "WatchedId") + .HasName("pk_link_user_show"); + + b.HasIndex("WatchedId") + .HasDatabaseName("ix_link_user_show_watched_id"); + + b.ToTable("link_user_show", (string)null); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.Property("collection_id") + .HasColumnType("integer") + .HasColumnName("collection_id"); + + b.Property("movie_id") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.HasKey("collection_id", "movie_id") + .HasName("pk_link_collection_movie"); + + b.HasIndex("movie_id") + .HasDatabaseName("ix_link_collection_movie_movie_id"); + + b.ToTable("link_collection_movie", (string)null); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.Property("collection_id") + .HasColumnType("integer") + .HasColumnName("collection_id"); + + b.Property("show_id") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.HasKey("collection_id", "show_id") + .HasName("pk_link_collection_show"); + + b.HasIndex("show_id") + .HasDatabaseName("ix_link_collection_show_show_id"); + + b.ToTable("link_collection_show", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.HasOne("Kyoo.Abstractions.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_episodes_seasons_season_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_episodes_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Season"); + + b.Navigation("Show"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("LibraryItemId"); + + b1.ToTable("library_items"); + + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("LibraryItemId"); + + b1.ToTable("library_items"); + + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("LibraryItemId"); + + b1.ToTable("library_items"); + + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Movies") + .HasForeignKey("StudioID") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_movies_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Abstractions.Models.Movie", "Movie") + .WithMany("People") + .HasForeignKey("MovieID") + .HasConstraintName("fk_people_roles_movies_movie_id"); + + b.HasOne("Kyoo.Abstractions.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_people_roles_people_people_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id"); + + b.Navigation("Movie"); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_seasons_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Show"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_shows_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("UserId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("UserId"); + + b1.ToTable("users"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_id"); + }); + + b.Navigation("Logo"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") + .WithMany() + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_watched_episode_episodes_episode_id"); + + b.HasOne("Kyoo.Abstractions.Models.User", null) + .WithMany("CurrentlyWatching") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_watched_episode_users_user_id"); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ShowUser", b => + { + b.HasOne("Kyoo.Abstractions.Models.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_user_show_users_users_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", null) + .WithMany() + .HasForeignKey("WatchedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_user_show_shows_watched_id"); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Movie", null) + .WithMany() + .HasForeignKey("movie_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_movies_movie_id"); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", null) + .WithMany() + .HasForeignKey("show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_shows_show_id"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Navigation("Episodes"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Navigation("Movies"); + + b.Navigation("Shows"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.cs b/back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.cs new file mode 100644 index 00000000..2cf9c227 --- /dev/null +++ b/back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.cs @@ -0,0 +1,547 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using Kyoo.Abstractions.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Kyoo.Postgresql.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western") + .Annotation("Npgsql:Enum:item_kind", "show,movie,collection") + .Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned"); + + migrationBuilder.CreateTable( + name: "collections", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + name = table.Column(type: "text", nullable: false), + overview = table.Column(type: "text", nullable: true), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + external_id = table.Column(type: "json", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_collections", 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: "character varying(256)", maxLength: 256, nullable: false), + name = table.Column(type: "text", nullable: false), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + external_id = table.Column(type: "json", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_people", 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: "character varying(256)", maxLength: 256, nullable: false), + name = table.Column(type: "text", nullable: false), + external_id = table.Column(type: "json", nullable: false) + }, + 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: "character varying(256)", maxLength: 256, nullable: false), + username = table.Column(type: "text", nullable: false), + email = table.Column(type: "text", nullable: false), + password = table.Column(type: "text", nullable: false), + permissions = table.Column(type: "text[]", nullable: false), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "movies", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + name = table.Column(type: "text", nullable: false), + tagline = table.Column(type: "text", nullable: true), + aliases = table.Column(type: "text[]", nullable: false), + path = table.Column(type: "text", nullable: false), + overview = table.Column(type: "text", nullable: true), + tags = table.Column(type: "text[]", nullable: false), + genres = table.Column(type: "genre[]", nullable: false), + status = table.Column(type: "status", nullable: false), + air_date = table.Column(type: "timestamp with time zone", nullable: true), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + trailer = table.Column(type: "text", nullable: true), + external_id = table.Column(type: "json", nullable: false), + studio_id = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_movies", x => x.id); + table.ForeignKey( + name: "fk_movies_studios_studio_id", + column: x => x.studio_id, + principalTable: "studios", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "shows", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + name = table.Column(type: "text", nullable: false), + tagline = table.Column(type: "text", nullable: true), + aliases = table.Column(type: "text[]", nullable: false), + overview = table.Column(type: "text", nullable: true), + tags = table.Column(type: "text[]", nullable: false), + genres = table.Column(type: "genre[]", nullable: false), + status = table.Column(type: "status", nullable: false), + start_air = table.Column(type: "timestamp with time zone", nullable: true), + end_air = table.Column(type: "timestamp with time zone", nullable: true), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + trailer = table.Column(type: "text", nullable: true), + external_id = table.Column(type: "json", 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.SetNull); + }); + + migrationBuilder.CreateTable( + name: "link_collection_movie", + columns: table => new + { + collection_id = table.Column(type: "integer", nullable: false), + movie_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_collection_movie", x => new { x.collection_id, x.movie_id }); + table.ForeignKey( + name: "fk_link_collection_movie_collections_collection_id", + column: x => x.collection_id, + principalTable: "collections", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_collection_movie_movies_movie_id", + column: x => x.movie_id, + principalTable: "movies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_collection_show", + columns: table => new + { + collection_id = table.Column(type: "integer", nullable: false), + show_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_collection_show", x => new { x.collection_id, x.show_id }); + table.ForeignKey( + name: "fk_link_collection_show_collections_collection_id", + column: x => x.collection_id, + principalTable: "collections", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_collection_show_shows_show_id", + column: x => x.show_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_user_show", + columns: table => new + { + users_id = table.Column(type: "integer", nullable: false), + watched_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_user_show", x => new { x.users_id, x.watched_id }); + table.ForeignKey( + name: "fk_link_user_show_shows_watched_id", + column: x => x.watched_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_user_show_users_users_id", + column: x => x.users_id, + principalTable: "users", + 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), + people_id = table.Column(type: "integer", nullable: false), + show_id = table.Column(type: "integer", nullable: true), + movie_id = table.Column(type: "integer", nullable: true), + type = table.Column(type: "text", nullable: false), + role = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_people_roles", x => x.id); + table.ForeignKey( + name: "fk_people_roles_movies_movie_id", + column: x => x.movie_id, + principalTable: "movies", + principalColumn: "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"); + }); + + migrationBuilder.CreateTable( + name: "seasons", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + show_id = table.Column(type: "integer", nullable: false), + season_number = table.Column(type: "integer", nullable: false), + name = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + start_date = table.Column(type: "timestamp with time zone", nullable: true), + end_date = table.Column(type: "timestamp with time zone", nullable: true), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + external_id = table.Column(type: "json", nullable: false) + }, + 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: "character varying(256)", maxLength: 256, nullable: false), + show_id = table.Column(type: "integer", nullable: false), + season_id = table.Column(type: "integer", nullable: true), + 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: false), + name = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + release_date = table.Column(type: "timestamp with time zone", nullable: true), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + external_id = table.Column(type: "json", nullable: false) + }, + 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: "watched_episode", + columns: table => new + { + user_id = table.Column(type: "integer", nullable: false), + episode_id = table.Column(type: "integer", nullable: false), + watched_percentage = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_watched_episode", x => new { x.user_id, x.episode_id }); + table.ForeignKey( + name: "fk_watched_episode_episodes_episode_id", + column: x => x.episode_id, + principalTable: "episodes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_watched_episode_users_user_id", + column: x => x.user_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_link_collection_movie_movie_id", + table: "link_collection_movie", + column: "movie_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_collection_show_show_id", + table: "link_collection_show", + column: "show_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_user_show_watched_id", + table: "link_user_show", + column: "watched_id"); + + migrationBuilder.CreateIndex( + name: "ix_movies_slug", + table: "movies", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_movies_studio_id", + table: "movies", + column: "studio_id"); + + migrationBuilder.CreateIndex( + name: "ix_people_slug", + table: "people", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_people_roles_movie_id", + table: "people_roles", + column: "movie_id"); + + 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_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_users_slug", + table: "users", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_watched_episode_episode_id", + table: "watched_episode", + column: "episode_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "link_collection_movie"); + + migrationBuilder.DropTable( + name: "link_collection_show"); + + migrationBuilder.DropTable( + name: "link_user_show"); + + migrationBuilder.DropTable( + name: "people_roles"); + + migrationBuilder.DropTable( + name: "watched_episode"); + + migrationBuilder.DropTable( + name: "collections"); + + migrationBuilder.DropTable( + name: "movies"); + + 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/back/src/Kyoo.Postgresql/Migrations/20230806025743_items.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20230806025743_items.Designer.cs new file mode 100644 index 00000000..8fff4c47 --- /dev/null +++ b/back/src/Kyoo.Postgresql/Migrations/20230806025743_items.Designer.cs @@ -0,0 +1,1502 @@ +// +using System; +using Kyoo.Abstractions.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; + +#nullable disable + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20230806025743_items")] + partial class Items + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "action", "adventure", "animation", "comedy", "crime", "documentary", "drama", "family", "fantasy", "history", "horror", "music", "mystery", "romance", "science_fiction", "thriller", "war", "western" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_kind", new[] { "show", "movie", "collection" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_collections"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_collections_slug"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsoluteNumber") + .HasColumnType("integer") + .HasColumnName("absolute_number"); + + b.Property("EpisodeNumber") + .HasColumnType("integer") + .HasColumnName("episode_number"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("release_date"); + + b.Property("SeasonID") + .HasColumnType("integer") + .HasColumnName("season_id"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_episodes"); + + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); + + b.ToTable("episodes", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AirDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date"); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("EndAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_air"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_library_items"); + + b.ToTable("library_items", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AirDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date"); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_movies"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_movies_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_movies_studio_id"); + + b.ToTable("movies", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_people"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_people_slug"); + + b.ToTable("people", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MovieID") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("PeopleID") + .HasColumnType("integer") + .HasColumnName("people_id"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_people_roles"); + + b.HasIndex("MovieID") + .HasDatabaseName("ix_people_roles_movie_id"); + + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); + + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); + + b.ToTable("people_roles", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_date"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("pk_seasons"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); + + b.ToTable("seasons", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("EndAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_air"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_shows"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_shows_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); + + b.ToTable("shows", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_studios"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_studios_slug"); + + b.ToTable("studios", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("permissions"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_users_slug"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("EpisodeID") + .HasColumnType("integer") + .HasColumnName("episode_id"); + + b.Property("WatchedPercentage") + .HasColumnType("integer") + .HasColumnName("watched_percentage"); + + b.HasKey("UserID", "EpisodeID") + .HasName("pk_watched_episode"); + + b.HasIndex("EpisodeID") + .HasDatabaseName("ix_watched_episode_episode_id"); + + b.ToTable("watched_episode", (string)null); + }); + + modelBuilder.Entity("ShowUser", b => + { + b.Property("UsersId") + .HasColumnType("integer") + .HasColumnName("users_id"); + + b.Property("WatchedId") + .HasColumnType("integer") + .HasColumnName("watched_id"); + + b.HasKey("UsersId", "WatchedId") + .HasName("pk_link_user_show"); + + b.HasIndex("WatchedId") + .HasDatabaseName("ix_link_user_show_watched_id"); + + b.ToTable("link_user_show", (string)null); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.Property("collection_id") + .HasColumnType("integer") + .HasColumnName("collection_id"); + + b.Property("movie_id") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.HasKey("collection_id", "movie_id") + .HasName("pk_link_collection_movie"); + + b.HasIndex("movie_id") + .HasDatabaseName("ix_link_collection_movie_movie_id"); + + b.ToTable("link_collection_movie", (string)null); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.Property("collection_id") + .HasColumnType("integer") + .HasColumnName("collection_id"); + + b.Property("show_id") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.HasKey("collection_id", "show_id") + .HasName("pk_link_collection_show"); + + b.HasIndex("show_id") + .HasDatabaseName("ix_link_collection_show_show_id"); + + b.ToTable("link_collection_show", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.HasOne("Kyoo.Abstractions.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_episodes_seasons_season_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_episodes_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Season"); + + b.Navigation("Show"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("LibraryItemId"); + + b1.ToTable("library_items"); + + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("LibraryItemId"); + + b1.ToTable("library_items"); + + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("LibraryItemId"); + + b1.ToTable("library_items"); + + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Movies") + .HasForeignKey("StudioID") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_movies_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Abstractions.Models.Movie", "Movie") + .WithMany("People") + .HasForeignKey("MovieID") + .HasConstraintName("fk_people_roles_movies_movie_id"); + + b.HasOne("Kyoo.Abstractions.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_people_roles_people_people_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id"); + + b.Navigation("Movie"); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_seasons_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Show"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_shows_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("UserId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("UserId"); + + b1.ToTable("users"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_id"); + }); + + b.Navigation("Logo"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") + .WithMany() + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_watched_episode_episodes_episode_id"); + + b.HasOne("Kyoo.Abstractions.Models.User", null) + .WithMany("CurrentlyWatching") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_watched_episode_users_user_id"); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ShowUser", b => + { + b.HasOne("Kyoo.Abstractions.Models.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_user_show_users_users_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", null) + .WithMany() + .HasForeignKey("WatchedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_user_show_shows_watched_id"); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Movie", null) + .WithMany() + .HasForeignKey("movie_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_movies_movie_id"); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", null) + .WithMany() + .HasForeignKey("show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_shows_show_id"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Navigation("Episodes"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Navigation("Movies"); + + b.Navigation("Shows"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/back/src/Kyoo.Postgresql/Migrations/20230806025743_items.cs b/back/src/Kyoo.Postgresql/Migrations/20230806025743_items.cs new file mode 100644 index 00000000..9a85bbde --- /dev/null +++ b/back/src/Kyoo.Postgresql/Migrations/20230806025743_items.cs @@ -0,0 +1,64 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Kyoo.Postgresql.Migrations +{ + /// + public partial class Items : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE VIEW library_items AS + SELECT + s.id, s.slug, s.name, s.tagline, s.aliases, s.overview, s.tags, s.genres, s.status, + s.start_air, s.end_air, s.poster_source, s.poster_blurhash, s.thumbnail_source, s.thumbnail_blurhash, + s.logo_source, s.logo_blurhash, s.trailer, s.external_id, s.start_air AS air_date, NULL as path, + 'show'::item_kind AS kind + FROM shows AS s + UNION ALL + SELECT + -m.id, m.slug, m.name, m.tagline, m.aliases, m.overview, m.tags, m.genres, m.status, + m.air_date as start_air, m.air_date as end_air, m.poster_source, m.poster_blurhash, m.thumbnail_source, + m.thumbnail_blurhash, m.logo_source, m.logo_blurhash, m.trailer, m.external_id, m.air_date, m.path, + 'movie'::item_kind AS kind + FROM movies AS m + UNION ALL + SELECT + c.id + 10000 AS id, c.slug, c.name, NULL as tagline, NULL as alises, c.overview, NULL AS tags, NULL AS genres, 'unknown'::status AS status, + NULL AS start_air, NULL AS end_air, c.poster_source, c.poster_blurhash, c.thumbnail_source, + c.thumbnail_blurhash, c.logo_source, c.logo_blurhash, NULL as trailer, c.external_id, NULL AS air_date, NULL as path, + 'collection'::item_kind AS kind + FROM collections AS c + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // language=PostgreSQL + migrationBuilder.Sql(@"DROP VIEW library_items"); + } + } +} diff --git a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index b0985624..5ac1b0cd 100644 --- a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -1,1186 +1,1499 @@ -// +// using System; -using System.Collections.Generic; using Kyoo.Abstractions.Models; +using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Kyoo.Postgresql.Migrations { - [DbContext(typeof(PostgresContext))] - partial class PostgresContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { + [DbContext(typeof(PostgresContext))] + partial class PostgresContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { #pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + modelBuilder + .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "action", "adventure", "animation", "comedy", "crime", "documentary", "drama", "family", "fantasy", "history", "horror", "music", "mystery", "romance", "science_fiction", "thriller", "war", "western" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_kind", new[] { "show", "movie", "collection" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_collections"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_collections_slug"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsoluteNumber") + .HasColumnType("integer") + .HasColumnName("absolute_number"); + + b.Property("EpisodeNumber") + .HasColumnType("integer") + .HasColumnName("episode_number"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("release_date"); + + b.Property("SeasonID") + .HasColumnType("integer") + .HasColumnName("season_id"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_episodes"); + + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); + + b.ToTable("episodes", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AirDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date"); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("EndAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_air"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_library_items"); + + b.ToTable("library_items", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AirDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date"); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_movies"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_movies_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_movies_studio_id"); + + b.ToTable("movies", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_people"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_people_slug"); + + b.ToTable("people", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MovieID") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("PeopleID") + .HasColumnType("integer") + .HasColumnName("people_id"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_people_roles"); + + b.HasIndex("MovieID") + .HasDatabaseName("ix_people_roles_movie_id"); + + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); + + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); + + b.ToTable("people_roles", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_date"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("pk_seasons"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); + + b.ToTable("seasons", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("EndAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_air"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_shows"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_shows_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); + + b.ToTable("shows", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_studios"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_studios_slug"); + + b.ToTable("studios", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("permissions"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_users_slug"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("EpisodeID") + .HasColumnType("integer") + .HasColumnName("episode_id"); + + b.Property("WatchedPercentage") + .HasColumnType("integer") + .HasColumnName("watched_percentage"); + + b.HasKey("UserID", "EpisodeID") + .HasName("pk_watched_episode"); + + b.HasIndex("EpisodeID") + .HasDatabaseName("ix_watched_episode_episode_id"); + + b.ToTable("watched_episode", (string)null); + }); + + modelBuilder.Entity("ShowUser", b => + { + b.Property("UsersId") + .HasColumnType("integer") + .HasColumnName("users_id"); + + b.Property("WatchedId") + .HasColumnType("integer") + .HasColumnName("watched_id"); + + b.HasKey("UsersId", "WatchedId") + .HasName("pk_link_user_show"); + + b.HasIndex("WatchedId") + .HasDatabaseName("ix_link_user_show_watched_id"); + + b.ToTable("link_user_show", (string)null); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.Property("collection_id") + .HasColumnType("integer") + .HasColumnName("collection_id"); + + b.Property("movie_id") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.HasKey("collection_id", "movie_id") + .HasName("pk_link_collection_movie"); + + b.HasIndex("movie_id") + .HasDatabaseName("ix_link_collection_movie_movie_id"); + + b.ToTable("link_collection_movie", (string)null); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.Property("collection_id") + .HasColumnType("integer") + .HasColumnName("collection_id"); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_type", new[] { "show", "movie", "collection" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" }); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + b.Property("show_id") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.HasKey("collection_id", "show_id") + .HasName("pk_link_collection_show"); + + b.HasIndex("show_id") + .HasDatabaseName("ix_link_collection_show_show_id"); + + b.ToTable("link_collection_show", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("CollectionId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.HasOne("Kyoo.Abstractions.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_episodes_seasons_season_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_episodes_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); - modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); - b.HasKey("ID") - .HasName("pk_collections"); + b1.HasKey("EpisodeId"); - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_collections_slug"); + b1.ToTable("episodes"); - b.ToTable("collections", (string)null); - }); + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Season"); + + b.Navigation("Show"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("LibraryItemId"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); + b1.ToTable("library_items"); - b.Property("AbsoluteNumber") - .HasColumnType("integer") - .HasColumnName("absolute_number"); + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); - b.Property("EpisodeNumber") - .HasColumnType("integer") - .HasColumnName("episode_number"); + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("LibraryItemId"); - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); + b1.ToTable("library_items"); - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("LibraryItemId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("LibraryItemId"); - b.Property("ReleaseDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("release_date"); + b1.ToTable("library_items"); - b.Property("SeasonID") - .HasColumnType("integer") - .HasColumnName("season_id"); + b1.WithOwner() + .HasForeignKey("LibraryItemId") + .HasConstraintName("fk_library_items_library_items_id"); + }); - b.Property("SeasonNumber") - .HasColumnType("integer") - .HasColumnName("season_number"); + b.Navigation("Logo"); - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); + b.Navigation("Poster"); - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); + b.Navigation("Thumbnail"); + }); - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Movies") + .HasForeignKey("StudioID") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_movies_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); - b.HasKey("ID") - .HasName("pk_episodes"); + b1.HasKey("MovieId"); - b.HasIndex("SeasonID") - .HasDatabaseName("ix_episodes_season_id"); + b1.ToTable("movies"); - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_episodes_slug"); + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); - b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique() - .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("PeopleId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("PeopleId"); + + b1.ToTable("people"); + + b1.WithOwner() + .HasForeignKey("PeopleId") + .HasConstraintName("fk_people_people_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Abstractions.Models.Movie", "Movie") + .WithMany("People") + .HasForeignKey("MovieID") + .HasConstraintName("fk_people_roles_movies_movie_id"); + + b.HasOne("Kyoo.Abstractions.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_people_roles_people_people_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id"); + + b.Navigation("Movie"); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_seasons_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("SeasonId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); - b.ToTable("episodes", (string)null); - }); + b.Navigation("Show"); - modelBuilder.Entity("Kyoo.Abstractions.Models.Genre", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_shows_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); + b1.HasKey("ShowId"); - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); + b1.ToTable("shows"); - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_genres"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_genres_slug"); - - b.ToTable("genres", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Library", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Paths") - .HasColumnType("text[]") - .HasColumnName("paths"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_libraries"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_libraries_slug"); - - b.ToTable("libraries", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.LibraryItem", b => - { - b.Property("ID") - .HasColumnType("integer") - .HasColumnName("id"); - - b.Property("EndAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_air"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Slug") - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_air"); - - b.Property("Status") - .HasColumnType("status") - .HasColumnName("status"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.Property("Type") - .HasColumnType("item_type") - .HasColumnName("type"); - - b.HasKey("ID") - .HasName("pk_library_items"); - - b.ToTable((string)null); - - b.ToView("library_items", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_people"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_people_slug"); - - b.ToTable("people", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("PeopleID") - .HasColumnType("integer") - .HasColumnName("people_id"); - - b.Property("Role") - .HasColumnType("text") - .HasColumnName("role"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Type") - .HasColumnType("text") - .HasColumnName("type"); - - b.HasKey("ID") - .HasName("pk_people_roles"); - - b.HasIndex("PeopleID") - .HasDatabaseName("ix_people_roles_people_id"); - - b.HasIndex("ShowID") - .HasDatabaseName("ix_people_roles_show_id"); - - b.ToTable("people_roles", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Provider", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_providers"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_providers_slug"); - - b.ToTable("providers", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_date"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("SeasonNumber") - .HasColumnType("integer") - .HasColumnName("season_number"); - - b.Property("ShowID") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_date"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_seasons"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_seasons_slug"); - - b.HasIndex("ShowID", "SeasonNumber") - .IsUnique() - .HasDatabaseName("ix_seasons_show_id_season_number"); - - b.ToTable("seasons", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Aliases") - .HasColumnType("text[]") - .HasColumnName("aliases"); - - b.Property("EndAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_air"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("IsMovie") - .HasColumnType("boolean") - .HasColumnName("is_movie"); - - b.Property("Overview") - .HasColumnType("text") - .HasColumnName("overview"); - - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("StartAir") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_air"); - - b.Property("Status") - .HasColumnType("status") - .HasColumnName("status"); - - b.Property("StudioID") - .HasColumnType("integer") - .HasColumnName("studio_id"); - - b.Property("Title") - .HasColumnType("text") - .HasColumnName("title"); - - b.HasKey("ID") - .HasName("pk_shows"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_shows_slug"); - - b.HasIndex("StudioID") - .HasDatabaseName("ix_shows_studio_id"); - - b.ToTable("shows", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("ID") - .HasName("pk_studios"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_studios_slug"); - - b.ToTable("studios", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); - - b.Property("Email") - .HasColumnType("text") - .HasColumnName("email"); - - b.Property>("ExtraData") - .HasColumnType("jsonb") - .HasColumnName("extra_data"); - - b.Property>("Images") - .HasColumnType("jsonb") - .HasColumnName("images"); - - b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); - - b.Property("Permissions") - .HasColumnType("text[]") - .HasColumnName("permissions"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Username") - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("ID") - .HasName("pk_users"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_users_slug"); - - b.ToTable("users", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => - { - b.Property("UserID") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("EpisodeID") - .HasColumnType("integer") - .HasColumnName("episode_id"); - - b.Property("WatchedPercentage") - .HasColumnType("integer") - .HasColumnName("watched_percentage"); - - b.HasKey("UserID", "EpisodeID") - .HasName("pk_watched_episodes"); - - b.HasIndex("EpisodeID") - .HasDatabaseName("ix_watched_episodes_episode_id"); - - b.ToTable("watched_episodes", (string)null); - }); - - modelBuilder.Entity("ShowUser", b => - { - b.Property("UsersID") - .HasColumnType("integer") - .HasColumnName("users_id"); - - b.Property("WatchedID") - .HasColumnType("integer") - .HasColumnName("watched_id"); - - b.HasKey("UsersID", "WatchedID") - .HasName("pk_link_user_show"); - - b.HasIndex("WatchedID") - .HasDatabaseName("ix_link_user_show_watched_id"); - - b.ToTable("link_user_show", (string)null); - }); - - modelBuilder.Entity("collection_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_collection_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_collection_metadata_id_provider_id"); - - b.ToTable("collection_metadata_id", (string)null); - }); - - modelBuilder.Entity("episode_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_episode_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_episode_metadata_id_provider_id"); - - b.ToTable("episode_metadata_id", (string)null); - }); - - modelBuilder.Entity("link_collection_show", b => - { - b.Property("collection_id") - .HasColumnType("integer") - .HasColumnName("collection_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("collection_id", "show_id") - .HasName("pk_link_collection_show"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_collection_show_show_id"); - - b.ToTable("link_collection_show", (string)null); - }); - - modelBuilder.Entity("link_library_collection", b => - { - b.Property("collection_id") - .HasColumnType("integer") - .HasColumnName("collection_id"); - - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.HasKey("collection_id", "library_id") - .HasName("pk_link_library_collection"); - - b.HasIndex("library_id") - .HasDatabaseName("ix_link_library_collection_library_id"); - - b.ToTable("link_library_collection", (string)null); - }); - - modelBuilder.Entity("link_library_provider", b => - { - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.Property("provider_id") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.HasKey("library_id", "provider_id") - .HasName("pk_link_library_provider"); - - b.HasIndex("provider_id") - .HasDatabaseName("ix_link_library_provider_provider_id"); - - b.ToTable("link_library_provider", (string)null); - }); - - modelBuilder.Entity("link_library_show", b => - { - b.Property("library_id") - .HasColumnType("integer") - .HasColumnName("library_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("library_id", "show_id") - .HasName("pk_link_library_show"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_library_show_show_id"); - - b.ToTable("link_library_show", (string)null); - }); - - modelBuilder.Entity("link_show_genre", b => - { - b.Property("genre_id") - .HasColumnType("integer") - .HasColumnName("genre_id"); - - b.Property("show_id") - .HasColumnType("integer") - .HasColumnName("show_id"); - - b.HasKey("genre_id", "show_id") - .HasName("pk_link_show_genre"); - - b.HasIndex("show_id") - .HasDatabaseName("ix_link_show_genre_show_id"); - - b.ToTable("link_show_genre", (string)null); - }); - - modelBuilder.Entity("people_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_people_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_people_metadata_id_provider_id"); - - b.ToTable("people_metadata_id", (string)null); - }); - - modelBuilder.Entity("season_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_season_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_season_metadata_id_provider_id"); - - b.ToTable("season_metadata_id", (string)null); - }); - - modelBuilder.Entity("show_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_show_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_show_metadata_id_provider_id"); - - b.ToTable("show_metadata_id", (string)null); - }); - - modelBuilder.Entity("studio_metadata_id", b => - { - b.Property("ResourceID") - .HasColumnType("integer") - .HasColumnName("resource_id"); - - b.Property("ProviderID") - .HasColumnType("integer") - .HasColumnName("provider_id"); - - b.Property("DataID") - .HasColumnType("text") - .HasColumnName("data_id"); - - b.Property("Link") - .HasColumnType("text") - .HasColumnName("link"); - - b.HasKey("ResourceID", "ProviderID") - .HasName("pk_studio_metadata_id"); - - b.HasIndex("ProviderID") - .HasDatabaseName("ix_studio_metadata_id_provider_id"); - - b.ToTable("studio_metadata_id", (string)null); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.HasOne("Kyoo.Abstractions.Models.Season", "Season") - .WithMany("Episodes") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade) - .HasConstraintName("fk_episodes_seasons_season_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("Episodes") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episodes_shows_show_id"); - - b.Navigation("Season"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.PeopleRole", b => - { - b.HasOne("Kyoo.Abstractions.Models.People", "People") - .WithMany("Roles") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_roles_people_people_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("People") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_roles_shows_show_id"); - - b.Navigation("People"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.HasOne("Kyoo.Abstractions.Models.Show", "Show") - .WithMany("Seasons") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_seasons_shows_show_id"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") - .WithMany("Shows") - .HasForeignKey("StudioID") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("fk_shows_studios_studio_id"); - - b.Navigation("Studio"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => - { - b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") - .WithMany() - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_watched_episodes_episodes_episode_id"); - - b.HasOne("Kyoo.Abstractions.Models.User", null) - .WithMany("CurrentlyWatching") - .HasForeignKey("UserID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_watched_episodes_users_user_id"); - - b.Navigation("Episode"); - }); - - modelBuilder.Entity("ShowUser", b => - { - b.HasOne("Kyoo.Abstractions.Models.User", null) - .WithMany() - .HasForeignKey("UsersID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_user_show_users_users_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("WatchedID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_user_show_shows_watched_id"); - }); - - modelBuilder.Entity("collection_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_collection_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_collection_metadata_id_collections_collection_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("episode_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episode_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Episode", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_episode_metadata_id_episodes_episode_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("link_collection_show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany() - .HasForeignKey("collection_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_collection_show_collections_collection_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_collection_show_shows_show_id"); - }); - - modelBuilder.Entity("link_library_collection", b => - { - b.HasOne("Kyoo.Abstractions.Models.Collection", null) - .WithMany() - .HasForeignKey("collection_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_collection_collections_collection_id"); - - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_collection_libraries_library_id"); - }); - - modelBuilder.Entity("link_library_provider", b => - { - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_provider_libraries_library_id"); - - b.HasOne("Kyoo.Abstractions.Models.Provider", null) - .WithMany() - .HasForeignKey("provider_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_provider_providers_provider_id"); - }); - - modelBuilder.Entity("link_library_show", b => - { - b.HasOne("Kyoo.Abstractions.Models.Library", null) - .WithMany() - .HasForeignKey("library_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_show_libraries_library_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_library_show_shows_show_id"); - }); - - modelBuilder.Entity("link_show_genre", b => - { - b.HasOne("Kyoo.Abstractions.Models.Genre", null) - .WithMany() - .HasForeignKey("genre_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_show_genre_genres_genre_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany() - .HasForeignKey("show_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_link_show_genre_shows_show_id"); - }); - - modelBuilder.Entity("people_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.People", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_people_metadata_id_people_people_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("season_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_season_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Season", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_season_metadata_id_seasons_season_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("show_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_show_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Show", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_show_metadata_id_shows_show_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("studio_metadata_id", b => - { - b.HasOne("Kyoo.Abstractions.Models.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_studio_metadata_id_providers_provider_id"); - - b.HasOne("Kyoo.Abstractions.Models.Studio", null) - .WithMany("ExternalIDs") - .HasForeignKey("ResourceID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_studio_metadata_id_studios_studio_id"); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => - { - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => - { - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Roles"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - - b.Navigation("People"); - - b.Navigation("Seasons"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Shows"); - }); - - modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => - { - b.Navigation("CurrentlyWatching"); - }); + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("poster_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("poster_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("ShowId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("thumbnail_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("thumbnail_source"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("UserId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("logo_blurhash"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("logo_source"); + + b1.HasKey("UserId"); + + b1.ToTable("users"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_id"); + }); + + b.Navigation("Logo"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") + .WithMany() + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_watched_episode_episodes_episode_id"); + + b.HasOne("Kyoo.Abstractions.Models.User", null) + .WithMany("CurrentlyWatching") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_watched_episode_users_user_id"); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ShowUser", b => + { + b.HasOne("Kyoo.Abstractions.Models.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_user_show_users_users_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", null) + .WithMany() + .HasForeignKey("WatchedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_user_show_shows_watched_id"); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Movie", null) + .WithMany() + .HasForeignKey("movie_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_movies_movie_id"); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", null) + .WithMany() + .HasForeignKey("show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_shows_show_id"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.People", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Navigation("Episodes"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Navigation("Movies"); + + b.Navigation("Shows"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + }); #pragma warning restore 612, 618 - } - } + } + } } diff --git a/back/src/Kyoo.Postgresql/PostgresContext.cs b/back/src/Kyoo.Postgresql/PostgresContext.cs index 8266fa1f..71ec5117 100644 --- a/back/src/Kyoo.Postgresql/PostgresContext.cs +++ b/back/src/Kyoo.Postgresql/PostgresContext.cs @@ -33,11 +33,6 @@ namespace Kyoo.Postgresql /// public class PostgresContext : DatabaseContext { - /// - /// The connection string to use. - /// - private readonly string _connection; - /// /// Is this instance in debug mode? /// @@ -48,12 +43,13 @@ namespace Kyoo.Postgresql /// private readonly bool _skipConfigure; - // TOOD: This needs ot be updated but ef-core still does not offer a way to use this. + // TODO: This needs ot be updated but ef-core still does not offer a way to use this. [Obsolete] static PostgresContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); } /// @@ -78,7 +74,6 @@ namespace Kyoo.Postgresql /// Is this instance in debug mode? public PostgresContext(string connection, bool debugMode) { - _connection = connection; _debugMode = debugMode; } @@ -106,51 +101,12 @@ namespace Kyoo.Postgresql protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasPostgresEnum(); - modelBuilder.HasPostgresEnum(); - - modelBuilder.Entity() - .ToView("library_items") - .HasKey(x => x.ID); - - modelBuilder.Entity() - .Property(x => x.ExtraData) - .HasColumnType("jsonb"); - - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); - modelBuilder.Entity() - .Property(x => x.Images) - .HasColumnType("jsonb"); + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); base.OnModelCreating(modelBuilder); } - /// - protected override string MetadataName() - { - SnakeCaseNameRewriter rewriter = new(CultureInfo.InvariantCulture); - return rewriter.RewriteName(typeof(T).Name + nameof(MetadataID)); - } - /// protected override string LinkName() { diff --git a/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs b/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs index e2d14bf9..25781efb 100644 --- a/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs +++ b/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs @@ -38,7 +38,7 @@ namespace Kyoo.Swagger { context.OperationDescription.Operation.Security ??= new List(); OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes() - .Aggregate(new OpenApiSecurityRequirement(), (agg, cur) => + .Aggregate(new OpenApiSecurityRequirement(), (agg, _) => { agg[nameof(Kyoo)] = Array.Empty(); return agg; @@ -60,15 +60,15 @@ namespace Kyoo.Swagger perms = context.MethodInfo.GetCustomAttributes() .Aggregate(perms, (agg, cur) => { - Group group = controller.Group != Group.Overall + Group? group = controller.Group != Group.Overall ? controller.Group : cur.Group; string type = controller.Type ?? cur.Type; - Kind kind = controller.Type == null + Kind? kind = controller.Type == null ? controller.Kind : cur.Kind; - ICollection permissions = _GetPermissionsList(agg, group); - permissions.Add($"{type}.{kind.ToString().ToLower()}"); + ICollection permissions = _GetPermissionsList(agg, group ?? Group.Overall); + permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}"); agg[nameof(Kyoo)] = permissions; return agg; }); diff --git a/back/src/Kyoo.Swagger/SwaggerModule.cs b/back/src/Kyoo.Swagger/SwaggerModule.cs index 886da471..c3e9ffa4 100644 --- a/back/src/Kyoo.Swagger/SwaggerModule.cs +++ b/back/src/Kyoo.Swagger/SwaggerModule.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; using System.Reflection; using Kyoo.Abstractions.Controllers; @@ -87,7 +86,6 @@ namespace Kyoo.Swagger x.IsNullableRaw = false; x.Type = JsonObjectType.String | JsonObjectType.Integer; })); - document.SchemaProcessors.Add(new ThumbnailProcessor()); document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme { diff --git a/back/src/Kyoo.Swagger/ThumbnailProcessor.cs b/back/src/Kyoo.Swagger/ThumbnailProcessor.cs deleted file mode 100644 index 591c8cd7..00000000 --- a/back/src/Kyoo.Swagger/ThumbnailProcessor.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using Kyoo.Abstractions.Models; -using NJsonSchema; -using NJsonSchema.Generation; - -namespace Kyoo.Swagger -{ - /// - /// An operation processor to add computed fields of . - /// - public class ThumbnailProcessor : ISchemaProcessor - { - /// - public void Process(SchemaProcessorContext context) - { - if (!context.ContextualType.OriginalType.IsAssignableTo(typeof(IThumbnails))) - return; - foreach ((int _, string imageP) in Images.ImageName) - { - string image = imageP.ToLower()[0] + imageP[1..]; - context.Schema.Properties.Add(image, new JsonSchemaProperty - { - Type = JsonObjectType.String, - IsNullableRaw = true, - Description = $"An url to the {image} of this resource. If this resource does not have an image, " + - $"the link will be null. If the kyoo's instance is not capable of handling this kind of image " + - $"for the specific resource, this field won't be present." - }); - } - } - } -} diff --git a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs index e320fd52..99fd8dcc 100644 --- a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs +++ b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs @@ -22,6 +22,7 @@ using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Core.Controllers; using Kyoo.Postgresql; +using Moq; using Xunit.Abstractions; namespace Kyoo.Tests.Database @@ -37,31 +38,28 @@ namespace Kyoo.Tests.Database { Context = new PostgresTestContext(postgres, output); - ProviderRepository provider = new(_NewContext()); - LibraryRepository library = new(_NewContext(), provider); - CollectionRepository collection = new(_NewContext(), provider); - GenreRepository genre = new(_NewContext()); - StudioRepository studio = new(_NewContext(), provider); - PeopleRepository people = new(_NewContext(), provider, - new Lazy(() => LibraryManager.ShowRepository)); - ShowRepository show = new(_NewContext(), studio, people, genre, provider); - SeasonRepository season = new(_NewContext(), show, provider); - LibraryItemRepository libraryItem = new(_NewContext(), - new Lazy(() => LibraryManager.LibraryRepository)); - EpisodeRepository episode = new(_NewContext(), show, provider); - UserRepository user = new(_NewContext()); + Mock thumbs = new(); + CollectionRepository collection = new(_NewContext(), thumbs.Object); + StudioRepository studio = new(_NewContext(), thumbs.Object); + PeopleRepository people = new(_NewContext(), + new Lazy(() => LibraryManager.ShowRepository), + thumbs.Object); + MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object); + ShowRepository show = new(_NewContext(), studio, people, thumbs.Object); + SeasonRepository season = new(_NewContext(), show, thumbs.Object); + LibraryItemRepository libraryItem = new(_NewContext(), thumbs.Object); + EpisodeRepository episode = new(_NewContext(), show, thumbs.Object); + UserRepository user = new(_NewContext(), thumbs.Object); LibraryManager = new LibraryManager(new IBaseRepository[] { - provider, - library, libraryItem, collection, + movies, show, season, episode, people, studio, - genre, user }); } diff --git a/back/tests/Kyoo.Tests/Database/RepositoryTests.cs b/back/tests/Kyoo.Tests/Database/RepositoryTests.cs index fd21f8ba..eada621e 100644 --- a/back/tests/Kyoo.Tests/Database/RepositoryTests.cs +++ b/back/tests/Kyoo.Tests/Database/RepositoryTests.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; @@ -30,7 +29,7 @@ using Xunit; namespace Kyoo.Tests.Database { public abstract class RepositoryTests : IDisposable, IAsyncDisposable - where T : class, IResource, new() + where T : class, IResource { protected readonly RepositoryActivator Repositories; private readonly IRepository _repository; @@ -63,7 +62,7 @@ namespace Kyoo.Tests.Database [Fact] public async Task GetByIdTest() { - T value = await _repository.Get(TestSample.Get().ID); + T value = await _repository.Get(TestSample.Get().Id); KAssert.DeepEqual(TestSample.Get(), value); } @@ -89,7 +88,7 @@ namespace Kyoo.Tests.Database [Fact] public async Task DeleteByIdTest() { - await _repository.Delete(TestSample.Get().ID); + await _repository.Delete(TestSample.Get().Id); Assert.Equal(0, await _repository.GetCount()); } @@ -114,23 +113,11 @@ namespace Kyoo.Tests.Database await _repository.Delete(TestSample.Get()); T expected = TestSample.Get(); - expected.ID = 0; + expected.Id = 0; 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 virtual async Task CreateIfNotExistTest() { @@ -140,22 +127,16 @@ namespace Kyoo.Tests.Database 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 EditNonExistingTest() + // { + // await Assert.ThrowsAsync(() => _repository.Edit(new T { Id = 56 })); + // } [Fact] public async Task GetExpressionIDTest() { - KAssert.DeepEqual(TestSample.Get(), await _repository.Get(x => x.ID == TestSample.Get().ID)); + KAssert.DeepEqual(TestSample.Get(), await _repository.Get(x => x.Id == TestSample.Get().Id)); } [Fact] @@ -170,12 +151,6 @@ namespace Kyoo.Tests.Database 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() { diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs index 1d6bdd9f..31fb2b3d 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs @@ -66,40 +66,29 @@ namespace Kyoo.Tests.Database Assert.Equal("2!", ret.Slug); } - [Fact] - public async Task CreateWithoutNameTest() - { - Collection collection = TestSample.GetNew(); - collection.Name = null; - await Assert.ThrowsAsync(() => _repository.Create(collection)); - } - [Fact] public async Task CreateWithExternalIdTest() { Collection collection = TestSample.GetNew(); - collection.ExternalIDs = new[] + collection.ExternalId = new Dictionary { - new MetadataID + ["1"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, - new MetadataID + ["2"] = new() { - Provider = TestSample.GetNew(), Link = "new-provider-link", - DataID = "new-id" + DataId = "new-id" } }; await _repository.Create(collection); Collection retrieved = await _repository.Get(2); - await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs); - Assert.Equal(2, retrieved.ExternalIDs.Count); - KAssert.DeepEqual(collection.ExternalIDs.First(), retrieved.ExternalIDs.First()); - KAssert.DeepEqual(collection.ExternalIDs.Last(), retrieved.ExternalIDs.Last()); + Assert.Equal(2, retrieved.ExternalId.Count); + KAssert.DeepEqual(collection.ExternalId.First(), retrieved.ExternalId.First()); + KAssert.DeepEqual(collection.ExternalId.Last(), retrieved.ExternalId.Last()); } [Fact] @@ -107,11 +96,8 @@ namespace Kyoo.Tests.Database { Collection value = await _repository.Get(TestSample.Get().Slug); value.Name = "New Title"; - value.Images = new Dictionary - { - [Images.Poster] = "new-poster" - }; - await _repository.Edit(value, false); + value.Poster = new Image("new-poster"); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); Collection retrieved = await database.Collections.FirstAsync(); @@ -123,22 +109,18 @@ namespace Kyoo.Tests.Database public async Task EditMetadataTest() { Collection value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new[] + value.ExternalId = new Dictionary { - new MetadataID + ["test"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); - Collection retrieved = await database.Collections - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Collection retrieved = await database.Collections.FirstAsync(); KAssert.DeepEqual(value, retrieved); } @@ -147,41 +129,33 @@ namespace Kyoo.Tests.Database public async Task AddMetadataTest() { Collection value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new List + value.ExternalId = new Dictionary { - new() + ["toto"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - Collection retrieved = await database.Collections - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Collection retrieved = await database.Collections.FirstAsync(); KAssert.DeepEqual(value, retrieved); } - value.ExternalIDs.Add(new MetadataID + value.ExternalId.Add("test", new MetadataId { - Provider = TestSample.GetNew(), Link = "link", - DataID = "id" + DataId = "id" }); - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - Collection retrieved = await database.Collections - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Collection retrieved = await database.Collections.FirstAsync(); KAssert.DeepEqual(value, retrieved); } diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs index 22b1befb..988ec0c3 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs @@ -55,12 +55,11 @@ namespace Kyoo.Tests.Database { Episode episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - Show show = new() + await Repositories.LibraryManager.ShowRepository.Patch(episode.ShowId, (x) => { - ID = episode.ShowID, - Slug = "new-slug" - }; - await Repositories.LibraryManager.ShowRepository.Edit(show, false); + x.Slug = "new-slug"; + return Task.FromResult(true); + }); episode = await _repository.Get(1); Assert.Equal("new-slug-s1e1", episode.Slug); } @@ -70,12 +69,11 @@ namespace Kyoo.Tests.Database { Episode episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - episode = await _repository.Edit(new Episode + episode = await _repository.Patch(1, (x) => { - ID = 1, - SeasonNumber = 2, - ShowID = 1 - }, false); + x.SeasonNumber = 2; + return Task.FromResult(true); + }); Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); @@ -86,12 +84,11 @@ namespace Kyoo.Tests.Database { Episode episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - episode = await _repository.Edit(new Episode + episode = await Repositories.LibraryManager.Patch(episode.Id, (x) => { - ID = 1, - EpisodeNumber = 2, - ShowID = 1 - }, false); + x.EpisodeNumber = 2; + return Task.FromResult(true); + }); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); @@ -100,12 +97,12 @@ namespace Kyoo.Tests.Database [Fact] public async Task EpisodeCreationSlugTest() { - Episode episode = await _repository.Create(new Episode - { - ShowID = TestSample.Get().ID, - SeasonNumber = 2, - EpisodeNumber = 4 - }); + Episode model = TestSample.Get(); + model.Id = 0; + model.ShowId = TestSample.Get().Id; + model.SeasonNumber = 2; + model.EpisodeNumber = 4; + Episode episode = await _repository.Create(model); Assert.Equal($"{TestSample.Get().Slug}-s2e4", episode.Slug); } @@ -127,12 +124,11 @@ namespace Kyoo.Tests.Database public async Task SlugEditAbsoluteTest() { Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); - Show show = new() + await Repositories.LibraryManager.ShowRepository.Patch(episode.ShowId, (x) => { - ID = episode.ShowID, - Slug = "new-slug" - }; - await Repositories.LibraryManager.ShowRepository.Edit(show, false); + x.Slug = "new-slug"; + return Task.FromResult(true); + }); episode = await _repository.Get(2); Assert.Equal($"new-slug-3", episode.Slug); } @@ -141,12 +137,11 @@ namespace Kyoo.Tests.Database public async Task AbsoluteNumberEditTest() { await _repository.Create(TestSample.GetAbsoluteEpisode()); - Episode episode = await _repository.Edit(new Episode + Episode episode = await _repository.Patch(2, (x) => { - ID = 2, - AbsoluteNumber = 56, - ShowID = 1 - }, false); + x.AbsoluteNumber = 56; + return Task.FromResult(true); + }); Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); episode = await _repository.Get(2); Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); @@ -156,13 +151,12 @@ namespace Kyoo.Tests.Database public async Task AbsoluteToNormalEditTest() { await _repository.Create(TestSample.GetAbsoluteEpisode()); - Episode episode = await _repository.Edit(new Episode + Episode episode = await _repository.Patch(2, (x) => { - ID = 2, - SeasonNumber = 1, - EpisodeNumber = 2, - ShowID = 1 - }, false); + x.SeasonNumber = 1; + x.EpisodeNumber = 2; + return Task.FromResult(true); + }); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); episode = await _repository.Get(2); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); @@ -174,72 +168,44 @@ namespace Kyoo.Tests.Database Episode episode = await _repository.Get(1); episode.SeasonNumber = null; episode.AbsoluteNumber = 12; - episode = await _repository.Edit(episode, true); + episode = await _repository.Edit(episode); Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); episode = await _repository.Get(1); Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); } - [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); - } - [Fact] public async Task CreateWithExternalIdTest() { Episode value = TestSample.GetNew(); - value.ExternalIDs = new[] + value.ExternalId = new Dictionary { - new MetadataID + ["2"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, - new MetadataID + ["3"] = new() { - Provider = TestSample.GetNew(), Link = "new-provider-link", - DataID = "new-id" + DataId = "new-id" } }; await _repository.Create(value); Episode retrieved = await _repository.Get(2); - await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs); - Assert.Equal(2, retrieved.ExternalIDs.Count); - KAssert.DeepEqual(value.ExternalIDs.First(), retrieved.ExternalIDs.First()); - KAssert.DeepEqual(value.ExternalIDs.Last(), retrieved.ExternalIDs.Last()); + Assert.Equal(2, retrieved.ExternalId.Count); + KAssert.DeepEqual(value.ExternalId.First(), retrieved.ExternalId.First()); + KAssert.DeepEqual(value.ExternalId.Last(), retrieved.ExternalId.Last()); } [Fact] public async Task EditTest() { Episode value = await _repository.Get(TestSample.Get().Slug); - value.Title = "New Title"; - value.Images = new Dictionary - { - [Images.Poster] = "new-poster" - }; - await _repository.Edit(value, false); + value.Name = "New Title"; + value.Poster = new Image("poster"); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); Episode retrieved = await database.Episodes.FirstAsync(); @@ -251,22 +217,18 @@ namespace Kyoo.Tests.Database public async Task EditMetadataTest() { Episode value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new[] + value.ExternalId = new Dictionary { - new MetadataID + ["1"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); - Episode retrieved = await database.Episodes - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Episode retrieved = await database.Episodes.FirstAsync(); KAssert.DeepEqual(value, retrieved); } @@ -275,41 +237,33 @@ namespace Kyoo.Tests.Database public async Task AddMetadataTest() { Episode value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new List + value.ExternalId = new Dictionary { - new() + ["toto"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - Episode retrieved = await database.Episodes - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Episode retrieved = await database.Episodes.FirstAsync(); KAssert.DeepEqual(value, retrieved); } - value.ExternalIDs.Add(new MetadataID + value.ExternalId.Add("test", new MetadataId { - Provider = TestSample.GetNew(), Link = "link", - DataID = "id" + DataId = "id" }); - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - Episode retrieved = await database.Episodes - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Episode retrieved = await database.Episodes.FirstAsync(); KAssert.DeepEqual(value, retrieved); } @@ -323,12 +277,10 @@ namespace Kyoo.Tests.Database [InlineData("SuPeR")] public async Task SearchTest(string query) { - Episode value = new() - { - Title = "This is a test super title", - ShowID = 1, - AbsoluteNumber = 2 - }; + Episode value = TestSample.Get(); + value.Id = 0; + value.Name = "This is a test super title"; + value.EpisodeNumber = 56; await _repository.Create(value); ICollection ret = await _repository.Search(query); value.Show = TestSample.Get(); @@ -342,9 +294,9 @@ namespace Kyoo.Tests.Database await _repository.Delete(TestSample.Get()); Episode expected = TestSample.Get(); - expected.ID = 0; - expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get())).ID; - expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get())).ID; + expected.Id = 0; + expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get())).Id; + expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get())).Id; await _repository.Create(expected); KAssert.DeepEqual(expected, await _repository.Get(expected.Slug)); } @@ -355,8 +307,8 @@ namespace Kyoo.Tests.Database Episode expected = TestSample.Get(); KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get())); await _repository.Delete(TestSample.Get()); - expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get())).ID; - expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get())).ID; + expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get())).Id; + expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get())).Id; KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected)); } } diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs deleted file mode 100644 index db572ea1..00000000 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Xunit; -using Xunit.Abstractions; - -namespace Kyoo.Tests.Database -{ - 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; - private readonly RepositoryActivator _repositories; - - protected ALibraryItemTest(RepositoryActivator repositories) - { - _repositories = 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); - } - - [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, - Name = "name" - }); - await Assert.ThrowsAsync(() => _repository.Get(TestSample.Get().Slug)); - } - } -} diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs deleted file mode 100644 index 57a7deea..00000000 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Postgresql; -using Kyoo.Utils; -using Microsoft.EntityFrameworkCore; -using Xunit; -using Xunit.Abstractions; - -namespace Kyoo.Tests.Database -{ - 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; - } - - [Fact] - public async Task CreateWithoutPathTest() - { - Library library = TestSample.GetNew(); - library.Paths = null; - await Assert.ThrowsAsync(() => _repository.Create(library)); - } - - [Fact] - public async Task CreateWithEmptySlugTest() - { - Library library = TestSample.GetNew(); - library.Slug = string.Empty; - await Assert.ThrowsAsync(() => _repository.Create(library)); - } - - [Fact] - public async Task CreateWithNumberSlugTest() - { - Library library = TestSample.GetNew(); - library.Slug = "2"; - Library ret = await _repository.Create(library); - Assert.Equal("2!", ret.Slug); - } - - [Fact] - public async Task CreateWithoutNameTest() - { - Library library = TestSample.GetNew(); - library.Name = null; - await Assert.ThrowsAsync(() => _repository.Create(library)); - } - - [Fact] - public async Task CreateWithProvider() - { - Library library = TestSample.GetNew(); - library.Providers = new[] { TestSample.Get() }; - await _repository.Create(library); - Library retrieved = await _repository.Get(2); - await Repositories.LibraryManager.Load(retrieved, x => x.Providers); - Assert.Single(retrieved.Providers); - Assert.Equal(TestSample.Get().Slug, retrieved.Providers.First().Slug); - } - - [Fact] - public async Task EditTest() - { - Library value = await _repository.Get(TestSample.Get().Slug); - value.Paths = new[] { "/super", "/test" }; - value.Name = "New Title"; - Library edited = await _repository.Edit(value, false); - - await using DatabaseContext database = Repositories.Context.New(); - Library show = await database.Libraries.FirstAsync(); - - KAssert.DeepEqual(show, edited); - } - - [Fact] - public async Task EditProvidersTest() - { - Library value = await _repository.Get(TestSample.Get().Slug); - value.Providers = new[] - { - TestSample.GetNew() - }; - Library edited = await _repository.Edit(value, false); - - await using DatabaseContext database = Repositories.Context.New(); - Library show = await database.Libraries - .Include(x => x.Providers) - .FirstAsync(); - - show.Providers.ForEach(x => x.Libraries = null); - edited.Providers.ForEach(x => x.Libraries = null); - KAssert.DeepEqual(show, edited); - } - - [Fact] - public async Task AddProvidersTest() - { - Library value = await _repository.Get(TestSample.Get().Slug); - await Repositories.LibraryManager.Load(value, x => x.Providers); - value.Providers.Add(TestSample.GetNew()); - Library edited = await _repository.Edit(value, false); - - await using DatabaseContext database = Repositories.Context.New(); - Library show = await database.Libraries - .Include(x => x.Providers) - .FirstAsync(); - - show.Providers.ForEach(x => x.Libraries = null); - edited.Providers.ForEach(x => x.Libraries = null); - KAssert.DeepEqual(show, edited); - } - - [Theory] - [InlineData("test")] - [InlineData("super")] - [InlineData("title")] - [InlineData("TiTlE")] - [InlineData("SuPeR")] - public async Task SearchTest(string query) - { - Library value = new() - { - Slug = "super-test", - Name = "This is a test title", - Paths = new[] { "path" } - }; - await _repository.Create(value); - ICollection ret = await _repository.Search(query); - KAssert.DeepEqual(value, ret.First()); - } - } -} diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs index 50dbf151..8bf3b9da 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs @@ -52,28 +52,25 @@ namespace Kyoo.Tests.Database public async Task CreateWithExternalIdTest() { People value = TestSample.GetNew(); - value.ExternalIDs = new[] + value.ExternalId = new Dictionary { - new MetadataID + ["2"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, - new MetadataID + ["1"] = new() { - Provider = TestSample.GetNew(), Link = "new-provider-link", - DataID = "new-id" + DataId = "new-id" } }; await _repository.Create(value); People retrieved = await _repository.Get(2); - await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs); - Assert.Equal(2, retrieved.ExternalIDs.Count); - KAssert.DeepEqual(value.ExternalIDs.First(), retrieved.ExternalIDs.First()); - KAssert.DeepEqual(value.ExternalIDs.Last(), retrieved.ExternalIDs.Last()); + Assert.Equal(2, retrieved.ExternalId.Count); + KAssert.DeepEqual(value.ExternalId.First(), retrieved.ExternalId.First()); + KAssert.DeepEqual(value.ExternalId.Last(), retrieved.ExternalId.Last()); } [Fact] @@ -81,11 +78,8 @@ namespace Kyoo.Tests.Database { People value = await _repository.Get(TestSample.Get().Slug); value.Name = "New Name"; - value.Images = new Dictionary - { - [Images.Poster] = "new-poster" - }; - await _repository.Edit(value, false); + value.Poster = new Image("poster"); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); People retrieved = await database.People.FirstAsync(); @@ -97,22 +91,18 @@ namespace Kyoo.Tests.Database public async Task EditMetadataTest() { People value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new[] + value.ExternalId = new Dictionary { - new MetadataID + ["toto"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); - People retrieved = await database.People - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + People retrieved = await database.People.FirstAsync(); KAssert.DeepEqual(value, retrieved); } @@ -121,41 +111,32 @@ namespace Kyoo.Tests.Database public async Task AddMetadataTest() { People value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new List + value.ExternalId = new Dictionary { - new() + ["1"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - People retrieved = await database.People - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); - + People retrieved = await database.People.FirstAsync(); KAssert.DeepEqual(value, retrieved); } - value.ExternalIDs.Add(new MetadataID + value.ExternalId.Add("toto", new MetadataId { - Provider = TestSample.GetNew(), Link = "link", - DataID = "id" + DataId = "id" }); - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - People retrieved = await database.People - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + People retrieved = await database.People.FirstAsync(); KAssert.DeepEqual(value, retrieved); } diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs index c5017e9a..e67fd367 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs @@ -53,12 +53,11 @@ namespace Kyoo.Tests.Database { Season season = await _repository.Get(1); Assert.Equal("anohana-s1", season.Slug); - Show show = new() + await Repositories.LibraryManager.ShowRepository.Patch(season.ShowId, (x) => { - ID = season.ShowID, - Slug = "new-slug" - }; - await Repositories.LibraryManager.ShowRepository.Edit(show, false); + x.Slug = "new-slug"; + return Task.FromResult(true); + }); season = await _repository.Get(1); Assert.Equal("new-slug-s1", season.Slug); } @@ -68,12 +67,12 @@ namespace Kyoo.Tests.Database { Season season = await _repository.Get(1); Assert.Equal("anohana-s1", season.Slug); - await _repository.Edit(new Season + await _repository.Patch(season.Id, (x) => { - ID = 1, - SeasonNumber = 2, - ShowID = 1 - }, false); + x.SeasonNumber = 2; + return Task.FromResult(true); + } + ); season = await _repository.Get(1); Assert.Equal("anohana-s2", season.Slug); } @@ -83,7 +82,7 @@ namespace Kyoo.Tests.Database { Season season = await _repository.Create(new Season { - ShowID = TestSample.Get().ID, + ShowId = TestSample.Get().Id, SeasonNumber = 2 }); Assert.Equal($"{TestSample.Get().Slug}-s2", season.Slug); @@ -93,40 +92,34 @@ namespace Kyoo.Tests.Database public async Task CreateWithExternalIdTest() { Season season = TestSample.GetNew(); - season.ExternalIDs = new[] + season.ExternalId = new Dictionary { - new MetadataID + ["2"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, - new MetadataID + ["1"] = new() { - Provider = TestSample.GetNew(), Link = "new-provider-link", - DataID = "new-id" + DataId = "new-id" } }; await _repository.Create(season); Season retrieved = await _repository.Get(2); - await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs); - Assert.Equal(2, retrieved.ExternalIDs.Count); - KAssert.DeepEqual(season.ExternalIDs.First(), retrieved.ExternalIDs.First()); - KAssert.DeepEqual(season.ExternalIDs.Last(), retrieved.ExternalIDs.Last()); + Assert.Equal(2, retrieved.ExternalId.Count); + KAssert.DeepEqual(season.ExternalId.First(), retrieved.ExternalId.First()); + KAssert.DeepEqual(season.ExternalId.Last(), retrieved.ExternalId.Last()); } [Fact] public async Task EditTest() { Season value = await _repository.Get(TestSample.Get().Slug); - value.Title = "New Title"; - value.Images = new Dictionary - { - [Images.Poster] = "new-poster" - }; - await _repository.Edit(value, false); + value.Name = "New Title"; + value.Poster = new Image("test"); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); Season retrieved = await database.Seasons.FirstAsync(); @@ -138,22 +131,18 @@ namespace Kyoo.Tests.Database public async Task EditMetadataTest() { Season value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new[] + value.ExternalId = new Dictionary { - new MetadataID + ["toto"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); await using DatabaseContext database = Repositories.Context.New(); - Season retrieved = await database.Seasons - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Season retrieved = await database.Seasons.FirstAsync(); KAssert.DeepEqual(value, retrieved); } @@ -162,41 +151,33 @@ namespace Kyoo.Tests.Database public async Task AddMetadataTest() { Season value = await _repository.Get(TestSample.Get().Slug); - value.ExternalIDs = new List + value.ExternalId = new Dictionary { - new() + ["1"] = new() { - Provider = TestSample.Get(), Link = "link", - DataID = "id" + DataId = "id" }, }; - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - Season retrieved = await database.Seasons - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Season retrieved = await database.Seasons.FirstAsync(); KAssert.DeepEqual(value, retrieved); } - value.ExternalIDs.Add(new MetadataID + value.ExternalId.Add("toto", new MetadataId { - Provider = TestSample.GetNew(), Link = "link", - DataID = "id" + DataId = "id" }); - await _repository.Edit(value, false); + await _repository.Edit(value); { await using DatabaseContext database = Repositories.Context.New(); - Season retrieved = await database.Seasons - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Season retrieved = await database.Seasons.FirstAsync(); KAssert.DeepEqual(value, retrieved); } @@ -212,8 +193,8 @@ namespace Kyoo.Tests.Database { Season value = new() { - Title = "This is a test super title", - ShowID = 1 + Name = "This is a test super title", + ShowId = 1 }; await _repository.Create(value); ICollection ret = await _repository.Search(query); diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs index 5d24ff39..ca537106 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -55,9 +54,8 @@ namespace Kyoo.Tests.Database 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); + value.Name = "New Title"; + Show edited = await _repository.Edit(value); KAssert.DeepEqual(value, edited); await using DatabaseContext database = Repositories.Context.New(); @@ -70,39 +68,34 @@ namespace Kyoo.Tests.Database 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); + value.Genres = new List { Genre.Action }; + Show edited = await _repository.Edit(value); 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 })); + Assert.Equal(value.Genres, edited.Genres); await using DatabaseContext database = Repositories.Context.New(); - Show show = await database.Shows - .Include(x => x.Genres) - .FirstAsync(); + Show show = await database.Shows.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 })); + Assert.Equal(value.Genres, show.Genres); } [Fact] public async Task AddGenreTest() { Show value = await _repository.Get(TestSample.Get().Slug); - await Repositories.LibraryManager.Load(value, x => x.Genres); - value.Genres.Add(new Genre("test")); - Show edited = await _repository.Edit(value, false); + value.Genres.Add(Genre.Drama); + Show edited = await _repository.Edit(value); 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 })); + Assert.Equal(value.Genres, edited.Genres); await using DatabaseContext database = Repositories.Context.New(); - Show show = await database.Shows - .Include(x => x.Genres) - .FirstAsync(); + Show show = await database.Shows.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 })); + Assert.Equal(value.Genres, show.Genres); } [Fact] @@ -110,26 +103,24 @@ namespace Kyoo.Tests.Database { Show value = await _repository.Get(TestSample.Get().Slug); value.Studio = new Studio("studio"); - Show edited = await _repository.Edit(value, false); + Show edited = await _repository.Edit(value); Assert.Equal(value.Slug, edited.Slug); - Assert.Equal("studio", edited.Studio.Slug); + Assert.Equal("studio", edited.Studio!.Slug); await using DatabaseContext database = Repositories.Context.New(); - Show show = await database.Shows - .Include(x => x.Studio) - .FirstAsync(); + Show show = await database.Shows.Include(x => x.Studio).FirstAsync(); Assert.Equal(value.Slug, show.Slug); - Assert.Equal("studio", show.Studio.Slug); + Assert.Equal("studio", show.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); + value.Aliases = new List() { "NiceNewAlias", "SecondAlias" }; + Show edited = await _repository.Edit(value); Assert.Equal(value.Slug, edited.Slug); Assert.Equal(value.Aliases, edited.Aliases); @@ -156,10 +147,10 @@ namespace Kyoo.Tests.Database Role = "NiceCharacter" } }; - Show edited = await _repository.Edit(value, false); + Show edited = await _repository.Edit(value); Assert.Equal(value.Slug, edited.Slug); - Assert.Equal(edited.People.First().ShowID, value.ID); + 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 })); @@ -173,86 +164,46 @@ namespace Kyoo.Tests.Database Assert.Equal(value.Slug, show.Slug); Assert.Equal( value.People.Select(x => new { x.Role, x.Slug, x.People.Name }), - show.People.Select(x => new { x.Role, x.Slug, x.People.Name })); + show.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[] + value.ExternalId = new Dictionary() { - new MetadataID + ["test"] = new() { - Provider = new Provider("test", "test.png"), - DataID = "1234" + DataId = "1234" } }; - Show edited = await _repository.Edit(value, false); + Show edited = await _repository.Edit(value); Assert.Equal(value.Slug, edited.Slug); - Assert.Equal( - value.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug }), - edited.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug })); + KAssert.DeepEqual(value.ExternalId, edited.ExternalId); await using DatabaseContext database = Repositories.Context.New(); - Show show = await database.Shows - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(); + Show show = await database.Shows.FirstAsync(); Assert.Equal(value.Slug, show.Slug); - Assert.Equal( - value.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug }), - show.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug })); - } - - [Fact] - public async Task EditResetOldTest() - { - Show value = await _repository.Get(TestSample.Get().Slug); - Show newValue = new() - { - ID = value.ID, - Slug = "reset", - Title = "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); + KAssert.DeepEqual(value.ExternalId, show.ExternalId); } [Fact] public async Task CreateWithRelationsTest() { Show expected = TestSample.Get(); - expected.ID = 0; + expected.Id = 0; expected.Slug = "created-relation-test"; - expected.ExternalIDs = new[] + expected.ExternalId = new Dictionary { - new MetadataID + ["test"] = new() { - Provider = new Provider("provider", "provider.png"), - DataID = "ID" - } - }; - expected.Genres = new[] - { - new Genre - { - Name = "Genre", - Slug = "genre" + DataId = "ID" } }; + expected.Genres = new List() { Genre.Action }; expected.People = new[] { new PeopleRole @@ -260,7 +211,8 @@ namespace Kyoo.Tests.Database People = TestSample.Get(), Show = expected, ForPeople = false, - Role = "actor" + Role = "actor", + Type = "actor" } }; expected.Studio = new Studio("studio"); @@ -269,62 +221,58 @@ namespace Kyoo.Tests.Database await using DatabaseContext context = Repositories.Context.New(); Show retrieved = await context.Shows - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .Include(x => x.Genres) .Include(x => x.People) .ThenInclude(x => x.People) .Include(x => x.Studio) - .FirstAsync(x => x.ID == created.ID); + .FirstAsync(x => x.Id == created.Id); retrieved.People.ForEach(x => { x.Show = null; x.People.Roles = null; + x.People.Poster = null; + x.People.Thumbnail = null; + x.People.Logo = null; }); - retrieved.Studio.Shows = null; - retrieved.Genres.ForEach(x => x.Shows = null); - - expected.Genres.ForEach(x => x.Shows = null); + retrieved.Studio!.Shows = null; expected.People.ForEach(x => { x.Show = null; x.People.Roles = null; + x.People.Poster = null; + x.People.Thumbnail = null; + x.People.Logo = null; }); - retrieved.Should().BeEquivalentTo(expected); + KAssert.DeepEqual(retrieved, expected); } [Fact] public async Task CreateWithExternalID() { Show expected = TestSample.Get(); - expected.ID = 0; + expected.Id = 0; expected.Slug = "created-relation-test"; - expected.ExternalIDs = new[] + expected.ExternalId = new Dictionary { - new MetadataID + ["test"] = new() { - Provider = TestSample.Get(), - DataID = "ID" + DataId = "ID" } }; Show created = await _repository.Create(expected); KAssert.DeepEqual(expected, created); await using DatabaseContext context = Repositories.Context.New(); - Show retrieved = await context.Shows - .Include(x => x.ExternalIDs) - .ThenInclude(x => x.Provider) - .FirstAsync(x => x.ID == created.ID); + Show retrieved = await context.Shows.FirstAsync(x => x.Id == created.Id); KAssert.DeepEqual(expected, retrieved); - Assert.Single(retrieved.ExternalIDs); - Assert.Equal("ID", retrieved.ExternalIDs.First().DataID); + Assert.Single(retrieved.ExternalId); + Assert.Equal("ID", retrieved.ExternalId["test"].DataId); } [Fact] public async Task SlugDuplicationTest() { Show test = TestSample.Get(); - test.ID = 0; + test.Id = 0; test.Slug = "300"; Show created = await _repository.Create(test); Assert.Equal("300!", created.Slug); @@ -334,7 +282,7 @@ namespace Kyoo.Tests.Database public async Task GetSlugTest() { Show reference = TestSample.Get(); - Assert.Equal(reference.Slug, await _repository.GetSlug(reference.ID)); + Assert.Equal(reference.Slug, await _repository.GetSlug(reference.Id)); } [Theory] @@ -348,7 +296,7 @@ namespace Kyoo.Tests.Database Show value = new() { Slug = "super-test", - Title = "This is a test title?" + Name = "This is a test title?" }; await _repository.Create(value); ICollection ret = await _repository.Search(query); @@ -362,25 +310,12 @@ namespace Kyoo.Tests.Database await Repositories.LibraryManager.Load(show, x => x.Seasons); await Repositories.LibraryManager.Load(show, x => x.Episodes); Assert.Equal(1, await _repository.GetCount()); - Assert.Single(show.Seasons); - Assert.Single(show.Episodes); + Assert.Single(show.Seasons!); + Assert.Single(show.Episodes!); 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()); } - - [Fact] - public async Task AddShowLinkTest() - { - await Repositories.LibraryManager.Create(TestSample.GetNew()); - await _repository.AddShowLink(1, 2, null); - - await using DatabaseContext context = Repositories.Context.New(); - Show show = context.Shows - .Include(x => x.Libraries) - .First(x => x.ID == 1); - Assert.Contains(2, show.Libraries.Select(x => x.ID)); - } } } diff --git a/back/tests/Kyoo.Tests/Database/TestContext.cs b/back/tests/Kyoo.Tests/Database/TestContext.cs index 0f8b48e0..1c5bdca5 100644 --- a/back/tests/Kyoo.Tests/Database/TestContext.cs +++ b/back/tests/Kyoo.Tests/Database/TestContext.cs @@ -102,8 +102,8 @@ namespace Kyoo.Tests { string server = Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "127.0.0.1"; string port = Environment.GetEnvironmentVariable("POSTGRES_PORT") ?? "5432"; - string username = Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "kyoo"; - string password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "kyooPassword"; + string username = Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "KyooUser"; + string password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "KyooPassword"; return $"Server={server};Port={port};Database={database};User ID={username};Password={password};Include Error Detail=true"; } diff --git a/back/tests/Kyoo.Tests/Database/TestSample.cs b/back/tests/Kyoo.Tests/Database/TestSample.cs index 709b44b5..889bb234 100644 --- a/back/tests/Kyoo.Tests/Database/TestSample.cs +++ b/back/tests/Kyoo.Tests/Database/TestSample.cs @@ -27,48 +27,31 @@ namespace Kyoo.Tests { private static readonly Dictionary> NewSamples = new() { - { - typeof(Library), - () => new Library - { - ID = 2, - Slug = "new-library", - Name = "New Library", - Paths = new[] { "/a/random/path" } - } - }, { typeof(Collection), () => new Collection { - ID = 2, + Id = 2, Slug = "new-collection", Name = "New Collection", Overview = "A collection created by new sample", - Images = new Dictionary - { - [Images.Thumbnail] = "thumbnail" - } + Thumbnail = new Image("thumbnail") } }, { typeof(Show), () => new Show { - ID = 2, + Id = 2, Slug = "new-show", - Title = "New Show", + Name = "New Show", Overview = "overview", Status = Status.Planned, StartAir = new DateTime(2011, 1, 1).ToUniversalTime(), EndAir = new DateTime(2011, 1, 1).ToUniversalTime(), - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "Logo", - [Images.Thumbnail] = "Thumbnail" - }, - IsMovie = false, + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail"), Studio = null } }, @@ -76,104 +59,69 @@ namespace Kyoo.Tests typeof(Season), () => new Season { - ID = 2, - ShowID = 1, + Id = 2, + ShowId = 1, ShowSlug = Get().Slug, - Title = "New season", + Name = "New season", Overview = "New overview", EndDate = new DateTime(2000, 10, 10).ToUniversalTime(), SeasonNumber = 2, StartDate = new DateTime(2010, 10, 10).ToUniversalTime(), - Images = new Dictionary - { - [Images.Logo] = "logo" - } + Logo = new Image("logo") } }, { typeof(Episode), () => new Episode { - ID = 2, - ShowID = 1, + Id = 2, + ShowId = 1, ShowSlug = Get().Slug, - SeasonID = 1, + SeasonId = 1, SeasonNumber = Get().SeasonNumber, EpisodeNumber = 3, AbsoluteNumber = 4, Path = "/episode-path", - Title = "New Episode Title", + Name = "New Episode Title", ReleaseDate = new DateTime(2000, 10, 10).ToUniversalTime(), Overview = "new episode overview", - Images = new Dictionary - { - [Images.Logo] = "new episode logo" - } - } - }, - { - typeof(Provider), - () => new Provider - { - ID = 2, - Slug = "new-provider", - Name = "Provider NewSample", - Images = new Dictionary - { - [Images.Logo] = "logo" - } + Logo = new Image("new episode logo") } }, { typeof(People), () => new People { - ID = 2, + Id = 2, Slug = "new-person-name", Name = "New person name", - Images = new Dictionary - { - [Images.Logo] = "Old Logo", - [Images.Poster] = "Old poster" - } + Logo = new Image("Old Logo"), + Poster = new Image("Old poster") } } }; private static readonly Dictionary> Samples = new() { - { - typeof(Library), - () => new Library - { - ID = 1, - Slug = "deck", - Name = "Deck", - Paths = new[] { "/path/to/deck" } - } - }, { typeof(Collection), () => new Collection { - ID = 1, + Id = 1, Slug = "collection", Name = "Collection", Overview = "A nice collection for tests", - Images = new Dictionary - { - [Images.Poster] = "Poster" - } + Poster = new Image("Poster") } }, { typeof(Show), () => new Show { - ID = 1, + Id = 1, Slug = "anohana", - Title = "Anohana: The Flower We Saw That Day", - Aliases = new[] + Name = "Anohana: The Flower We Saw That Day", + Aliases = new List { "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", "AnoHana", @@ -183,16 +131,12 @@ namespace Kyoo.Tests "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, - StudioID = 1, + StudioId = 1, StartAir = new DateTime(2011, 1, 1).ToUniversalTime(), EndAir = new DateTime(2011, 1, 1).ToUniversalTime(), - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "Logo", - [Images.Thumbnail] = "Thumbnail" - }, - IsMovie = false, + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail"), Studio = null } }, @@ -200,41 +144,35 @@ namespace Kyoo.Tests typeof(Season), () => new Season { - ID = 1, + Id = 1, ShowSlug = "anohana", - ShowID = 1, + ShowId = 1, SeasonNumber = 1, - Title = "Season 1", + Name = "Season 1", Overview = "The first season", StartDate = new DateTime(2020, 06, 05).ToUniversalTime(), EndDate = new DateTime(2020, 07, 05).ToUniversalTime(), - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "Logo", - [Images.Thumbnail] = "Thumbnail" - }, + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail") } }, { typeof(Episode), () => new Episode { - ID = 1, + Id = 1, ShowSlug = "anohana", - ShowID = 1, - SeasonID = 1, + ShowId = 1, + SeasonId = 1, SeasonNumber = 1, EpisodeNumber = 1, AbsoluteNumber = 1, Path = "/home/kyoo/anohana-s1e1", - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "Logo", - [Images.Thumbnail] = "Thumbnail" - }, - Title = "Episode 1", + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail"), + Name = "Episode 1", Overview = "Summary of the first episode", ReleaseDate = new DateTime(2020, 06, 05).ToUniversalTime() } @@ -243,55 +181,28 @@ namespace Kyoo.Tests typeof(People), () => new People { - ID = 1, + Id = 1, Slug = "the-actor", Name = "The Actor", - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "Logo", - [Images.Thumbnail] = "Thumbnail" - }, + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail") } }, { typeof(Studio), () => new Studio { - ID = 1, + 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", - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "path/tvdb.svg", - [Images.Thumbnail] = "Thumbnail" - } - } - }, { typeof(User), () => new User { - ID = 1, + Id = 1, Slug = "user", Username = "User", Email = "user@im-a-user.com", @@ -314,54 +225,39 @@ namespace Kyoo.Tests public static void FillDatabase(DatabaseContext context) { Collection collection = Get(); - collection.ID = 0; + collection.Id = 0; context.Collections.Add(collection); Show show = Get(); - show.ID = 0; - show.StudioID = 0; + show.Id = 0; + show.StudioId = 0; context.Shows.Add(show); Season season = Get(); - season.ID = 0; - season.ShowID = 0; + season.Id = 0; + season.ShowId = 0; season.Show = show; context.Seasons.Add(season); Episode episode = Get(); - episode.ID = 0; - episode.ShowID = 0; + episode.Id = 0; + episode.ShowId = 0; episode.Show = show; - episode.SeasonID = 0; + episode.SeasonId = 0; episode.Season = season; context.Episodes.Add(episode); Studio studio = Get(); - studio.ID = 0; + 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; + people.Id = 0; context.People.Add(people); - Provider provider = Get(); - provider.ID = 0; - context.Providers.Add(provider); - - 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; + user.Id = 0; context.Users.Add(user); context.SaveChanges(); @@ -371,43 +267,20 @@ namespace Kyoo.Tests { return new() { - ID = 2, + Id = 2, ShowSlug = "anohana", - ShowID = 1, + ShowId = 1, SeasonNumber = null, EpisodeNumber = null, AbsoluteNumber = 3, Path = "/home/kyoo/anohana-3", - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "Logo", - [Images.Thumbnail] = "Thumbnail" - }, - Title = "Episode 3", + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail"), + Name = "Episode 3", Overview = "Summary of the third absolute episode", ReleaseDate = new DateTime(2020, 06, 05).ToUniversalTime() }; } - - public static Episode GetMovieEpisode() - { - return new() - { - ID = 3, - ShowSlug = "anohana", - ShowID = 1, - Path = "/home/kyoo/john-wick", - Images = new Dictionary - { - [Images.Poster] = "Poster", - [Images.Logo] = "Logo", - [Images.Thumbnail] = "Thumbnail" - }, - Title = "John wick", - Overview = "A movie episode test", - ReleaseDate = new DateTime(1595, 05, 12).ToUniversalTime() - }; - } } } diff --git a/back/tests/Kyoo.Tests/KAssert.cs b/back/tests/Kyoo.Tests/KAssert.cs index 01a4093e..585878fc 100644 --- a/back/tests/Kyoo.Tests/KAssert.cs +++ b/back/tests/Kyoo.Tests/KAssert.cs @@ -18,6 +18,7 @@ using FluentAssertions; using JetBrains.Annotations; +using Kyoo.Abstractions.Models; using Xunit.Sdk; namespace Kyoo.Tests @@ -36,6 +37,22 @@ namespace Kyoo.Tests [AssertionMethod] public static void DeepEqual(T expected, T value) { + if (expected is IResource res && expected is IThumbnails thumbs) { + if (thumbs.Poster != null) + thumbs.Poster.Path = $"/{expected.GetType().Name.ToLower()}/{res.Slug}/poster"; + if (thumbs.Thumbnail != null) + thumbs.Thumbnail.Path = $"/{expected.GetType().Name.ToLower()}/{res.Slug}/thumbnail"; + if (thumbs.Logo != null) + thumbs.Logo.Path = $"/{expected.GetType().Name.ToLower()}/{res.Slug}/logo"; + } + if (value is IResource resV && value is IThumbnails thumbsV) { + if (thumbsV.Poster != null) + thumbsV.Poster.Path = $"/{value.GetType().Name.ToLower()}/{resV.Slug}/poster"; + if (thumbsV.Thumbnail != null) + thumbsV.Thumbnail.Path = $"/{value.GetType().Name.ToLower()}/{resV.Slug}/thumbnail"; + if (thumbsV.Logo != null) + thumbsV.Logo.Path = $"/{value.GetType().Name.ToLower()}/{resV.Slug}/logo"; + } value.Should().BeEquivalentTo(expected); } diff --git a/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs b/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs index b2a444ad..80a55a6f 100644 --- a/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs +++ b/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs @@ -17,9 +17,7 @@ // along with Kyoo. If not, see . using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Kyoo.Utils; using Xunit; @@ -27,61 +25,11 @@ namespace Kyoo.Tests.Utility { 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(() => { })); diff --git a/back/tests/Kyoo.Tests/Utility/MergerTests.cs b/back/tests/Kyoo.Tests/Utility/MergerTests.cs index 810e68c9..4e9f3f3b 100644 --- a/back/tests/Kyoo.Tests/Utility/MergerTests.cs +++ b/back/tests/Kyoo.Tests/Utility/MergerTests.cs @@ -16,13 +16,9 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using JetBrains.Annotations; using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Xunit; @@ -30,318 +26,21 @@ namespace Kyoo.Tests.Utility { 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); - } - - [Fact] - public void MergeTest() - { - Genre genre = new() - { - ID = 5 - }; - Genre genre2 = new() - { - Name = "test" - }; - Genre ret = Merger.Merge(genre, genre2); - Assert.True(ReferenceEquals(genre, ret)); - Assert.Equal(5, ret.ID); - Assert.Equal("test", genre.Name); - Assert.Null(genre.Slug); - } - - [Fact] - [SuppressMessage("ReSharper", "ExpressionIsAlwaysNull")] - public void MergeNullTests() - { - Genre genre = new() - { - ID = 5 - }; - Assert.True(ReferenceEquals(genre, Merger.Merge(genre, null))); - Assert.True(ReferenceEquals(genre, Merger.Merge(null, genre))); - Assert.Null(Merger.Merge(null, null)); - } - - private class TestIOnMerge : IOnMerge - { - public void OnMerge(object other) - { - Exception exception = new(); - exception.Data[0] = other; - throw exception; - } - } - - [Fact] - public void OnMergeTest() - { - TestIOnMerge test = new(); - TestIOnMerge test2 = new(); - Assert.Throws(() => Merger.Merge(test, test2)); - try - { - Merger.Merge(test, test2); - } - catch (Exception ex) - { - Assert.True(ReferenceEquals(test2, ex.Data[0])); - } - } - - private class Test - { - public int ID { get; set; } - - public int[] Numbers { get; set; } - } - - [Fact] - public void GlobalMergeListTest() - { - Test test = new() - { - ID = 5, - Numbers = new[] { 1 } - }; - Test test2 = new() - { - Numbers = new[] { 3 } - }; - Test ret = Merger.Merge(test, test2); - Assert.True(ReferenceEquals(test, ret)); - Assert.Equal(5, ret.ID); - - Assert.Equal(2, ret.Numbers.Length); - Assert.Equal(1, ret.Numbers[0]); - Assert.Equal(3, ret.Numbers[1]); - } - - [Fact] - public void GlobalMergeListDuplicatesTest() - { - Test test = new() - { - ID = 5, - Numbers = new[] { 1 } - }; - Test test2 = new() - { - Numbers = new[] - { - 1, - 3, - 3 - } - }; - Test ret = Merger.Merge(test, test2); - Assert.True(ReferenceEquals(test, ret)); - Assert.Equal(5, ret.ID); - - Assert.Equal(4, ret.Numbers.Length); - Assert.Equal(1, ret.Numbers[0]); - Assert.Equal(1, ret.Numbers[1]); - Assert.Equal(3, ret.Numbers[2]); - Assert.Equal(3, ret.Numbers[3]); - } - - private class MergeDictionaryTest - { - public int ID { get; set; } - - public Dictionary Dictionary { get; set; } - } - - [Fact] - public void GlobalMergeDictionariesTest() - { - MergeDictionaryTest test = new() - { - ID = 5, - Dictionary = new Dictionary - { - [2] = "two" - } - }; - MergeDictionaryTest test2 = new() - { - Dictionary = new Dictionary - { - [3] = "third" - } - }; - MergeDictionaryTest ret = Merger.Merge(test, test2); - Assert.True(ReferenceEquals(test, ret)); - Assert.Equal(5, ret.ID); - - Assert.Equal(2, ret.Dictionary.Count); - Assert.Equal("two", ret.Dictionary[2]); - Assert.Equal("third", ret.Dictionary[3]); - } - - [Fact] - public void GlobalMergeDictionariesDuplicatesTest() - { - MergeDictionaryTest test = new() - { - ID = 5, - Dictionary = new Dictionary - { - [2] = "two" - } - }; - MergeDictionaryTest test2 = new() - { - Dictionary = new Dictionary - { - [2] = "nope", - [3] = "third" - } - }; - MergeDictionaryTest ret = Merger.Merge(test, test2); - Assert.True(ReferenceEquals(test, ret)); - Assert.Equal(5, ret.ID); - - Assert.Equal(2, ret.Dictionary.Count); - Assert.Equal("two", ret.Dictionary[2]); - Assert.Equal("third", ret.Dictionary[3]); - } - - [Fact] - public void GlobalMergeListDuplicatesResourcesTest() - { - Show test = new() - { - ID = 5, - Genres = new[] { new Genre("test") } - }; - Show test2 = new() - { - Genres = new[] - { - new Genre("test"), - new Genre("test2") - } - }; - Show ret = Merger.Merge(test, test2); - Assert.True(ReferenceEquals(test, ret)); - Assert.Equal(5, ret.ID); - - Assert.Equal(2, ret.Genres.Count); - Assert.Equal("test", ret.Genres.ToArray()[0].Slug); - Assert.Equal("test2", ret.Genres.ToArray()[1].Slug); - } - - [Fact] - public void MergeListTest() - { - int[] first = { 1 }; - int[] second = { 3, 3 }; - int[] ret = Merger.MergeLists(first, second); - - Assert.Equal(3, ret.Length); - Assert.Equal(1, ret[0]); - Assert.Equal(3, ret[1]); - Assert.Equal(3, ret[2]); - } - - [Fact] - public void MergeListDuplicateTest() - { - int[] first = { 1 }; - int[] second = { - 1, - 3, - 3 - }; - int[] ret = Merger.MergeLists(first, second); - - Assert.Equal(4, ret.Length); - Assert.Equal(1, ret[0]); - Assert.Equal(1, ret[1]); - Assert.Equal(3, ret[2]); - Assert.Equal(3, ret[3]); - } - - [Fact] - public void MergeListDuplicateCustomEqualityTest() - { - int[] first = { 1 }; - int[] second = { 3, 2 }; - int[] ret = Merger.MergeLists(first, second, (x, y) => x % 2 == y % 2); - - Assert.Equal(2, ret.Length); - Assert.Equal(1, ret[0]); - Assert.Equal(2, ret[1]); - } - - [Fact] - public void MergeDictionariesTest() - { - Dictionary first = new() - { - [1] = "test", - [5] = "value" - }; - Dictionary second = new() - { - [3] = "third", - }; - IDictionary ret = Merger.MergeDictionaries(first, second); - - Assert.Equal(3, ret.Count); - Assert.Equal("test", ret[1]); - Assert.Equal("value", ret[5]); - Assert.Equal("third", ret[3]); - } - - [Fact] - public void MergeDictionariesDuplicateTest() - { - Dictionary first = new() - { - [1] = "test", - [5] = "value" - }; - Dictionary second = new() - { - [3] = "third", - [5] = "new-value", - }; - IDictionary ret = Merger.MergeDictionaries(first, second); - - Assert.Equal(3, ret.Count); - Assert.Equal("test", ret[1]); - Assert.Equal("value", ret[5]); - Assert.Equal("third", ret[3]); - } - [Fact] public void CompleteTest() { - Genre genre = new() + Studio genre = new() { - ID = 5, Name = "merged" }; - Genre genre2 = new() + Studio genre2 = new() { - Name = "test" + Name = "test", + Id = 5, }; - Genre ret = Merger.Complete(genre, genre2); + Studio ret = Merger.Complete(genre, genre2); Assert.True(ReferenceEquals(genre, ret)); - Assert.Equal(5, ret.ID); + Assert.Equal(5, ret.Id); Assert.Equal("test", genre.Name); Assert.Null(genre.Slug); } @@ -351,71 +50,56 @@ namespace Kyoo.Tests.Utility { Collection collection = new() { - ID = 5, Name = "merged", - Images = new Dictionary - { - [Images.Logo] = "logo", - [Images.Poster] = "poster" - } - }; Collection collection2 = new() { + Id = 5, Name = "test", - Images = new Dictionary - { - [Images.Poster] = "new-poster", - [Images.Thumbnail] = "thumbnails" - } }; Collection ret = Merger.Complete(collection, collection2); Assert.True(ReferenceEquals(collection, ret)); - Assert.Equal(5, ret.ID); + Assert.Equal(5, ret.Id); Assert.Equal("test", ret.Name); Assert.Null(ret.Slug); - Assert.Equal(3, ret.Images.Count); - Assert.Equal("new-poster", ret.Images[Images.Poster]); - Assert.Equal("thumbnails", ret.Images[Images.Thumbnail]); - Assert.Equal("logo", ret.Images[Images.Logo]); } [Fact] public void CompleteDictionaryOutParam() { - Dictionary first = new() + Dictionary first = new() { - [Images.Logo] = "logo", - [Images.Poster] = "poster" + ["logo"] = "logo", + ["poster"] = "poster" }; - Dictionary second = new() + Dictionary second = new() { - [Images.Poster] = "new-poster", - [Images.Thumbnail] = "thumbnails" + ["poster"] = "new-poster", + ["thumbnail"] = "thumbnails" }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); Assert.True(changed); Assert.Equal(3, ret.Count); - Assert.Equal("new-poster", ret[Images.Poster]); - Assert.Equal("thumbnails", ret[Images.Thumbnail]); - Assert.Equal("logo", ret[Images.Logo]); + Assert.Equal("new-poster", ret["poster"]); + Assert.Equal("thumbnails", ret["thumbnail"]); + Assert.Equal("logo", ret["logo"]); } [Fact] public void CompleteDictionaryEqualTest() { - Dictionary first = new() + Dictionary first = new() { - [Images.Poster] = "poster" + ["poster"] = "poster" }; - Dictionary second = new() + Dictionary second = new() { - [Images.Poster] = "new-poster", + ["poster"] = "new-poster", }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); Assert.True(changed); Assert.Single(ret); - Assert.Equal("new-poster", ret[Images.Poster]); + Assert.Equal("new-poster", ret["poster"]); } private class TestMergeSetter @@ -452,102 +136,44 @@ namespace Kyoo.Tests.Utility // This should no call the setter of first so the test should pass. } - [Fact] - public void MergeDictionaryNoChangeNoSetTest() - { - TestMergeSetter first = new() - { - Backing = new Dictionary - { - [2] = 3 - } - }; - TestMergeSetter second = new() - { - Backing = new Dictionary() - }; - Merger.Merge(first, second); - // This should no call the setter of first so the test should pass. - } - - [Fact] - public void MergeDictionaryNullValue() - { - Dictionary first = new() - { - [Images.Logo] = "logo", - [Images.Poster] = null - }; - Dictionary second = new() - { - [Images.Poster] = "new-poster", - [Images.Thumbnail] = "thumbnails" - }; - IDictionary ret = Merger.MergeDictionaries(first, second, out bool changed); - Assert.True(changed); - Assert.Equal(3, ret.Count); - Assert.Equal("new-poster", ret[Images.Poster]); - Assert.Equal("thumbnails", ret[Images.Thumbnail]); - Assert.Equal("logo", ret[Images.Logo]); - } - - [Fact] - public void MergeDictionaryNullValueNoChange() - { - Dictionary first = new() - { - [Images.Logo] = "logo", - [Images.Poster] = null - }; - Dictionary second = new() - { - [Images.Poster] = null, - }; - IDictionary ret = Merger.MergeDictionaries(first, second, out bool changed); - Assert.False(changed); - Assert.Equal(2, ret.Count); - Assert.Null(ret[Images.Poster]); - Assert.Equal("logo", ret[Images.Logo]); - } - [Fact] public void CompleteDictionaryNullValue() { - Dictionary first = new() + Dictionary first = new() { - [Images.Logo] = "logo", - [Images.Poster] = null + ["logo"] = "logo", + ["poster"] = null }; - Dictionary second = new() + Dictionary second = new() { - [Images.Poster] = "new-poster", - [Images.Thumbnail] = "thumbnails" + ["poster"] = "new-poster", + ["thumbnail"] = "thumbnails" }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); Assert.True(changed); Assert.Equal(3, ret.Count); - Assert.Equal("new-poster", ret[Images.Poster]); - Assert.Equal("thumbnails", ret[Images.Thumbnail]); - Assert.Equal("logo", ret[Images.Logo]); + Assert.Equal("new-poster", ret["poster"]); + Assert.Equal("thumbnails", ret["thumbnail"]); + Assert.Equal("logo", ret["logo"]); } [Fact] public void CompleteDictionaryNullValueNoChange() { - Dictionary first = new() + Dictionary first = new() { - [Images.Logo] = "logo", - [Images.Poster] = null + ["logo"] = "logo", + ["poster"] = null }; - Dictionary second = new() + Dictionary second = new() { - [Images.Poster] = null, + ["poster"] = null, }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); Assert.False(changed); Assert.Equal(2, ret.Count); - Assert.Null(ret[Images.Poster]); - Assert.Equal("logo", ret[Images.Logo]); + Assert.Null(ret["poster"]); + Assert.Equal("logo", ret["logo"]); } } } diff --git a/back/tests/Kyoo.Tests/Utility/TaskTests.cs b/back/tests/Kyoo.Tests/Utility/TaskTests.cs index e00ca321..7151871a 100644 --- a/back/tests/Kyoo.Tests/Utility/TaskTests.cs +++ b/back/tests/Kyoo.Tests/Utility/TaskTests.cs @@ -26,13 +26,6 @@ namespace Kyoo.Tests.Utility { 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() { @@ -59,37 +52,5 @@ namespace Kyoo.Tests.Utility 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)); - } } } diff --git a/back/tests/Kyoo.Tests/Utility/UtilityTests.cs b/back/tests/Kyoo.Tests/Utility/UtilityTests.cs index ab7805f6..aea71e8e 100644 --- a/back/tests/Kyoo.Tests/Utility/UtilityTests.cs +++ b/back/tests/Kyoo.Tests/Utility/UtilityTests.cs @@ -20,7 +20,6 @@ using System; using System.Linq.Expressions; using System.Reflection; using Kyoo.Abstractions.Models; -using Kyoo.Utils; using Xunit; using KUtility = Kyoo.Utils.Utility; @@ -32,26 +31,24 @@ namespace Kyoo.Tests.Utility [Fact] public void IsPropertyExpression_Tests() { - Expression> member = x => x.ID; - Expression> memberCast = x => x.ID; + Expression> member = x => x.Id; + Expression> memberCast = x => x.Id; - Assert.False(KUtility.IsPropertyExpression(null)); Assert.True(KUtility.IsPropertyExpression(member)); Assert.True(KUtility.IsPropertyExpression(memberCast)); - Expression> call = x => x.GetID("test"); + Expression> call = x => x.ToString(); Assert.False(KUtility.IsPropertyExpression(call)); } [Fact] public void GetPropertyName_Test() { - Expression> member = x => x.ID; - Expression> memberCast = x => x.ID; + Expression> member = x => x.Id; + Expression> memberCast = x => x.Id; - Assert.Equal("ID", KUtility.GetPropertyName(member)); - Assert.Equal("ID", KUtility.GetPropertyName(memberCast)); - Assert.Throws(() => KUtility.GetPropertyName(null)); + Assert.Equal("Id", KUtility.GetPropertyName(member)); + Assert.Equal("Id", KUtility.GetPropertyName(memberCast)); } [Fact] @@ -84,16 +81,5 @@ namespace Kyoo.Tests.Utility Array.Empty(), new object[] { this })); } - - [Fact] - public void GetMethodTest2() - { - MethodInfo method = KUtility.GetMethod(typeof(Merger), - BindingFlags.Static | BindingFlags.Public, - nameof(Merger.MergeLists), - new[] { typeof(string) }, - new object[] { "string", "string2", null }); - Assert.Equal(nameof(Merger.MergeLists), method.Name); - } } } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 5dd99093..4d6ab613 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -26,11 +26,12 @@ services: - ./front:/app - /app/.yarn - /app/node_modules + - /app/apps/mobile/node_modules - /app/apps/web/.next/ - /app/apps/mobile/.expo/ ports: - "3000:3000" - - "19000:19000" + - "8081:8081" restart: on-failure environment: - KYOO_URL=${KYOO_URL:-http://back:5000} diff --git a/front/Dockerfile.dev b/front/Dockerfile.dev index 6c22cbbd..f67710af 100644 --- a/front/Dockerfile.dev +++ b/front/Dockerfile.dev @@ -13,5 +13,5 @@ RUN yarn --immutable ENV NEXT_TELEMETRY_DISABLED 1 EXPOSE 3000 -EXPOSE 19000 +EXPOSE 8081 CMD yarn dev diff --git a/front/apps/mobile/app/_layout.tsx b/front/apps/mobile/app/_layout.tsx index 7eab0372..b4f27db8 100644 --- a/front/apps/mobile/app/_layout.tsx +++ b/front/apps/mobile/app/_layout.tsx @@ -40,10 +40,10 @@ import { useTheme } from "yoshiki/native"; import { CircularProgress } from "@kyoo/primitives"; import { useRouter } from "solito/router"; import "intl-pluralrules"; -import '@formatjs/intl-locale/polyfill' -import '@formatjs/intl-displaynames/polyfill' -import '@formatjs/intl-displaynames/locale-data/en' -import '@formatjs/intl-displaynames/locale-data/fr' +import "@formatjs/intl-locale/polyfill"; +import "@formatjs/intl-displaynames/polyfill"; +import "@formatjs/intl-displaynames/locale-data/en"; +import "@formatjs/intl-displaynames/locale-data/fr"; // TODO: use a backend to load jsons. import en from "../../../translations/en.json"; @@ -105,14 +105,20 @@ const AuthGuard = ({ selected }: { selected: number | null }) => { }; let rendered: boolean = false; +SplashScreen.preventAutoHideAsync(); export default function Root() { const [queryClient] = useState(() => createQueryClient()); const theme = useColorScheme(); const [fontsLoaded] = useFonts({ Poppins_300Light, Poppins_400Regular, Poppins_900Black }); const info = useAccounts(); + const isReady = fontsLoaded && (rendered || info.type !== "loading"); - if (!fontsLoaded || (!rendered && info.type === "loading")) return ; + useEffect(() => { + if (isReady) SplashScreen.hideAsync(); + }, [isReady]); + + if (!isReady) return null; rendered = true; return ( diff --git a/front/apps/mobile/app/movie/[slug].tsx b/front/apps/mobile/app/movie/[slug]/index.tsx similarity index 95% rename from front/apps/mobile/app/movie/[slug].tsx rename to front/apps/mobile/app/movie/[slug]/index.tsx index fb4abc17..fd97ad59 100644 --- a/front/apps/mobile/app/movie/[slug].tsx +++ b/front/apps/mobile/app/movie/[slug]/index.tsx @@ -19,7 +19,7 @@ */ import { MovieDetails } from "@kyoo/ui"; -import { withRoute } from "../../utils"; +import { withRoute } from "../../../utils"; export default withRoute(MovieDetails, { options: { headerTransparent: true, headerStyle: { backgroundColor: "transparent" } }, diff --git a/front/packages/models/src/resources/library.ts b/front/apps/mobile/app/movie/[slug]/watch.tsx similarity index 64% rename from front/packages/models/src/resources/library.ts rename to front/apps/mobile/app/movie/[slug]/watch.tsx index 8453b72a..25758b2b 100644 --- a/front/packages/models/src/resources/library.ts +++ b/front/apps/mobile/app/movie/[slug]/watch.tsx @@ -18,22 +18,17 @@ * along with Kyoo. If not, see . */ -import { z } from "zod"; -import { ResourceP } from "../traits/resource"; +import { Player } from "@kyoo/ui"; +import { withRoute } from "../../../utils"; -/** - * The library that will contain Shows, Collections... - */ -export const LibraryP = ResourceP.extend({ - /** - * The name of this library. - */ - name: z.string(), - - /** - * The list of paths that this library is responsible for. This is mainly used by the Scan task. - */ - paths: z.array(z.string()), -}); - -export type Library = z.infer; +export default withRoute( + Player, + { + options: { + headerShown: false, + }, + statusBar: { hidden: true }, + fullscreen: true, + }, + { type: "movie" }, +); diff --git a/front/apps/mobile/app/watch/[slug].tsx b/front/apps/mobile/app/watch/[slug].tsx index ca211966..ecfdbdf7 100644 --- a/front/apps/mobile/app/watch/[slug].tsx +++ b/front/apps/mobile/app/watch/[slug].tsx @@ -21,10 +21,14 @@ import { Player } from "@kyoo/ui"; import { withRoute } from "../../utils"; -export default withRoute(Player, { - options: { - headerShown: false, +export default withRoute( + Player, + { + options: { + headerShown: false, + }, + statusBar: { hidden: true }, + fullscreen: true, }, - statusBar: { hidden: true }, - fullscreen: true, -}); + { type: "episode" }, +); diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json index 4d21e273..8848da02 100644 --- a/front/apps/mobile/package.json +++ b/front/apps/mobile/package.json @@ -17,45 +17,47 @@ "@formatjs/intl-locale": "^3.3.2", "@gorhom/portal": "^1.0.14", "@kyoo/ui": "workspace:^", - "@material-symbols/svg-400": "^0.5.0", - "@shopify/flash-list": "1.4.0", - "@tanstack/react-query": "^4.26.1", + "@material-symbols/svg-400": "^0.10.3", + "@shopify/flash-list": "1.4.3", + "@tanstack/react-query": "^4.32.6", "babel-plugin-transform-inline-environment-variables": "^0.4.4", - "expo": "^48.0.7", - "expo-build-properties": "~0.6.0", - "expo-constants": "~14.2.1", - "expo-dev-client": "~2.2.1", - "expo-font": "~11.1.1", - "expo-linear-gradient": "~12.1.2", - "expo-linking": "~4.0.1", - "expo-localization": "~14.1.1", - "expo-navigation-bar": "~2.1.1", - "expo-router": "1.5.3", - "expo-screen-orientation": "~5.1.1", - "expo-secure-store": "~12.1.1", - "expo-status-bar": "~1.4.4", - "expo-updates": "~0.16.4", - "i18next": "^22.4.11", - "intl-pluralrules": "^1.3.1", - "moti": "^0.24.2", + "expo": "^49.0.6", + "expo-build-properties": "~0.8.3", + "expo-constants": "~14.4.2", + "expo-dev-client": "~2.4.6", + "expo-font": "~11.4.0", + "expo-linear-gradient": "~12.3.0", + "expo-linking": "~5.0.2", + "expo-localization": "~14.3.0", + "expo-navigation-bar": "~2.3.0", + "expo-router": "2.0.0", + "expo-screen-orientation": "~6.0.5", + "expo-secure-store": "~12.3.1", + "expo-status-bar": "~1.6.0", + "expo-updates": "~0.18.11", + "i18next": "^23.4.2", + "intl-pluralrules": "^2.0.1", + "moti": "^0.26.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-i18next": "^12.2.0", - "react-native": "0.71.8", + "react-i18next": "^13.0.3", + "react-native": "0.72.3", + "react-native-blurhash": "^1.1.11", + "react-native-fast-image": "^8.6.3", "react-native-mmkv": "^2.10.1", - "react-native-reanimated": "~2.14.4", - "react-native-safe-area-context": "4.5.0", - "react-native-screens": "~3.20.0", - "react-native-svg": "13.4.0", + "react-native-reanimated": "~3.3.0", + "react-native-safe-area-context": "4.6.3", + "react-native-screens": "~3.22.0", + "react-native-svg": "13.9.0", "react-native-uuid": "^2.0.1", - "react-native-video": "^6.0.0-alpha.5", - "yoshiki": "1.2.2" + "react-native-video": "^6.0.0-alpha.7", + "yoshiki": "1.2.7" }, "devDependencies": { - "@babel/core": "^7.21.3", - "@types/react": "~18.0.28", - "react-native-svg-transformer": "^1.0.0", - "typescript": "^4.9.5" + "@babel/core": "^7.22.10", + "@types/react": "18.2.0", + "react-native-svg-transformer": "^1.1.0", + "typescript": "^5.1.6" }, "installConfig": { "hoistingLimits": "workspaces" diff --git a/front/apps/mobile/utils.tsx b/front/apps/mobile/utils.tsx index 978891fc..fb201bac 100644 --- a/front/apps/mobile/utils.tsx +++ b/front/apps/mobile/utils.tsx @@ -42,6 +42,7 @@ export const withRoute = ( statusBar?: StatusBarProps; fullscreen?: boolean; }, + defaultProps?: Partial, ) => { const { statusBar, fullscreen, ...routeOptions } = options ?? {}; const WithUseRoute = (props: any) => { @@ -51,7 +52,7 @@ export const withRoute = ( {routeOptions && } {statusBar && } {fullscreen && } - + ); }; diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js index 75d48866..abeb7f64 100755 --- a/front/apps/web/next.config.js +++ b/front/apps/web/next.config.js @@ -48,6 +48,7 @@ const nextConfig = { alias: { ...config.resolve.alias, "react-native$": "react-native-web", + 'react-native/Libraries/Image/AssetRegistry$': 'react-native-web/dist/modules/AssetRegistry', }, extensions: [".web.ts", ".web.tsx", ".web.js", ".web.jsx", ...config.resolve.extensions], }; diff --git a/front/apps/web/package.json b/front/apps/web/package.json index 17434ba9..b467bdfd 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -14,42 +14,43 @@ "@kyoo/models": "workspace:^", "@kyoo/primitives": "workspace:^", "@kyoo/ui": "workspace:^", - "@material-symbols/svg-400": "^0.5.0", - "@radix-ui/react-dropdown-menu": "^2.0.4", - "@tanstack/react-query": "^4.26.1", - "expo-linear-gradient": "^12.1.2", - "hls.js": "^1.3.4", - "i18next": "^22.4.11", - "jotai": "^2.0.3", + "@material-symbols/svg-400": "^0.10.3", + "@radix-ui/react-dropdown-menu": "^2.0.5", + "@tanstack/react-query": "^4.32.6", + "expo-linear-gradient": "^12.4.0", + "expo-modules-core": "^1.5.9", + "hls.js": "^1.4.10", + "i18next": "^23.4.2", + "jotai": "^2.3.1", "libass-wasm": "^4.1.0", - "moti": "^0.24.2", - "next": "13.2.4", - "next-translate": "^2.0.2", + "moti": "^0.26.0", + "next": "13.4.13", + "next-translate": "^2.5.3", "raf": "^3.4.1", "react": "18.2.0", "react-dom": "18.2.0", - "react-i18next": "^12.2.0", - "react-native-reanimated": "2.14.4", - "react-native-svg": "13.4.0", - "react-native-video": "^6.0.0-alpha.5", - "react-native-web": "0.19.1", - "solito": "^3.0.0", + "react-i18next": "^13.0.3", + "react-native-reanimated": "~3.3.0", + "react-native-svg": "13.11.0", + "react-native-video": "^6.0.0-alpha.7", + "react-native-web": "0.19.7", + "solito": "^4.0.1", "srt-webvtt": "^2.0.0", - "superjson": "^1.12.2", - "sweetalert2": "^11.7.12", - "yoshiki": "1.2.2", + "superjson": "^1.13.1", + "sweetalert2": "^11.7.20", + "yoshiki": "1.2.7", "zod": "^3.21.4" }, "devDependencies": { - "@svgr/webpack": "^6.5.1", - "@types/node": "18.15.3", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", - "@types/react-native-video": "^5.0.14", + "@svgr/webpack": "^8.0.1", + "@types/node": "20.4.8", + "@types/react": "18.2.0", + "@types/react-dom": "18.2.0", "copy-webpack-plugin": "^11.0.0", - "eslint": "^8.36.0", - "eslint-config-next": "13.2.4", - "typescript": "^4.9.5", - "webpack": "^5.76.1" + "eslint": "^8.46.0", + "eslint-config-next": "13.4.13", + "react-native": "0.72.3", + "typescript": "^5.1.6", + "webpack": "^5.88.2" } } diff --git a/front/apps/web/src/pages/movie/[slug].tsx b/front/apps/web/src/pages/movie/[slug]/index.tsx similarity index 100% rename from front/apps/web/src/pages/movie/[slug].tsx rename to front/apps/web/src/pages/movie/[slug]/index.tsx diff --git a/front/apps/web/src/pages/movie/[slug]/watch.tsx b/front/apps/web/src/pages/movie/[slug]/watch.tsx new file mode 100644 index 00000000..17349725 --- /dev/null +++ b/front/apps/web/src/pages/movie/[slug]/watch.tsx @@ -0,0 +1,24 @@ +/* + * Kyoo - A portable and vast media library solution. + * Copyright (c) Kyoo. + * + * See AUTHORS.md and LICENSE file in the project root for full license information. + * + * Kyoo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Kyoo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Kyoo. If not, see . + */ + +import { Player } from "@kyoo/ui"; +import { withRoute } from "~/router"; + +export default withRoute(Player, { type: "movie" }); diff --git a/front/apps/web/src/pages/watch/[slug].tsx b/front/apps/web/src/pages/watch/[slug].tsx index 006fcb49..a26ad2ca 100644 --- a/front/apps/web/src/pages/watch/[slug].tsx +++ b/front/apps/web/src/pages/watch/[slug].tsx @@ -21,4 +21,4 @@ import { Player } from "@kyoo/ui"; import { withRoute } from "~/router"; -export default withRoute(Player); +export default withRoute(Player, { type: "episode" }); diff --git a/front/apps/web/src/router.tsx b/front/apps/web/src/router.tsx index 3ee341a8..571fa0dd 100644 --- a/front/apps/web/src/router.tsx +++ b/front/apps/web/src/router.tsx @@ -21,12 +21,12 @@ import { useRouter } from "next/router"; import { ComponentType } from "react"; -export const withRoute = (Component: ComponentType) => { +export const withRoute = (Component: ComponentType, defaultProps?: Partial) => { const WithUseRoute = (props: Props) => { const router = useRouter(); // @ts-ignore - return ; + return ; }; const { ...all } = Component; diff --git a/front/apps/web/tsconfig.json b/front/apps/web/tsconfig.json index c212ed86..0cd6e2a1 100755 --- a/front/apps/web/tsconfig.json +++ b/front/apps/web/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/front/package.json b/front/package.json index e9769c92..3ff5d777 100644 --- a/front/package.json +++ b/front/package.json @@ -29,13 +29,13 @@ "tsdoc": true }, "devDependencies": { - "eslint": "8.36.0", - "eslint-config-next": "13.2.4", - "eslint-config-prettier": "^8.7.0", + "eslint": "8.46.0", + "eslint-config-next": "13.4.13", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-header": "^3.1.1", - "prettier": "^2.8.4", - "prettier-plugin-jsdoc": "^0.4.2", - "typescript": "4.9.5" + "prettier": "^3.0.1", + "prettier-plugin-jsdoc": "^1.0.1", + "typescript": "5.1.6" }, "packageManager": "yarn@3.2.4" } diff --git a/front/packages/models/package.json b/front/packages/models/package.json index 0b5c0fc5..9526e993 100644 --- a/front/packages/models/package.json +++ b/front/packages/models/package.json @@ -4,9 +4,9 @@ "types": "src/index.ts", "packageManager": "yarn@3.2.4", "devDependencies": { - "@types/react": "^18.0.28", + "@types/react": "18.2.0", "react-native-mmkv": "^2.10.1", - "typescript": "^4.9.5" + "typescript": "^5.1.6" }, "peerDependencies": { "@tanstack/react-query": "*", diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx index 4f5fe001..0773907b 100644 --- a/front/packages/models/src/query.tsx +++ b/front/packages/models/src/query.tsx @@ -124,7 +124,7 @@ export const queryFn = async ( const parsed = await type.safeParseAsync(data); if (!parsed.success) { console.log("Parse error: ", parsed.error); - throw { errors: parsed.error.errors.map((x) => x.message) } as KyooErrors; + throw { errors: ["Invalid response from kyoo. Possible version mismatch between the server and the application."] } as KyooErrors; } return parsed.data; }; diff --git a/front/packages/models/src/resources/episode.ts b/front/packages/models/src/resources/episode.ts index 8df707ba..fff3be27 100644 --- a/front/packages/models/src/resources/episode.ts +++ b/front/packages/models/src/resources/episode.ts @@ -20,47 +20,72 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { ImagesP } from "../traits"; +import { ImagesP, imageFn } from "../traits"; import { ResourceP } from "../traits/resource"; +import { ShowP } from "./show"; -export const EpisodeP = z.preprocess( - (x: any) => { - if (!x) return x; - x.name = x.title; - return x; - }, - ResourceP.merge(ImagesP).extend({ +const BaseEpisodeP = ResourceP.merge(ImagesP).extend({ + /** + * The season in witch this episode is in. + */ + seasonNumber: z.number().nullable(), + + /** + * The number of this episode in it's season. + */ + episodeNumber: z.number().nullable(), + + /** + * The absolute number of this episode. It's an episode number that is not reset to 1 after a new + * season. + */ + absoluteNumber: z.number().nullable(), + + /** + * The title of this episode. + */ + name: z.string().nullable(), + + /** + * The overview of this episode. + */ + overview: z.string().nullable(), + + /** + * The release date of this episode. It can be null if unknown. + */ + releaseDate: zdate().nullable(), + + /** + * The links to see a movie or an episode. + */ + links: z.object({ /** - * The season in witch this episode is in. + * The direct link to the unprocessed video (pristine quality). */ - seasonNumber: z.number().nullable(), + direct: z.string().transform(imageFn), /** - * The number of this episode in it's season. + * The link to an HLS master playlist containing all qualities available for this video. */ - episodeNumber: z.number().nullable(), - - /** - * The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. - */ - absoluteNumber: z.number().nullable(), - - /** - * The title of this episode. - */ - name: z.string().nullable(), - - /** - * The overview of this episode. - */ - overview: z.string().nullable(), - - /** - * The release date of this episode. It can be null if unknown. - */ - releaseDate: zdate().nullable(), + hls: z.string().transform(imageFn), }), -); + + show: ShowP.optional() +}); + +export const EpisodeP = BaseEpisodeP.extend({ + /** + * 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. + */ + previousEpisode: BaseEpisodeP.nullable().optional(), + /** + * 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. + */ + nextEpisode: BaseEpisodeP.nullable().optional(), +}) /** * A class to represent a single show's episode. diff --git a/front/packages/models/src/resources/genre.ts b/front/packages/models/src/resources/genre.ts index 60686494..dfb04d9a 100644 --- a/front/packages/models/src/resources/genre.ts +++ b/front/packages/models/src/resources/genre.ts @@ -18,17 +18,23 @@ * along with Kyoo. If not, see . */ -import { z } from "zod"; -import { ResourceP } from "../traits/resource"; - -export const GenreP = ResourceP.extend({ - /** - * The name of this genre. - */ - name: z.string(), -}); - -/** - * A genre that allow one to specify categories for shows. - */ -export type Genre = z.infer; +export enum Genre { + Action = "Action", + Adventure = "Adventure", + Animation = "Animation", + Comedy = "Comedy", + Crime = "Crime", + Documentary = "Documentary", + Drama = "Drama", + Family = "Family", + Fantasy = "Fantasy", + History = "History", + Horror = "Horror", + Music = "Music", + Mystery = "Mystery", + Romance = "Romance", + ScienceFiction = "ScienceFiction", + Thriller = "Thriller", + War = "War", + Western = "Western", +} diff --git a/front/packages/models/src/resources/index.ts b/front/packages/models/src/resources/index.ts index 39332330..5f06357f 100644 --- a/front/packages/models/src/resources/index.ts +++ b/front/packages/models/src/resources/index.ts @@ -18,7 +18,6 @@ * along with Kyoo. If not, see . */ -export * from "./library"; export * from "./library-item"; export * from "./show"; export * from "./movie"; @@ -28,5 +27,5 @@ export * from "./person"; export * from "./studio"; export * from "./episode"; export * from "./season"; -export * from "./watch-item"; +export * from "./watch-info"; export * from "./user"; diff --git a/front/packages/models/src/resources/library-item.ts b/front/packages/models/src/resources/library-item.ts index 6ceb1555..d3f648b7 100644 --- a/front/packages/models/src/resources/library-item.ts +++ b/front/packages/models/src/resources/library-item.ts @@ -26,32 +26,26 @@ import { ShowP } from "./show"; /** * The type of item, ether a show, a movie or a collection. */ -export enum ItemType { - Show = 0, - Movie = 1, - Collection = 2, +export enum ItemKind { + Show = "Show", + Movie = "Movie", + Collection = "Collection", } -export const LibraryItemP = z.preprocess( - (x: any) => { - if (!x.aliases) x.aliases = []; - return x; - }, - z.union([ - /* - * Either a Show - */ - ShowP.and(z.object({ type: z.literal(ItemType.Show) })), - /* - * Or a Movie - */ - MovieP.and(z.object({ type: z.literal(ItemType.Movie) })), - /* - * Or a Collection - */ - CollectionP.and(z.object({ type: z.literal(ItemType.Collection) })), - ]), -); +export const LibraryItemP = z.union([ + /* + * Either a Show + */ + ShowP.and(z.object({ kind: z.literal(ItemKind.Show) })), + /* + * Or a Movie + */ + MovieP.and(z.object({ kind: z.literal(ItemKind.Movie) })), + /* + * Or a Collection + */ + CollectionP.and(z.object({ kind: z.literal(ItemKind.Collection) })), +]); /** * An item that can be contained by a Library (so a Show, a Movie or a Collection). diff --git a/front/packages/models/src/resources/movie.ts b/front/packages/models/src/resources/movie.ts index afa59735..ca3d99fb 100644 --- a/front/packages/models/src/resources/movie.ts +++ b/front/packages/models/src/resources/movie.ts @@ -20,33 +20,21 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { ImagesP, ResourceP } from "../traits"; -import { GenreP } from "./genre"; +import { ImagesP, ResourceP, imageFn } from "../traits"; +import { Genre } from "./genre"; import { StudioP } from "./studio"; +import { Status } from "./show"; -/** - * The enum containing movie's status. - */ -export enum MovieStatus { - Unknown = 0, - Finished = 1, - Planned = 3, -} - -export const MovieP = z.preprocess( - (x: any) => { - // Waiting for the API to be updaded - x.name = x.title; - if (x.aliases === null) x.aliases = []; - x.airDate = x.startAir; - x.trailer = x.images["3"]; - return x; - }, - ResourceP.merge(ImagesP).extend({ +export const MovieP = ResourceP.merge(ImagesP) + .extend({ /** * The title of this movie. */ name: z.string(), + /** + * A catchphrase for this show. + */ + tagline: z.string().nullable(), /** * The list of alternative titles of this movie. */ @@ -56,23 +44,55 @@ export const MovieP = z.preprocess( */ overview: z.string().nullable(), /** - * Is this movie not aired yet or finished? + * A list of tags that match this movie. */ - status: z.nativeEnum(MovieStatus), + tags: z.array(z.string()), + /** + * /** Is this movie not aired yet or finished? + */ + status: z.nativeEnum(Status), /** * The date this movie aired. It can also be null if this is unknown. */ airDate: zdate().nullable(), + /** + * A youtube url for the trailer. + */ + trailer: z.string().optional().nullable(), /** * The list of genres (themes) this movie has. */ - genres: z.array(GenreP).optional(), + genres: z.array(z.nativeEnum(Genre)), /** * The studio that made this movie. */ studio: StudioP.optional().nullable(), - }), -); + + /** + * The links to see a movie or an episode. + */ + links: z.object({ + /** + * The direct link to the unprocessed video (pristine quality). + */ + direct: z.string().transform(imageFn), + + /** + * The link to an HLS master playlist containing all qualities available for this video. + */ + hls: z.string().transform(imageFn), + }), + }) + .transform((x) => { + if (!x.thumbnail && x.poster) { + x.thumbnail = { ...x.poster }; + if (x.thumbnail) { + x.thumbnail.low = x.thumbnail.high; + x.thumbnail.medium = x.thumbnail.high; + } + } + return x; + }); /** * A Movie type diff --git a/front/packages/models/src/resources/season.ts b/front/packages/models/src/resources/season.ts index e858f7eb..d187a5e3 100644 --- a/front/packages/models/src/resources/season.ts +++ b/front/packages/models/src/resources/season.ts @@ -23,37 +23,28 @@ import { zdate } from "../utils"; import { ImagesP } from "../traits"; import { ResourceP } from "../traits/resource"; -export const SeasonP = z.preprocess( - (x: any) => { - x.name = x.title; - return x; - }, - ResourceP.merge(ImagesP).extend({ - /** - * The name of this season. - */ - name: z.string(), - /** - * The number of this season. This can be set to 0 to indicate specials. - */ - seasonNumber: z.number(), - - /** - * A quick overview of this season. - */ - overview: z.string().nullable(), - - /** - * The starting air date of this season. - */ - startDate: zdate().nullable(), - - /** - * The ending date of this season. - */ - endDate: zdate().nullable(), - }), -); +export const SeasonP = ResourceP.merge(ImagesP).extend({ + /** + * The name of this season. + */ + name: z.string(), + /** + * The number of this season. This can be set to 0 to indicate specials. + */ + seasonNumber: z.number(), + /** + * A quick overview of this season. + */ + overview: z.string().nullable(), + /** + * The starting air date of this season. + */ + startDate: zdate().nullable(), + /** + * The ending date of this season. + */ + endDate: zdate().nullable(), +}); /** * A season of a Show. diff --git a/front/packages/models/src/resources/show.ts b/front/packages/models/src/resources/show.ts index aa82c299..3b1b2d5b 100644 --- a/front/packages/models/src/resources/show.ts +++ b/front/packages/models/src/resources/show.ts @@ -21,7 +21,7 @@ import { z } from "zod"; import { zdate } from "../utils"; import { ImagesP, ResourceP } from "../traits"; -import { GenreP } from "./genre"; +import { Genre } from "./genre"; import { SeasonP } from "./season"; import { StudioP } from "./studio"; @@ -29,26 +29,22 @@ import { StudioP } from "./studio"; * The enum containing show's status. */ export enum Status { - Unknown = 0, - Finished = 1, - Airing = 2, - Planned = 3, + Unknown = "Unknown", + Finished = "Finished", + Airing = "Airing", + Planned = "Planned", } -export const ShowP = z.preprocess( - (x: any) => { - if (!x) return x; - // Waiting for the API to be updaded - x.name = x.title; - if (x.aliases === null) x.aliases = []; - x.trailer = x.images["3"]; - return x; - }, - ResourceP.merge(ImagesP).extend({ +export const ShowP = ResourceP.merge(ImagesP) + .extend({ /** * The title of this show. */ name: z.string(), + /** + * A catchphrase for this show. + */ + tagline: z.string().nullable(), /** * The list of alternative titles of this show. */ @@ -57,6 +53,10 @@ export const ShowP = z.preprocess( * The summary of this show. */ overview: z.string().nullable(), + /** + * A list of tags that match this movie. + */ + tags: z.array(z.string()), /** * Is this show airing, not aired yet or finished? */ @@ -72,7 +72,11 @@ export const ShowP = z.preprocess( /** * The list of genres (themes) this show has. */ - genres: z.array(GenreP).optional(), + genres: z.array(z.nativeEnum(Genre)), + /** + * A youtube url for the trailer. + */ + trailer: z.string().optional().nullable(), /** * The studio that made this show. */ @@ -81,8 +85,17 @@ export const ShowP = z.preprocess( * The list of seasons of this show. */ seasons: z.array(SeasonP).optional(), - }), -); + }) + .transform((x) => { + if (!x.thumbnail && x.poster) { + x.thumbnail = { ...x.poster }; + if (x.thumbnail) { + x.thumbnail.low = x.thumbnail.high; + x.thumbnail.medium = x.thumbnail.high; + } + } + return x; + }); /** * A tv serie or an anime. diff --git a/front/packages/models/src/resources/watch-info.ts b/front/packages/models/src/resources/watch-info.ts new file mode 100644 index 00000000..4c2ed400 --- /dev/null +++ b/front/packages/models/src/resources/watch-info.ts @@ -0,0 +1,118 @@ +/* + * Kyoo - A portable and vast media library solution. + * Copyright (c) Kyoo. + * + * See AUTHORS.md and LICENSE file in the project root for full license information. + * + * Kyoo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Kyoo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Kyoo. If not, see . + */ + +import { z } from "zod"; +import { imageFn } from "../traits"; + +/** + * A audio or subtitle track. + */ +export const TrackP = z.object({ + /** + * The index of this track on the episode. + */ + index: z.number(), + /** + * The title of the stream. + */ + title: z.string().nullable(), + /** + * The language of this stream (as a ISO-639-2 language code) + */ + language: z.string().nullable(), + /** + * The codec of this stream. + */ + codec: z.string(), + /** + * Is this stream the default one of it's type? + */ + isDefault: z.boolean(), + /** + * Is this stream tagged as forced? + */ + isForced: z.boolean(), +}); +export type Audio = z.infer; + +export const SubtitleP = TrackP.extend({ + /* + * The url of this track (only if this is a subtitle).. + */ + link: z.string().transform(imageFn).nullable(), +}); +export type Subtitle = z.infer; + +export const ChapterP = z.object({ + /** + * The start time of the chapter (in second from the start of the episode). + */ + startTime: z.number(), + /** + * The end time of the chapter (in second from the start of the episode). + */ + endTime: z.number(), + /** + * 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. + */ + name: z.string(), +}); +export type Chapter = z.infer; + +/** + * The transcoder's info for this item. This include subtitles, fonts, chapters... + */ +export const WatchInfoP = z.object({ + /** + * The sha1 of the video file. + */ + sha: z.string(), + /** + * The internal path of the video file. + */ + path: z.string(), + /** + * The container of the video file of this episode. Common containers are mp4, mkv, avi and so on. + */ + container: z.string(), + /** + * The list of audio tracks. + */ + audios: z.array(TrackP), + /** + * The list of subtitles tracks. + */ + subtitles: z.array(SubtitleP), + /** + * The list of fonts that can be used to display subtitles. + */ + fonts: z.array(z.string().transform(imageFn)), + /** + * The list of chapters. See Chapter for more information. + */ + chapters: z.array(ChapterP), +}); + +/** + * A watch info for a video + */ +export type WatchInfo = z.infer; diff --git a/front/packages/models/src/resources/watch-item.ts b/front/packages/models/src/resources/watch-item.ts deleted file mode 100644 index cf20b8dc..00000000 --- a/front/packages/models/src/resources/watch-item.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - -import { z } from "zod"; -import { zdate } from "../utils"; -import { ImagesP, imageFn } from "../traits"; -import { EpisodeP } from "./episode"; - -/** - * A audio or subtitle track. - */ -export const TrackP = z.object({ - /** - * The index of this track on the episode. - */ - index: z.number(), - /** - * The title of the stream. - */ - title: z.string().nullable(), - /** - * The language of this stream (as a ISO-639-2 language code) - */ - language: z.string().nullable(), - /** - * The codec of this stream. - */ - codec: z.string(), - /** - * Is this stream the default one of it's type? - */ - isDefault: z.boolean(), - /** - * Is this stream tagged as forced? - */ - isForced: z.boolean(), -}); -export type Audio = z.infer; - -export const SubtitleP = TrackP.extend({ - /* - * The url of this track (only if this is a subtitle).. - */ - link: z.string().transform(imageFn).nullable(), -}); -export type Subtitle = z.infer; - -export const ChapterP = z.object({ - /** - * The start time of the chapter (in second from the start of the episode). - */ - startTime: z.number(), - /** - * The end time of the chapter (in second from the start of the episode). - */ - endTime: z.number(), - /** - * 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. - */ - name: z.string(), -}); -export type Chapter = z.infer; - -const WatchMovieP = z.preprocess( - (x: any) => { - if (!x) return x; - - x.name = x.title; - return x; - }, - ImagesP.extend({ - /** - * The slug of this episode. - */ - slug: z.string(), - /** - * The title of this episode. - */ - name: z.string().nullable(), - /** - * The sumarry of this episode. - */ - overview: z.string().nullable(), - /** - * The release date of this episode. It can be null if unknown. - */ - releaseDate: zdate().nullable(), - - /** - * The transcoder's info for this item. This include subtitles, fonts, chapters... - */ - info: z.object({ - /** - * The sha1 of the video file. - */ - sha: z.string(), - /** - * The internal path of the video file. - */ - path: z.string(), - /** - * The container of the video file of this episode. Common containers are mp4, mkv, avi and so - * on. - */ - container: z.string(), - /** - * The list of audio tracks. - */ - audios: z.array(TrackP), - /** - * The list of subtitles tracks. - */ - subtitles: z.array(SubtitleP), - /** - * The list of fonts that can be used to display subtitles. - */ - fonts: z.array(z.string().transform(imageFn)), - /** - * The list of chapters. See Chapter for more information. - */ - chapters: z.array(ChapterP), - }), - /** - * The links to the videos of this watch item. - */ - link: z.object({ - direct: z.string().transform(imageFn), - hls: z.string().transform(imageFn), - }), - }), -); - -const WatchEpisodeP = WatchMovieP.and( - z.object({ - /** - * The ID of the episode associated with this item. - */ - episodeID: z.number(), - /** - * The title of the show containing this episode. - */ - showTitle: z.string(), - /** - * The slug of the show containing this episode - */ - showSlug: z.string(), - /** - * The season in witch this episode is in. - */ - seasonNumber: z.number().nullable(), - /** - * The number of this episode is it's season. - */ - episodeNumber: z.number().nullable(), - /** - * The absolute number of this episode. It's an episode number that is not reset to 1 after a - * new season. - */ - absoluteNumber: z.number().nullable(), - /** - * 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. - */ - previousEpisode: EpisodeP.nullable(), - /** - * 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. - */ - nextEpisode: EpisodeP.nullable(), - }), -); - -export const WatchItemP = z.union([ - WatchMovieP.and(z.object({ isMovie: z.literal(true) })), - WatchEpisodeP.and(z.object({ isMovie: z.literal(false) })), -]); - -/** - * A watch item for a movie or an episode - */ -export type WatchItem = z.infer; diff --git a/front/packages/models/src/traits/images.ts b/front/packages/models/src/traits/images.ts index c4dec740..969bfd8b 100644 --- a/front/packages/models/src/traits/images.ts +++ b/front/packages/models/src/traits/images.ts @@ -27,37 +27,38 @@ export const imageFn = (url: string) => ? `/api${url}` : kyooApiUrl + url; +export const Img = z.object({ + source: z.string(), + blurhash: z.string(), + low: z.string().transform(imageFn), + medium: z.string().transform(imageFn), + high: z.string().transform(imageFn), +}); + export const ImagesP = z.object({ /** * An url to the poster of this resource. If this resource does not have an image, the link will * be null. If the kyoo's instance is not capable of handling this kind of image for the specific * resource, this field won't be present. */ - poster: z.string().transform(imageFn).optional().nullable(), + poster: Img.nullable(), /** * An url to the thumbnail of this resource. If this resource does not have an image, the link * will be null. If the kyoo's instance is not capable of handling this kind of image for the * specific resource, this field won't be present. */ - thumbnail: z.string().transform(imageFn).optional().nullable(), + thumbnail: Img.nullable(), /** * An url to the logo of this resource. If this resource does not have an image, the link will be * null. If the kyoo's instance is not capable of handling this kind of image for the specific * resource, this field won't be present. */ - logo: z.string().transform(imageFn).optional().nullable(), - - /** - * An url to the thumbnail of this resource. If this resource does not have an image, the link - * will be null. If the kyoo's instance is not capable of handling this kind of image for the - * specific resource, this field won't be present. - */ - trailer: z.string().optional().nullable(), + logo: Img.nullable(), }); /** * Base traits for items that has image resources. */ -export type Images = z.infer; +export type KyooImage = z.infer; diff --git a/front/packages/models/tsconfig.json b/front/packages/models/tsconfig.json index cafc1ac7..8376e1ef 100755 --- a/front/packages/models/tsconfig.json +++ b/front/packages/models/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "declaration": true, "sourceMap": true, diff --git a/front/packages/primitives/package.json b/front/packages/primitives/package.json index f33302cd..b66e6948 100644 --- a/front/packages/primitives/package.json +++ b/front/packages/primitives/package.json @@ -5,17 +5,20 @@ "packageManager": "yarn@3.2.4", "devDependencies": { "@gorhom/portal": "^1.0.14", - "@types/react": "^18.0.28", - "typescript": "^4.9.5" + "@types/react": "18.2.0", + "typescript": "^5.1.6" }, "peerDependencies": { "@gorhom/portal": "*", "@material-symbols/svg-400": "*", "@radix-ui/react-dropdown-menu": "*", + "blurhash": "*", "expo-linear-gradient": "*", "moti": "*", "react": "*", "react-native": "*", + "react-native-blurhash": "*", + "react-native-fast-image": "*", "react-native-reanimated": "*", "react-native-svg": "*", "yoshiki": "*" @@ -27,13 +30,27 @@ "@radix-ui/react-dropdown-menu": { "optional": true }, + "blurhash": { + "optional": true + }, + "react-native-blurhash": { + "optional": true + }, + "react-native-fast-image": { + "optional": true + }, "react-native-web": { "optional": true } }, "dependencies": { - "@expo/html-elements": "^0.4.1", - "@tanstack/react-query": "^4.26.1", - "solito": "^3.0.0" + "@expo/html-elements": "^0.5.1", + "@tanstack/react-query": "^4.32.6", + "solito": "^4.0.1" + }, + "optionalDependencies": { + "blurhash": "^2.0.5", + "react-native-blurhash": "^1.1.11", + "react-native-fast-image": "^8.6.3" } } diff --git a/front/packages/primitives/src/avatar.tsx b/front/packages/primitives/src/avatar.tsx index 502ae5c2..97f021dd 100644 --- a/front/packages/primitives/src/avatar.tsx +++ b/front/packages/primitives/src/avatar.tsx @@ -19,14 +19,13 @@ */ import { View, ViewStyle } from "react-native"; -import { Image } from "./image"; +import { Image, ImageProps } from "./image"; import { useYoshiki, px, Stylable } from "yoshiki/native"; import { Icon } from "./icons"; import { P } from "./text"; import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg"; -import { YoshikiStyle } from "yoshiki/dist/type"; +import { YoshikiStyle } from "yoshiki/src/type"; import { ComponentType, forwardRef, RefAttributes } from "react"; -import { ts } from "./utils"; const stringToColor = (string: string) => { let hash = 0; @@ -47,7 +46,8 @@ const stringToColor = (string: string) => { export const Avatar = forwardRef< View, { - src?: string | null; + src?: ImageProps["src"]; + quality?: ImageProps["quality"]; alt?: string; size?: YoshikiStyle; placeholder?: string; @@ -56,8 +56,19 @@ export const Avatar = forwardRef< fill?: boolean; as?: ComponentType<{ style?: ViewStyle } & RefAttributes>; } & Stylable ->(function _Avatar( - { src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, as, ...props }, +>(function Avatar( + { + src, + quality = "low", + alt, + size = px(24), + color, + placeholder, + isLoading = false, + fill = false, + as, + ...props + }, ref, ) { const { css, theme } = useYoshiki(); @@ -88,7 +99,12 @@ export const Avatar = forwardRef< )} > {src || isLoading ? ( - {alt} + {alt} ) : placeholder ? (

{ +export const Chip = ({ + label, + color, + size = "medium", + ...props +}: { label: string; color?: string; size?: "small" | "medium" | "large" } & Stylable) => { const { css } = useYoshiki(); + const sizeMult = size == "medium" ? 1 : size == "small" ? 0.75 : 1.25; + return (

theme.contrast, bg: color ?? ((theme: Theme) => theme.accent), }, diff --git a/front/packages/primitives/src/icons.tsx b/front/packages/primitives/src/icons.tsx index 53c33c2c..8d8163d0 100644 --- a/front/packages/primitives/src/icons.tsx +++ b/front/packages/primitives/src/icons.tsx @@ -21,7 +21,7 @@ import React, { ComponentProps, ComponentType, ForwardedRef, forwardRef } from "react"; import { Platform, PressableProps, ViewStyle } from "react-native"; import { SvgProps } from "react-native-svg"; -import { YoshikiStyle } from "yoshiki/dist/type"; +import { YoshikiStyle } from "yoshiki/src/type"; import { px, Theme, useYoshiki } from "yoshiki/native"; import { PressableFeedback } from "./links"; import { alpha } from "./themes"; @@ -61,7 +61,7 @@ export const Icon = ({ icon: Icon, color, size = 24, ...props }: IconProps) => { ); }; -export const IconButton = forwardRef(function _IconButton( +export const IconButton = forwardRef(function IconButton( { icon, size, @@ -113,7 +113,7 @@ export const IconFab = ( bg: (theme) => theme.accent, fover: { self: { - transform: [{ scale: 1.3 }], + transform: "scale(1.3)" as any, bg: (theme: Theme) => theme.accent, }, }, diff --git a/front/packages/primitives/src/image.tsx b/front/packages/primitives/src/image.tsx deleted file mode 100644 index 62c3f1eb..00000000 --- a/front/packages/primitives/src/image.tsx +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - -import { ComponentType, ReactNode, useState } from "react"; -import { - View, - Image as Img, - ImageSourcePropType, - ImageStyle, - Platform, - ImageProps, - ViewProps, - ViewStyle, -} from "react-native"; -import { percent, useYoshiki } from "yoshiki/native"; -import { YoshikiStyle } from "yoshiki/dist/type"; -import { Skeleton } from "./skeleton"; -import { LinearGradient, LinearGradientProps } from "expo-linear-gradient"; -import { alpha, ContrastArea } from "./themes"; - -type YoshikiEnhanced