mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05:00 
			
		
		
		
	Feat rework images, delete providers (#191)
This commit is contained in:
		
						commit
						105aa7874f
					
				@ -33,10 +33,11 @@
 | 
			
		||||
		<Rule Id="SA1513" Action="None"/>         <!-- ClosingBraceMustBeFollowedByBlankLine -->
 | 
			
		||||
	</Rules>
 | 
			
		||||
	<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
 | 
			
		||||
		<Rule Id="SA1600" Action="None" />        <!-- Elements Shuld be Documented -->
 | 
			
		||||
		<Rule Id="SA1602" Action="None" />        <!-- Enums should be documented -->
 | 
			
		||||
		<Rule Id="SA1642" Action="None" />        <!-- ConstructorSummaryDocumentationMustBeginWithStandardText -->
 | 
			
		||||
		<Rule Id="SA1643" Action="None" />        <!-- DestructorSummaryDocumentationMustBeginWithStandardText -->
 | 
			
		||||
		<Rule Id="SA1623" Action="None" />        <!-- PropertySummaryDocumentationMustMatchAccessors -->
 | 
			
		||||
		<Rule Id="SA1629" Action="None" />        <!-- DocumentationTextMustEndWithAPeriod -->
 | 
			
		||||
		<Rule Id="SA1600" Action="None" />        <!-- Elements Shuld be Documented -->
 | 
			
		||||
	</Rules>
 | 
			
		||||
</RuleSet>
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup Condition="$(CheckCodingStyle) == true">
 | 
			
		||||
		<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet>
 | 
			
		||||
		<NoWarn>1591</NoWarn>
 | 
			
		||||
		<NoWarn>1591;1305;8618</NoWarn>
 | 
			
		||||
		<!-- <AnalysisMode>All</AnalysisMode> -->
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
 | 
			
		||||
@ -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<T> GetRepository<T>()
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The repository that handle libraries.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		ILibraryRepository LibraryRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The repository that handle libraries items (a wrapper around shows and collections).
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -55,6 +49,11 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		ICollectionRepository CollectionRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The repository that handle shows.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		IMovieRepository MovieRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The repository that handle shows.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -80,16 +79,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		IStudioRepository StudioRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The repository that handle genres.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		IGenreRepository GenreRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The repository that handle providers.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		IProviderRepository ProviderRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The repository that handle users.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -102,7 +91,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T">The type of the resource</typeparam>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Get<T>(int id)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
@ -113,7 +101,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T">The type of the resource</typeparam>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Get<T>(string slug)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
@ -124,7 +111,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T">The type of the resource</typeparam>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The first resource found that match the where function</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Get<T>(Expression<Func<T, bool>> where)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
@ -135,7 +121,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="seasonNumber">The season's number</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The season found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<Season> Get(int showID, int seasonNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -145,7 +130,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="seasonNumber">The season's number</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The season found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<Season> Get(string showSlug, int seasonNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -156,7 +140,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="episodeNumber">The episode's number</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The episode found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -167,7 +150,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="episodeNumber">The episode's number</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The episode found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -176,8 +158,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="id">The id of the resource</param>
 | 
			
		||||
		/// <typeparam name="T">The type of the resource</typeparam>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<T> GetOrDefault<T>(int id)
 | 
			
		||||
		Task<T?> GetOrDefault<T>(int id)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -186,8 +167,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="slug">The slug of the resource</param>
 | 
			
		||||
		/// <typeparam name="T">The type of the resource</typeparam>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<T> GetOrDefault<T>(string slug)
 | 
			
		||||
		Task<T?> GetOrDefault<T>(string slug)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -197,8 +177,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
 | 
			
		||||
		/// <typeparam name="T">The type of the resource</typeparam>
 | 
			
		||||
		/// <returns>The first resource found that match the where function</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T> sortBy = default)
 | 
			
		||||
		Task<T?> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T>? sortBy = default)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -207,8 +186,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="showID">The id of the show</param>
 | 
			
		||||
		/// <param name="seasonNumber">The season's number</param>
 | 
			
		||||
		/// <returns>The season found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<Season> GetOrDefault(int showID, int seasonNumber);
 | 
			
		||||
		Task<Season?> GetOrDefault(int showID, int seasonNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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
 | 
			
		||||
		/// <param name="showSlug">The slug of the show</param>
 | 
			
		||||
		/// <param name="seasonNumber">The season's number</param>
 | 
			
		||||
		/// <returns>The season found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<Season> GetOrDefault(string showSlug, int seasonNumber);
 | 
			
		||||
		Task<Season?> GetOrDefault(string showSlug, int seasonNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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
 | 
			
		||||
		/// <param name="seasonNumber">The season's number</param>
 | 
			
		||||
		/// <param name="episodeNumber">The episode's number</param>
 | 
			
		||||
		/// <returns>The episode found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
 | 
			
		||||
		Task<Episode?> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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
 | 
			
		||||
		/// <param name="seasonNumber">The season's number</param>
 | 
			
		||||
		/// <param name="episodeNumber">The episode's number</param>
 | 
			
		||||
		/// <returns>The episode found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
 | 
			
		||||
		Task<Episode?> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Load a related resource
 | 
			
		||||
@ -253,7 +228,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/>
 | 
			
		||||
		/// <seealso cref="Load{T}(T, string, bool)"/>
 | 
			
		||||
		/// <seealso cref="Load(IResource, string, bool)"/>
 | 
			
		||||
		Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member, bool force = false)
 | 
			
		||||
		Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member, bool force = false)
 | 
			
		||||
			where T : class, IResource
 | 
			
		||||
			where T2 : class, IResource;
 | 
			
		||||
 | 
			
		||||
@ -271,7 +246,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <seealso cref="Load{T,T2}(T, Expression{Func{T,T2}}, bool)"/>
 | 
			
		||||
		/// <seealso cref="Load{T}(T, string, bool)"/>
 | 
			
		||||
		/// <seealso cref="Load(IResource, string, bool)"/>
 | 
			
		||||
		Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
 | 
			
		||||
		Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
 | 
			
		||||
			where T : class, IResource
 | 
			
		||||
			where T2 : class;
 | 
			
		||||
 | 
			
		||||
@ -288,7 +263,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <seealso cref="Load{T,T2}(T, Expression{Func{T,T2}}, bool)"/>
 | 
			
		||||
		/// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/>
 | 
			
		||||
		/// <seealso cref="Load(IResource, string, bool)"/>
 | 
			
		||||
		Task<T> Load<T>([NotNull] T obj, string memberName, bool force = false)
 | 
			
		||||
		Task<T> Load<T>(T obj, string memberName, bool force = false)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -303,35 +278,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/>
 | 
			
		||||
		/// <seealso cref="Load{T}(T, string, bool)"/>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		Task Load([NotNull] IResource obj, string memberName, bool force = false);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get items (A wrapper around shows or collections) from a library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="id">The ID of the library</param>
 | 
			
		||||
		/// <param name="where">A filter function</param>
 | 
			
		||||
		/// <param name="sort">Sort information (sort order and sort by)</param>
 | 
			
		||||
		/// <param name="limit">How many items to return and where to start</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get items (A wrapper around shows or collections) from a library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="slug">The slug of the library</param>
 | 
			
		||||
		/// <param name="where">A filter function</param>
 | 
			
		||||
		/// <param name="sort">Sort information (sort order and sort by)</param>
 | 
			
		||||
		/// <param name="limit">How many items to return and where to start</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
		Task Load(IResource obj, string memberName, bool force = false);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get people's roles from a show.
 | 
			
		||||
@ -343,9 +290,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get people's roles from a show.
 | 
			
		||||
@ -357,9 +304,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get people's roles from a person.
 | 
			
		||||
@ -371,9 +318,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get people's roles from a person.
 | 
			
		||||
@ -385,27 +332,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Setup relations between a show, a library and a collection
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="showID">The show's ID to setup relations with</param>
 | 
			
		||||
		/// <param name="libraryID">The library's ID to setup relations with (optional)</param>
 | 
			
		||||
		/// <param name="collectionID">The collection's ID to setup relations with (optional)</param>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		Task AddShowLink(int showID, int? libraryID, int? collectionID);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Setup relations between a show, a library and a collection
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="show">The show to setup relations with</param>
 | 
			
		||||
		/// <param name="library">The library to setup relations with (optional)</param>
 | 
			
		||||
		/// <param name="collection">The collection to setup relations with (optional)</param>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		Task AddShowLink([NotNull] Show show, Library library, Collection collection);
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get all resources with filters
 | 
			
		||||
@ -415,9 +344,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="limit">How many items to return and where to start</param>
 | 
			
		||||
		/// <typeparam name="T">The type of resources to load</typeparam>
 | 
			
		||||
		/// <returns>A list of resources that match every filters</returns>
 | 
			
		||||
		Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null,
 | 
			
		||||
			Sort<T> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
		Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>>? where = null,
 | 
			
		||||
			Sort<T>? sort = default,
 | 
			
		||||
			Pagination? limit = default)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -426,7 +355,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="where">A filter function</param>
 | 
			
		||||
		/// <typeparam name="T">The type of resources to load</typeparam>
 | 
			
		||||
		/// <returns>A list of resources that match every filters</returns>
 | 
			
		||||
		Task<int> GetCount<T>(Expression<Func<T, bool>> where = null)
 | 
			
		||||
		Task<int> GetCount<T>(Expression<Func<T, bool>>? where = null)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -444,7 +373,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="item">The item to register</param>
 | 
			
		||||
		/// <typeparam name="T">The type of resource</typeparam>
 | 
			
		||||
		/// <returns>The resource registers and completed by database's information (related items and so on)</returns>
 | 
			
		||||
		Task<T> Create<T>([NotNull] T item)
 | 
			
		||||
		Task<T> Create<T>(T item)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -453,18 +382,31 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="item">The item to register</param>
 | 
			
		||||
		/// <typeparam name="T">The type of resource</typeparam>
 | 
			
		||||
		/// <returns>The newly created item or the existing value if it existed.</returns>
 | 
			
		||||
		Task<T> CreateIfNotExists<T>([NotNull] T item)
 | 
			
		||||
		Task<T> CreateIfNotExists<T>(T item)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Edit a resource
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="item">The resource to edit, it's ID can't change.</param>
 | 
			
		||||
		/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
 | 
			
		||||
		/// <typeparam name="T">The type of resources</typeparam>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
 | 
			
		||||
		Task<T> Edit<T>(T item, bool resetOld)
 | 
			
		||||
		Task<T> Edit<T>(T item)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Edit only specific properties of a resource
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="id">The id of the resource to edit</param>
 | 
			
		||||
		/// <param name="patch">
 | 
			
		||||
		/// 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
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <typeparam name="T">The type of resources</typeparam>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
 | 
			
		||||
		Task<T> Patch<T>(int id, Func<T, Task<bool>> patch)
 | 
			
		||||
			where T : class, IResource;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
		/// <param name="id">The id of the resource</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Get(int id);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -54,7 +52,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="slug">The slug of the resource</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Get(string slug);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -63,7 +60,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="where">A predicate to filter the resource.</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Get(Expression<Func<T, bool>> where);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -71,16 +67,14 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="id">The id of the resource</param>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<T> GetOrDefault(int id);
 | 
			
		||||
		Task<T?> GetOrDefault(int id);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get a resource from it's slug or null if it is not found.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="slug">The slug of the resource</param>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<T> GetOrDefault(string slug);
 | 
			
		||||
		Task<T?> GetOrDefault(string slug);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get the first resource that match the predicate or null if it is not found.
 | 
			
		||||
@ -88,15 +82,13 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="where">A predicate to filter the resource.</param>
 | 
			
		||||
		/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
 | 
			
		||||
		/// <returns>The resource found</returns>
 | 
			
		||||
		[ItemCanBeNull]
 | 
			
		||||
		Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default);
 | 
			
		||||
		Task<T?> GetOrDefault(Expression<Func<T, bool>> where, Sort<T>? sortBy = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Search for resources.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="query">The query string.</param>
 | 
			
		||||
		/// <returns>A list of resources found</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<ICollection<T>> Search(string query);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -106,33 +98,30 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="sort">Sort information about the query (sort by, sort order)</param>
 | 
			
		||||
		/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
 | 
			
		||||
		/// <returns>A list of resources that match every filters</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
 | 
			
		||||
			Sort<T> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
		Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null,
 | 
			
		||||
			Sort<T>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get the number of resources that match the filter's predicate.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="where">A filter predicate</param>
 | 
			
		||||
		/// <returns>How many resources matched that filter</returns>
 | 
			
		||||
		Task<int> GetCount(Expression<Func<T, bool>> where = null);
 | 
			
		||||
		Task<int> GetCount(Expression<Func<T, bool>>? where = null);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new resource.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="obj">The item to register</param>
 | 
			
		||||
		/// <returns>The resource registers and completed by database's information (related items and so on)</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Create([NotNull] T obj);
 | 
			
		||||
		Task<T> Create(T obj);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="obj">The object to create</param>
 | 
			
		||||
		/// <returns>The newly created item or the existing value if it existed.</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> CreateIfNotExists([NotNull] T obj);
 | 
			
		||||
		Task<T> CreateIfNotExists(T obj);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Called when a resource has been created.
 | 
			
		||||
@ -140,14 +129,24 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		event ResourceEventHandler OnCreated;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Edit a resource
 | 
			
		||||
		/// Edit a resource and replace every property
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="edited">The resource to edit, it's ID can't change.</param>
 | 
			
		||||
		/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
 | 
			
		||||
		[ItemNotNull]
 | 
			
		||||
		Task<T> Edit([NotNull] T edited, bool resetOld);
 | 
			
		||||
		Task<T> Edit(T edited);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Edit only specific properties of a resource
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="id">The id of the resource to edit</param>
 | 
			
		||||
		/// <param name="patch">
 | 
			
		||||
		/// 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
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
 | 
			
		||||
		Task<T> Patch(int id, Func<T, Task<bool>> patch);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Called when a resource has been edited.
 | 
			
		||||
@ -176,14 +175,14 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="obj">The resource to delete</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">If the item is not found</exception>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		Task Delete([NotNull] T obj);
 | 
			
		||||
		Task Delete(T obj);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Delete all resources that match the predicate.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		Task DeleteAll([NotNull] Expression<Func<T, bool>> where);
 | 
			
		||||
		Task DeleteAll(Expression<Func<T, bool>> where);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Called when a resource has been edited.
 | 
			
		||||
@ -202,21 +201,16 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		Type RepositoryType { get; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository to handle shows.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface IMovieRepository : IRepository<Movie> { }
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository to handle shows.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface IShowRepository : IRepository<Show>
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="showID">The ID of the show</param>
 | 
			
		||||
		/// <param name="libraryID">The ID of the library (optional)</param>
 | 
			
		||||
		/// <param name="collectionID">The ID of the collection (optional)</param>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		Task AddShowLink(int showID, int? libraryID, int? collectionID);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get a show's slug from it's ID.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -330,55 +324,16 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository to handle libraries.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface ILibraryRepository : IRepository<Library> { }
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository to handle library items (A wrapper around shows and collections).
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface ILibraryItemRepository : IRepository<LibraryItem>
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get items (A wrapper around shows or collections) from a library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="id">The ID of the library</param>
 | 
			
		||||
		/// <param name="where">A filter function</param>
 | 
			
		||||
		/// <param name="sort">Sort information (sort order and sort by)</param>
 | 
			
		||||
		/// <param name="limit">How many items to return and where to start</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get items (A wrapper around shows or collections) from a library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="slug">The slug of the library</param>
 | 
			
		||||
		/// <param name="where">A filter function</param>
 | 
			
		||||
		/// <param name="sort">Sort information (sort order and sort by)</param>
 | 
			
		||||
		/// <param name="limit">How many items to return and where to start</param>
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
	}
 | 
			
		||||
	public interface ILibraryItemRepository : IRepository<ILibraryItem> { }
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository for collections
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface ICollectionRepository : IRepository<Collection> { }
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository for genres.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface IGenreRepository : IRepository<Genre> { }
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository for studios.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
@ -399,9 +354,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetFromShow(int showID,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get people's roles from a show.
 | 
			
		||||
@ -413,9 +368,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get people's roles from a person.
 | 
			
		||||
@ -427,9 +382,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetFromPeople(int id,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get people's roles from a person.
 | 
			
		||||
@ -441,28 +396,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
 | 
			
		||||
		/// <returns>A list of items that match every filters</returns>
 | 
			
		||||
		Task<ICollection<PeopleRole>> GetFromPeople(string slug,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
			Sort<PeopleRole> sort = default,
 | 
			
		||||
			Pagination limit = default);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A repository to handle providers.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface IProviderRepository : IRepository<Provider>
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get a list of external ids that match all filters
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="where">A predicate to add arbitrary filter</param>
 | 
			
		||||
		/// <param name="sort">Sort information (sort order and sort by)</param>
 | 
			
		||||
		/// <param name="limit">Pagination information (where to start and how many to get)</param>
 | 
			
		||||
		/// <typeparam name="T">The type of metadata to retrieve</typeparam>
 | 
			
		||||
		/// <returns>A filtered list of external ids.</returns>
 | 
			
		||||
		Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
 | 
			
		||||
			Sort<MetadataID> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
			where T : class, IMetadata;
 | 
			
		||||
			Expression<Func<PeopleRole, bool>>? where = null,
 | 
			
		||||
			Sort<PeopleRole>? sort = default,
 | 
			
		||||
			Pagination? limit = default);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
 | 
			
		||||
@ -35,22 +35,20 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <param name="item">
 | 
			
		||||
		/// The item to cache images.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="alwaysDownload">
 | 
			
		||||
		/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <typeparam name="T">The type of the item</typeparam>
 | 
			
		||||
		/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
 | 
			
		||||
		Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
 | 
			
		||||
		Task DownloadImages<T>(T item)
 | 
			
		||||
			where T : IThumbnails;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Retrieve the local path of an image of the given item.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="item">The item to retrieve the poster from.</param>
 | 
			
		||||
		/// <param name="imageId">The ID of the image. See <see cref="Images"/> for values.</param>
 | 
			
		||||
		/// <param name="image">The ID of the image.</param>
 | 
			
		||||
		/// <param name="quality">The quality of the image</param>
 | 
			
		||||
		/// <typeparam name="T">The type of the item</typeparam>
 | 
			
		||||
		/// <returns>The path of the image for the given resource or null if it does not exists.</returns>
 | 
			
		||||
		string? GetImagePath<T>(T item, int imageId)
 | 
			
		||||
		string GetImagePath<T>(T item, string image, ImageQuality quality)
 | 
			
		||||
			where T : IThumbnails;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -83,6 +83,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T">A dependency that this action will use.</typeparam>
 | 
			
		||||
		/// <returns>A new <see cref="StartupAction"/></returns>
 | 
			
		||||
		public static StartupAction<T> New<T>(Action<T> action, int priority)
 | 
			
		||||
			where T : notnull
 | 
			
		||||
			=> new(action, priority);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -94,6 +95,8 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T2">A second dependency that this action will use.</typeparam>
 | 
			
		||||
		/// <returns>A new <see cref="StartupAction"/></returns>
 | 
			
		||||
		public static StartupAction<T, T2> New<T, T2>(Action<T, T2> action, int priority)
 | 
			
		||||
			where T : notnull
 | 
			
		||||
			where T2 : notnull
 | 
			
		||||
			=> new(action, priority);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -106,6 +109,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T3">A third dependency that this action will use.</typeparam>
 | 
			
		||||
		/// <returns>A new <see cref="StartupAction"/></returns>
 | 
			
		||||
		public static StartupAction<T, T2, T3> New<T, T2, T3>(Action<T, T2, T3> action, int priority)
 | 
			
		||||
			where T : notnull
 | 
			
		||||
			where T2 : notnull
 | 
			
		||||
			where T3 : notnull
 | 
			
		||||
			=> new(action, priority);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -144,6 +150,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <typeparam name="T">The dependency to use.</typeparam>
 | 
			
		||||
		public class StartupAction<T> : IStartupAction
 | 
			
		||||
			where T : notnull
 | 
			
		||||
		{
 | 
			
		||||
			/// <summary>
 | 
			
		||||
			/// The action to execute at startup.
 | 
			
		||||
@ -177,6 +184,8 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T">The dependency to use.</typeparam>
 | 
			
		||||
		/// <typeparam name="T2">The second dependency to use.</typeparam>
 | 
			
		||||
		public class StartupAction<T, T2> : IStartupAction
 | 
			
		||||
			where T : notnull
 | 
			
		||||
			where T2 : notnull
 | 
			
		||||
		{
 | 
			
		||||
			/// <summary>
 | 
			
		||||
			/// The action to execute at startup.
 | 
			
		||||
@ -214,6 +223,9 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <typeparam name="T2">The second dependency to use.</typeparam>
 | 
			
		||||
		/// <typeparam name="T3">The third dependency to use.</typeparam>
 | 
			
		||||
		public class StartupAction<T, T2, T3> : IStartupAction
 | 
			
		||||
			where T : notnull
 | 
			
		||||
			where T2 : notnull
 | 
			
		||||
			where T3 : notnull
 | 
			
		||||
		{
 | 
			
		||||
			/// <summary>
 | 
			
		||||
			/// The action to execute at startup.
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
		<Title>Kyoo.Abstractions</Title>
 | 
			
		||||
		<Description>Base package to create plugins for Kyoo.</Description>
 | 
			
		||||
		<RootNamespace>Kyoo.Abstractions</RootNamespace>
 | 
			
		||||
		<Nullable>enable</Nullable>
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using JetBrains.Annotations;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Abstractions.Models.Attributes
 | 
			
		||||
{
 | 
			
		||||
@ -32,23 +31,21 @@ namespace Kyoo.Abstractions.Models.Attributes
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The public name of this api.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[NotNull] public string Name { get; }
 | 
			
		||||
		public string Name { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The name of the group in witch this API is. You can also specify a custom sort order using the following
 | 
			
		||||
		/// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
 | 
			
		||||
		/// th alphabetical ordering.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Group { get; set; }
 | 
			
		||||
		public string? Group { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="ApiDefinitionAttribute"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="name">The name of the api that will be used on the documentation page.</param>
 | 
			
		||||
		public ApiDefinitionAttribute([NotNull] string name)
 | 
			
		||||
		public ApiDefinitionAttribute(string name)
 | 
			
		||||
		{
 | 
			
		||||
			if (name == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(name));
 | 
			
		||||
			Name = name;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ namespace Kyoo.Abstractions.Models.Attributes
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The name of the field containing the related resource's ID.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string RelationID { get; }
 | 
			
		||||
		public string? RelationID { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="LoadableRelationAttribute"/>.
 | 
			
		||||
 | 
			
		||||
@ -32,17 +32,17 @@ namespace Kyoo.Abstractions.Models.Permissions
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The needed permission type.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Type { get; }
 | 
			
		||||
		public string? Type { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The needed permission kind.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Kind Kind { get; }
 | 
			
		||||
		public Kind? Kind { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The group of this permission.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Group Group { get; set; }
 | 
			
		||||
		public Group? Group { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Ask a permission to run an action.
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using JetBrains.Annotations;
 | 
			
		||||
using Kyoo.Utils;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Abstractions.Models
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A class given information about a strongly typed configuration.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class ConfigurationReference
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The path of the resource (separated by ':')
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Path { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The type of the resource.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Type Type { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="ConfigurationReference"/> using a given path and type.
 | 
			
		||||
		/// This method does not create sub configuration resources. Please see <see cref="CreateReference"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="path">The path of the resource (separated by ':' or "__")</param>
 | 
			
		||||
		/// <param name="type">The type of the resource</param>
 | 
			
		||||
		/// <seealso cref="CreateReference"/>
 | 
			
		||||
		public ConfigurationReference(string path, Type type)
 | 
			
		||||
		{
 | 
			
		||||
			Path = path;
 | 
			
		||||
			Type = type;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Return the list of configuration reference a type has.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="path">
 | 
			
		||||
		/// The base path of the type (separated by ':' or "__". If empty, it will start at root)
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="type">The type of the object</param>
 | 
			
		||||
		/// <returns>The list of configuration reference a type has.</returns>
 | 
			
		||||
		public static IEnumerable<ConfigurationReference> CreateReference(string path, [NotNull] Type type)
 | 
			
		||||
		{
 | 
			
		||||
			if (type == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(type));
 | 
			
		||||
 | 
			
		||||
			List<ConfigurationReference> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Return the list of configuration reference a type has.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="path">
 | 
			
		||||
		/// The base path of the type (separated by ':' or "__". If empty, it will start at root)
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <typeparam name="T">The type of the object</typeparam>
 | 
			
		||||
		/// <returns>The list of configuration reference a type has.</returns>
 | 
			
		||||
		public static IEnumerable<ConfigurationReference> CreateReference<T>(string path)
 | 
			
		||||
		{
 | 
			
		||||
			return CreateReference(path, typeof(T));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Return a <see cref="ConfigurationReference"/> meaning that the given path is of any type.
 | 
			
		||||
		/// It means that the type can't be edited.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="path">
 | 
			
		||||
		/// The path that will be untyped (separated by ':' or "__". If empty, it will start at root).
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <returns>A configuration reference representing a path of any type.</returns>
 | 
			
		||||
		public static ConfigurationReference CreateUntyped(string path)
 | 
			
		||||
		{
 | 
			
		||||
			return new ConfigurationReference(path, null);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -30,13 +30,13 @@ namespace Kyoo.Abstractions.Models.Exceptions
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The existing object.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public object Existing { get; }
 | 
			
		||||
		public object? Existing { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="DuplicatedItemException"/> with the default message.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="existing">The existing object.</param>
 | 
			
		||||
		public DuplicatedItemException(object existing = null)
 | 
			
		||||
		public DuplicatedItemException(object? existing = null)
 | 
			
		||||
			: base("Already exists in the database.")
 | 
			
		||||
		{
 | 
			
		||||
			Existing = existing;
 | 
			
		||||
 | 
			
		||||
@ -19,13 +19,27 @@
 | 
			
		||||
namespace Kyoo.Abstractions.Models
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// 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.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface ILink
 | 
			
		||||
	public enum Genre
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The link to return, in most cases this should be a string.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public object Link { get; }
 | 
			
		||||
		Action,
 | 
			
		||||
		Adventure,
 | 
			
		||||
		Animation,
 | 
			
		||||
		Comedy,
 | 
			
		||||
		Crime,
 | 
			
		||||
		Documentary,
 | 
			
		||||
		Drama,
 | 
			
		||||
		Family,
 | 
			
		||||
		Fantasy,
 | 
			
		||||
		History,
 | 
			
		||||
		Horror,
 | 
			
		||||
		Music,
 | 
			
		||||
		Mystery,
 | 
			
		||||
		Romance,
 | 
			
		||||
		ScienceFiction,
 | 
			
		||||
		Thriller,
 | 
			
		||||
		War,
 | 
			
		||||
		Western,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// The type of item, ether a show, a movie or a collection.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public enum ItemType
 | 
			
		||||
	public enum ItemKind
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The <see cref="LibraryItem"/> is a <see cref="Show"/>.
 | 
			
		||||
@ -34,8 +35,7 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		Show,
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The <see cref="LibraryItem"/> is a Movie (a <see cref="Show"/> with
 | 
			
		||||
		/// <see cref="Models.Show.IsMovie"/> equals to true).
 | 
			
		||||
		/// The <see cref="LibraryItem"/> is a Movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		Movie,
 | 
			
		||||
 | 
			
		||||
@ -45,128 +45,135 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		Collection
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A type union between <see cref="Show"/> and <see cref="Collection"/>.
 | 
			
		||||
	/// This is used to list content put inside a library.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class LibraryItem : CustomTypeDescriptor, IResource, IThumbnails
 | 
			
		||||
	public class LibraryItem : IResource, ILibraryItem, IThumbnails, IMetadata
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[MaxLength(256)]
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of the show or collection.
 | 
			
		||||
		/// The title of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The summary of the show or collection.
 | 
			
		||||
		/// A catchphrase for this movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Overview { get; set; }
 | 
			
		||||
		public string? Tagline { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Is this show airing, not aired yet or finished? This is only applicable for shows.
 | 
			
		||||
		/// The list of alternative titles of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Status? Status { get; set; }
 | 
			
		||||
		public string[] Aliases { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this show or collection started airing. It can be null if this is unknown.
 | 
			
		||||
		/// The path of the movie video file.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Path { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The summary of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Overview { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A list of tags that match this movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of genres (themes) this show has.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Genre[] Genres { get; set; } = Array.Empty<Genre>();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Is this show airing, not aired yet or finished?
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Status Status { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this show started airing. It can be null if this is unknown.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DateTime? StartAir { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this show or collection finished airing.
 | 
			
		||||
		/// It must be after the <see cref="StartAir"/> but can be the same (example: for movies).
 | 
			
		||||
		/// The date this show finished airing.
 | 
			
		||||
		/// It can also be null if this is unknown.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DateTime? EndAir { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this movie aired.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DateTime? AirDate { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The type of this item (ether a collection, a show or a movie).
 | 
			
		||||
		/// A video of a few minutes that tease the content.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public ItemType Type { get; set; }
 | 
			
		||||
		public string? Trailer { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public ItemKind Kind { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new, empty <see cref="LibraryItem"/>.
 | 
			
		||||
		/// Links to watch this movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public VideoLinks? Links => Kind == ItemKind.Movie ? new()
 | 
			
		||||
		{
 | 
			
		||||
			Direct = $"/video/movie/{Slug}/direct",
 | 
			
		||||
			Hls = $"/video/movie/{Slug}/master.m3u8",
 | 
			
		||||
		}
 | 
			
		||||
		: null;
 | 
			
		||||
 | 
			
		||||
		public LibraryItem() { }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a <see cref="LibraryItem"/> from a show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="show">The show that this library item should represent.</param>
 | 
			
		||||
		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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a <see cref="LibraryItem"/> from a collection
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="collection">The collection that this library item should represent.</param>
 | 
			
		||||
		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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// An expression to create a <see cref="LibraryItem"/> representing a show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public static Expression<Func<Show, LibraryItem>> 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
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// An expression to create a <see cref="LibraryItem"/> representing a collection.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public static Expression<Func<Collection, LibraryItem>> 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
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override string GetClassName()
 | 
			
		||||
		{
 | 
			
		||||
			return Type.ToString();
 | 
			
		||||
			Slug = Utility.ToSlug(name);
 | 
			
		||||
			Name = name;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A type union between <see cref="Show"/> and <see cref="Collection"/>.
 | 
			
		||||
	/// This is used to list content put inside a library.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public interface ILibraryItem : IResource
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Is the item a collection, a movie or a show?
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public ItemKind Kind { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Name { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The summary of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Overview { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this movie aired.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DateTime? AirDate { get; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,48 +16,21 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
using Kyoo.Abstractions.Models.Attributes;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Abstractions.Models
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// ID and link of an item on an external provider.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class MetadataID
 | 
			
		||||
	public class MetadataId
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public static Expression<Func<MetadataID, object>> PrimaryKey
 | 
			
		||||
		{
 | 
			
		||||
			get { return x => new { First = x.ResourceID, Second = x.ProviderID }; }
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the resource which possess the metadata.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public int ResourceID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the provider.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public int ProviderID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The provider that can do something with this ID.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Provider Provider { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the resource on the external provider.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string DataID { get; set; }
 | 
			
		||||
		public string DataId { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The URL of the resource on the external provider.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Link { get; set; }
 | 
			
		||||
		public string? Link { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,33 +16,11 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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<Provider>
 | 
			
		||||
	{
 | 
			
		||||
		[SuppressMessage("ReSharper", "NotAccessedField.Local")]
 | 
			
		||||
		private readonly IProviderRepository _repository;
 | 
			
		||||
 | 
			
		||||
		protected AProviderTests(RepositoryActivator repositories)
 | 
			
		||||
			: base(repositories)
 | 
			
		||||
		{
 | 
			
		||||
			_repository = Repositories.LibraryManager.ProviderRepository;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	public string? Slug { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@ -29,10 +29,10 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
	public class PeopleRole : IResource
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public string Slug => ForPeople ? Show.Slug : People.Slug;
 | 
			
		||||
		public string Slug => ForPeople ? Show!.Slug : People.Slug;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Should this role be used as a Show substitute (the value is <c>true</c>) or
 | 
			
		||||
@ -53,12 +53,16 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the Show where the People playing in.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public int ShowID { get; set; }
 | 
			
		||||
		public int? ShowID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The show where the People played in.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Show Show { get; set; }
 | 
			
		||||
		public Show? Show { get; set; }
 | 
			
		||||
 | 
			
		||||
		public int? MovieID { get; set; }
 | 
			
		||||
 | 
			
		||||
		public Movie? Movie { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The type of work the person has done for the show.
 | 
			
		||||
 | 
			
		||||
@ -17,46 +17,63 @@
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using Kyoo.Abstractions.Models.Attributes;
 | 
			
		||||
using Kyoo.Utils;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Abstractions.Models
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A class representing collections of <see cref="Show"/>.
 | 
			
		||||
	/// A collection can also be stored in a <see cref="Library"/>.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class Collection : IResource, IMetadata, IThumbnails
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
		[MaxLength(256)] public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The name of this collection.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The description of this collection.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Overview { get; set; }
 | 
			
		||||
		public string? Overview { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of movies contained in this collection.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Movie>? Movies { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of shows contained in this collection.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Show> Shows { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of libraries that contains this collection.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Library> Libraries { get; set; }
 | 
			
		||||
		[LoadableRelation] public ICollection<Show>? Shows { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		public Collection() { }
 | 
			
		||||
 | 
			
		||||
		[JsonConstructor]
 | 
			
		||||
		public Collection(string name)
 | 
			
		||||
		{
 | 
			
		||||
			Slug = Utility.ToSlug(name);
 | 
			
		||||
			Name = name;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[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, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\d+)");
 | 
			
		||||
 | 
			
		||||
				if (match.Success)
 | 
			
		||||
@ -80,22 +76,22 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public string ShowSlug { private get; set; }
 | 
			
		||||
		[SerializeIgnore] public string? ShowSlug { private get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the Show containing this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public int ShowID { get; set; }
 | 
			
		||||
		[SerializeIgnore] public int ShowId { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The show that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
 | 
			
		||||
		[LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the Season containing this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public int? SeasonID { get; set; }
 | 
			
		||||
		[SerializeIgnore] public int? SeasonId { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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 <see cref="AbsoluteNumber"/>.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		[LoadableRelation(nameof(SeasonID))] public Season Season { get; set; }
 | 
			
		||||
		[LoadableRelation(nameof(SeasonId))] public Season? Season { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The season in witch this episode is in.
 | 
			
		||||
@ -127,18 +123,15 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
		public string? Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The overview of this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Overview { get; set; }
 | 
			
		||||
		public string? Overview { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The previous episode that should be seen before viewing this one.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public Episode? PreviousEpisode { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The next episode to watch after this one.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public Episode? NextEpisode { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Links to watch this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public VideoLinks Links => new()
 | 
			
		||||
		{
 | 
			
		||||
			Direct = $"/video/episode/{Slug}/direct",
 | 
			
		||||
			Hls = $"/video/episode/{Slug}/master.m3u8",
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <returns>The slug corresponding to the given arguments</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">The given show slug was null.</exception>
 | 
			
		||||
		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,
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using Kyoo.Abstractions.Models.Attributes;
 | 
			
		||||
using Kyoo.Utils;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Abstractions.Models
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A genre that allow one to specify categories for shows.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class Genre : IResource
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The name of this genre.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of shows that have this genre.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Show> Shows { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new, empty <see cref="Genre"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Genre() { }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="Genre"/> and specify it's <see cref="Name"/>.
 | 
			
		||||
		/// The <see cref="Slug"/> is automatically calculated from it's name.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="name">The name of this genre.</param>
 | 
			
		||||
		public Genre(string name)
 | 
			
		||||
		{
 | 
			
		||||
			Slug = Utility.ToSlug(name);
 | 
			
		||||
			Name = name;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -16,11 +16,7 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The link to metadata providers that this show has. See <see cref="MetadataID"/> for more information.
 | 
			
		||||
		/// The link to metadata providers that this show has. See <see cref="MetadataId"/> for more information.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[EditableRelation]
 | 
			
		||||
		[LoadableRelation]
 | 
			
		||||
		public ICollection<MetadataID> ExternalIDs { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A static class containing extensions method for every <see cref="IMetadata"/> class.
 | 
			
		||||
	/// This allow one to use metadata more easily.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public static class MetadataExtension
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Retrieve the internal provider's ID of an item using it's provider slug.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// This method will never return anything if the <see cref="IMetadata.ExternalIDs"/> are not loaded.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="self">An instance of <see cref="IMetadata"/> to retrieve the ID from.</param>
 | 
			
		||||
		/// <param name="provider">The slug of the provider</param>
 | 
			
		||||
		/// <returns>The <see cref="MetadataID.DataID"/> field of the asked provider.</returns>
 | 
			
		||||
		[CanBeNull]
 | 
			
		||||
		public static string GetID(this IMetadata self, string provider)
 | 
			
		||||
		{
 | 
			
		||||
			return self.ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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 <typeparamref name="T"/> type and <c>true</c> is returned.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// This method will never succeed if the <see cref="IMetadata.ExternalIDs"/> are not loaded.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="self">An instance of <see cref="IMetadata"/> to retrieve the ID from.</param>
 | 
			
		||||
		/// <param name="provider">The slug of the provider</param>
 | 
			
		||||
		/// <param name="id">
 | 
			
		||||
		/// The <see cref="MetadataID.DataID"/> field of the asked provider parsed
 | 
			
		||||
		/// and converted to the <typeparamref name="T"/> type.
 | 
			
		||||
		/// It is only relevant if this method returns <c>true</c>.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <typeparam name="T">The type to convert the <see cref="MetadataID.DataID"/> to.</typeparam>
 | 
			
		||||
		/// <returns><c>true</c> if this method succeeded, <c>false</c> otherwise.</returns>
 | 
			
		||||
		public static bool TryGetID<T>(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<string, MetadataId> ExternalId { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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 <see cref="IRepository{T}"/>.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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}.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		[MaxLength(256)]
 | 
			
		||||
		public string Slug { get; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,8 +16,11 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of images mapped to a certain index.
 | 
			
		||||
		/// A poster is a 2/3 format image with the cover of the resource.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// An arbitrary index should not be used, instead use indexes from <see cref="Models.Images"/>
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <example>{"0": "example.com/dune/poster"}</example>
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A class containing constant values for images. To be used as index of a <see cref="IThumbnails.Images"/>.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public static class Images
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A poster is a 9/16 format image with the cover of the resource.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public const int Poster = 0;
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public const int Thumbnail = 1;
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A logo is a small image representing the resource.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public const int Logo = 2;
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	[TypeConverter(typeof(ImageConvertor))]
 | 
			
		||||
	public class Image
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The original image from another server.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Source { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A video of a few minutes that tease the content.
 | 
			
		||||
		/// A hash to display as placeholder while the image is loading.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public const int Trailer = 3;
 | 
			
		||||
		[MaxLength(32)]
 | 
			
		||||
		public string Blurhash { get; set; }
 | 
			
		||||
 | 
			
		||||
		[SerializeIgnore]
 | 
			
		||||
		public string Path { private get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public static Dictionary<int, string> ImageName { get; } = new()
 | 
			
		||||
		public string Low => $"{Path}?quality=low";
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The url to retrieve the medium quality image.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Medium => $"{Path}?quality=medium";
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The url to retrieve the high quality image.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		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
 | 
			
		||||
		{
 | 
			
		||||
			/// <inheritdoc />
 | 
			
		||||
			public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
 | 
			
		||||
			{
 | 
			
		||||
				if (sourceType == typeof(string))
 | 
			
		||||
					return true;
 | 
			
		||||
				return base.CanConvertFrom(context, sourceType);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/// <inheritdoc />
 | 
			
		||||
			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);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/// <inheritdoc />
 | 
			
		||||
			public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
 | 
			
		||||
			{
 | 
			
		||||
				return false;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// The quality of an image
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public enum ImageQuality
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Small
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		Low,
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Medium
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		Medium,
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Large
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		High,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using Kyoo.Abstractions.Models.Attributes;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Abstractions.Models
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A library containing <see cref="Show"/> and <see cref="Collection"/>.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class Library : IResource
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The name of this library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of paths that this library is responsible for. This is mainly used by the Scan task.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string[] Paths { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of <see cref="Provider"/> used for items in this library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<Provider> Providers { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of shows in this library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Show> Shows { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of collections in this library.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Collection> Collections { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										152
									
								
								back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
									
									
									
									
									
										Normal file
									
								
							@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A series or a movie.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class Movie : IResource, IMetadata, IOnMerge, IThumbnails
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[MaxLength(256)]
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A catchphrase for this movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Tagline { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of alternative titles of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string[] Aliases { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The path of the movie video file.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The summary of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Overview { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A list of tags that match this movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of genres (themes) this show has.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Genre[] Genres { get; set; } = Array.Empty<Genre>();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Is this show airing, not aired yet or finished?
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Status Status { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this movie aired.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DateTime? AirDate { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A video of a few minutes that tease the content.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Trailer { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the Studio that made this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public int? StudioID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The Studio that made this show.
 | 
			
		||||
		/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation(nameof(StudioID))][EditableRelation] public Studio? Studio { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of people that made this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation][EditableRelation] public ICollection<PeopleRole>? People { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of collections that contains this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Collection>? Collections { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Links to watch this movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public VideoLinks Links => new()
 | 
			
		||||
		{
 | 
			
		||||
			Direct = $"/video/movie/{Slug}/direct",
 | 
			
		||||
			Hls = $"/video/movie/{Slug}/master.m3u8",
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -17,7 +17,10 @@
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[MaxLength(256)]
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -38,14 +42,29 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<PeopleRole> Roles { get; set; }
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<PeopleRole>? Roles { get; set; }
 | 
			
		||||
 | 
			
		||||
		public People() { }
 | 
			
		||||
 | 
			
		||||
		[JsonConstructor]
 | 
			
		||||
		public People(string name)
 | 
			
		||||
		{
 | 
			
		||||
			Slug = Utility.ToSlug(name);
 | 
			
		||||
			Name = name;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using Kyoo.Abstractions.Models.Attributes;
 | 
			
		||||
using Kyoo.Utils;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Abstractions.Models
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A dead class that will be removed later.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	// TODO: Delete this class
 | 
			
		||||
	public class Provider : IResource, IThumbnails
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The name of this provider.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of libraries that uses this provider.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Library> Libraries { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new, default, <see cref="Provider"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Provider() { }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="Provider"/> and specify it's <see cref="Name"/>.
 | 
			
		||||
		/// The <see cref="Slug"/> is automatically calculated from it's name.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="name">The name of this provider.</param>
 | 
			
		||||
		/// <param name="logo">The logo of this provider.</param>
 | 
			
		||||
		public Provider(string name, string logo)
 | 
			
		||||
		{
 | 
			
		||||
			Slug = Utility.ToSlug(name);
 | 
			
		||||
			Name = name;
 | 
			
		||||
			Images = new Dictionary<int, string>
 | 
			
		||||
			{
 | 
			
		||||
				[Models.Images.Logo] = logo
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[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, @"(?<show>.+)-s(?<season>\d+)");
 | 
			
		||||
				Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)");
 | 
			
		||||
 | 
			
		||||
				if (!match.Success)
 | 
			
		||||
					throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}");
 | 
			
		||||
@ -60,18 +62,18 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public string ShowSlug { private get; set; }
 | 
			
		||||
		[SerializeIgnore] public string? ShowSlug { private get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the Show containing this season.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public int ShowID { get; set; }
 | 
			
		||||
		[SerializeIgnore] public int ShowId { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The show that contains this season.
 | 
			
		||||
		/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
 | 
			
		||||
		[LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The number of this season. This can be set to 0 to indicate specials.
 | 
			
		||||
@ -81,12 +83,12 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of this season.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
		public string? Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A quick overview of this season.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Overview { get; set; }
 | 
			
		||||
		public string? Overview { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The starting air date of this season.
 | 
			
		||||
@ -99,14 +101,20 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		public DateTime? EndDate { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of episodes that this season contains.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
 | 
			
		||||
		[LoadableRelation] public ICollection<Episode>? Episodes { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[MaxLength(256)]
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A catchphrase for this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Tagline { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of alternative titles of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[EditableRelation] public string[] Aliases { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The path of the root directory of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public string Path { get; set; }
 | 
			
		||||
		public List<string> Aliases { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The summary of this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Overview { get; set; }
 | 
			
		||||
		public string? Overview { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A list of tags that match this movie.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public List<string> Tags { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of genres (themes) this show has.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public List<Genre> Genres { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Is this show airing, not aired yet or finished?
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Status Status { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// An URL to a trailer.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// 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);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this show started airing. It can be null if this is unknown.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -73,64 +80,61 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The date this show finished airing.
 | 
			
		||||
		/// It must be after the <see cref="StartAir"/> but can be the same (example: for movies).
 | 
			
		||||
		/// It can also be null if this is unknown.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DateTime? EndAir { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// True if this show represent a movie, false otherwise.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public bool IsMovie { get; set; }
 | 
			
		||||
		public Image? Poster { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
 | 
			
		||||
		public Image? Thumbnail { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A video of a few minutes that tease the content.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string? Trailer { get; set; }
 | 
			
		||||
 | 
			
		||||
		[SerializeIgnore] public DateTime? AirDate => StartAir;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the Studio that made this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore] public int? StudioID { get; set; }
 | 
			
		||||
		[SerializeIgnore] public int? StudioId { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The Studio that made this show.
 | 
			
		||||
		/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation(nameof(StudioID))][EditableRelation] public Studio Studio { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of genres (themes) this show has.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation][EditableRelation] public ICollection<Genre> Genres { get; set; }
 | 
			
		||||
		[LoadableRelation(nameof(StudioId))][EditableRelation] public Studio? Studio { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of people that made this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation][EditableRelation] public ICollection<PeopleRole> People { get; set; }
 | 
			
		||||
		[LoadableRelation][EditableRelation] public ICollection<PeopleRole>? People { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The different seasons in this show. If this is a movie, this list is always null or empty.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Season> Seasons { get; set; }
 | 
			
		||||
		[LoadableRelation] public ICollection<Season>? Seasons { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of libraries that contains this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Library> Libraries { get; set; }
 | 
			
		||||
		[LoadableRelation] public ICollection<Episode>? Episodes { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of collections that contains this show.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Collection> Collections { get; set; }
 | 
			
		||||
		[LoadableRelation] public ICollection<Collection>? Collections { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// <summary>
 | 
			
		||||
 | 
			
		||||
@ -17,8 +17,10 @@
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[MaxLength(256)]
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -41,10 +44,15 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of shows that are made by this studio.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Show> Shows { get; set; }
 | 
			
		||||
		[LoadableRelation] public ICollection<Show>? Shows { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of movies that are made by this studio.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[LoadableRelation] public ICollection<Movie>? Movies { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
 | 
			
		||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new, empty, <see cref="Studio"/>.
 | 
			
		||||
@ -55,6 +63,7 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="name">The name of the studio.</param>
 | 
			
		||||
		[JsonConstructor]
 | 
			
		||||
		public Studio(string name)
 | 
			
		||||
		{
 | 
			
		||||
			Slug = Utility.ToSlug(name);
 | 
			
		||||
 | 
			
		||||
@ -16,20 +16,25 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A single user of the app.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class User : IResource, IThumbnails
 | 
			
		||||
	public class User : IResource
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public int ID { get; set; }
 | 
			
		||||
		public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		[MaxLength(256)]
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -51,27 +56,32 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of permissions of the user. The format of this is implementation dependent.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string[] Permissions { get; set; }
 | 
			
		||||
		public string[] Permissions { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Arbitrary extra data that can be used by specific authentication implementations.
 | 
			
		||||
		/// A logo is a small image representing the resource.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore]
 | 
			
		||||
		public Dictionary<string, string> ExtraData { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
		public Image? Logo { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of shows the user has finished.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore]
 | 
			
		||||
		public ICollection<Show> Watched { get; set; }
 | 
			
		||||
		public ICollection<Show>? Watched { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of episodes the user is watching (stopped in progress or the next episode of the show)
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		[SerializeIgnore]
 | 
			
		||||
		public ICollection<WatchedEpisode> CurrentlyWatching { get; set; }
 | 
			
		||||
		public ICollection<WatchedEpisode>? CurrentlyWatching { get; set; }
 | 
			
		||||
 | 
			
		||||
		public User() { }
 | 
			
		||||
 | 
			
		||||
		[JsonConstructor]
 | 
			
		||||
		public User(string username)
 | 
			
		||||
		{
 | 
			
		||||
			Slug = Utility.ToSlug(username);
 | 
			
		||||
			Username = username;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The <see cref="Episode"/> started.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Episode Episode { get; set; }
 | 
			
		||||
		public Episode? Episode { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Where the player has stopped watching the episode (between 0 and 100).
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,16 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public ICollection<Collection> Collections { get; init; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The items that matched the search.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public ICollection<ILibraryItem> Items { get; init; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The movies that matched the search.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public ICollection<Movie> Movies { get; init; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The shows that matched the search.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -50,11 +60,6 @@ namespace Kyoo.Abstractions.Models
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public ICollection<People> People { get; init; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The genres that matched the search.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public ICollection<Genre> Genres { get; init; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The studios that matched the search.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The slug of the resource or null if the id is specified.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly string _slug;
 | 
			
		||||
		private readonly string? _slug;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="Identifier"/> for the given id.
 | 
			
		||||
@ -58,10 +57,8 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
			
		||||
		/// Create a new <see cref="Identifier"/> for the given slug.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="slug">The slug of the resource.</param>
 | 
			
		||||
		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!);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -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<T2>());
 | 
			
		||||
			MethodCallExpression call = Expression.Call(null, method, listGetter.Body, IsSame<T2>());
 | 
			
		||||
			return Expression.Lambda<Func<T, bool>>(call, listGetter.Parameters);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -183,7 +180,7 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
			
		||||
		{
 | 
			
		||||
			return _id.HasValue
 | 
			
		||||
				? _id.Value.ToString()
 | 
			
		||||
				: _slug;
 | 
			
		||||
				: _slug!;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -192,7 +189,7 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
			
		||||
		public class IdentifierConvertor : TypeConverter
 | 
			
		||||
		{
 | 
			
		||||
			/// <inheritdoc />
 | 
			
		||||
			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
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/// <inheritdoc />
 | 
			
		||||
			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);
 | 
			
		||||
 | 
			
		||||
@ -31,13 +31,13 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
			
		||||
		/// The list of errors that where made in the request.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <example><c>["InvalidFilter: no field 'startYear' on a collection"]</c></example>
 | 
			
		||||
		[NotNull] public string[] Errors { get; set; }
 | 
			
		||||
		public string[] Errors { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="RequestError"/> with one error.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="error">The error to specify in the response.</param>
 | 
			
		||||
		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 <see cref="RequestError"/> with multiple errors.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="errors">The errors to specify in the response.</param>
 | 
			
		||||
		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));
 | 
			
		||||
 | 
			
		||||
@ -33,11 +33,11 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Sort by a specific key
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="key">The sort keys. This members will be used to sort the results.</param>
 | 
			
		||||
		/// <param name="desendant">
 | 
			
		||||
		/// <param name="Key">The sort keys. This members will be used to sort the results.</param>
 | 
			
		||||
		/// <param name="Desendant">
 | 
			
		||||
		/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		public record By(string key, bool desendant = false) : Sort<T>
 | 
			
		||||
		public record By(string Key, bool Desendant = false) : Sort<T>
 | 
			
		||||
		{
 | 
			
		||||
			/// <summary>
 | 
			
		||||
			/// Sort by a specific key
 | 
			
		||||
@ -53,8 +53,8 @@ namespace Kyoo.Abstractions.Controllers
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Sort by multiple keys.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="list">The list of keys to sort by.</param>
 | 
			
		||||
		public record Conglomerate(params Sort<T>[] list) : Sort<T>;
 | 
			
		||||
		/// <param name="List">The list of keys to sort by.</param>
 | 
			
		||||
		public record Conglomerate(params Sort<T>[] List) : Sort<T>;
 | 
			
		||||
 | 
			
		||||
		/// <summary>The default sort method for the given type.</summary>
 | 
			
		||||
		public record Default : Sort<T>;
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -16,33 +16,21 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// The links to see a movie or an episode.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class VideoLinks
 | 
			
		||||
	{
 | 
			
		||||
		[Collection(nameof(Postgresql))]
 | 
			
		||||
		public class GenreTests : AGenreTests
 | 
			
		||||
		{
 | 
			
		||||
			public GenreTests(PostgresFixture postgres, ITestOutputHelper output)
 | 
			
		||||
				: base(new RepositoryActivator(output, postgres)) { }
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The direct link to the unprocessed video (pristine quality).
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Direct { get; set; }
 | 
			
		||||
 | 
			
		||||
	public abstract class AGenreTests : RepositoryTests<Genre>
 | 
			
		||||
	{
 | 
			
		||||
		[SuppressMessage("ReSharper", "NotAccessedField.Local")]
 | 
			
		||||
		private readonly IGenreRepository _repository;
 | 
			
		||||
 | 
			
		||||
		protected AGenreTests(RepositoryActivator repositories)
 | 
			
		||||
			: base(repositories)
 | 
			
		||||
		{
 | 
			
		||||
			_repository = Repositories.LibraryManager.GenreRepository;
 | 
			
		||||
		}
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The link to an HLS master playlist containing all qualities available for this video.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Hls { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// 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 <see cref="Episode"/> with another form.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class WatchItem : CustomTypeDescriptor, IThumbnails, ILink
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The ID of the episode associated with this item.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public int EpisodeID { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The slug of this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Slug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of the show containing this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string ShowTitle { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The slug of the show containing this episode
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string ShowSlug { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The season in witch this episode is in.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public int? SeasonNumber { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The number of this episode is it's season.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public int? EpisodeNumber { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public int? AbsoluteNumber { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The title of this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The summary of this episode.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public string Overview { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The release date of this episode. It can be null if unknown.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DateTime? ReleaseDate { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Episode PreviousEpisode { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public Episode NextEpisode { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// <c>true</c> if this is a movie, <c>false</c> otherwise.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public bool IsMovie { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Dictionary<int, string> Images { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The transcoder's info for this item. This include subtitles, fonts, chapters...
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public object Info { get; set; }
 | 
			
		||||
 | 
			
		||||
		[SerializeIgnore]
 | 
			
		||||
		private string _Type => IsMovie ? "movie" : "episode";
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public object Link => new
 | 
			
		||||
		{
 | 
			
		||||
			Direct = $"/video/{_Type}/{Slug}/direct",
 | 
			
		||||
			Hls = $"/video/{_Type}/{Slug}/master.m3u8",
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a <see cref="WatchItem"/> from an <see cref="Episode"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="ep">The episode to transform.</param>
 | 
			
		||||
		/// <param name="library">
 | 
			
		||||
		/// A library manager to retrieve the next and previous episode and load the show and tracks of the episode.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="client">A http client to reach the transcoder.</param>
 | 
			
		||||
		/// <returns>A new WatchItem representing the given episode.</returns>
 | 
			
		||||
		public static async Task<WatchItem> 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<Episode>(
 | 
			
		||||
							where: x => x.ShowID == ep.ShowID,
 | 
			
		||||
							limit: new Pagination(1, ep.ID, true)
 | 
			
		||||
						)).FirstOrDefault(),
 | 
			
		||||
				NextEpisode = ep.Show.IsMovie
 | 
			
		||||
					? null
 | 
			
		||||
					: (await library.GetAll<Episode>(
 | 
			
		||||
							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<object> _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<object>(content);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override string GetClassName()
 | 
			
		||||
		{
 | 
			
		||||
			return nameof(Show);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override string GetComponentName()
 | 
			
		||||
		{
 | 
			
		||||
			return ShowSlug;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -43,7 +43,7 @@ namespace Kyoo.Abstractions
 | 
			
		||||
		{
 | 
			
		||||
			return builder.RegisterType<T>()
 | 
			
		||||
				.As<IBaseRepository>()
 | 
			
		||||
				.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>)))
 | 
			
		||||
				.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))!)
 | 
			
		||||
				.InstancePerLifetimeScope();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -59,6 +59,7 @@ namespace Kyoo.Abstractions
 | 
			
		||||
		/// <returns>The initial container.</returns>
 | 
			
		||||
		public static IRegistrationBuilder<T2, ConcreteReflectionActivatorData, SingleRegistrationStyle>
 | 
			
		||||
			RegisterRepository<T, T2>(this ContainerBuilder builder)
 | 
			
		||||
			where T : notnull
 | 
			
		||||
			where T2 : IBaseRepository, T
 | 
			
		||||
		{
 | 
			
		||||
			return builder.RegisterRepository<T2>().As<T>();
 | 
			
		||||
 | 
			
		||||
@ -17,9 +17,7 @@
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public static class EnumerableExtensions
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A Select where the index of the item can be used.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
 | 
			
		||||
		/// <param name="mapper">The function that will map each items</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in <paramref name="self"/></typeparam>
 | 
			
		||||
		/// <typeparam name="T2">The type of items in the returned list</typeparam>
 | 
			
		||||
		/// <returns>The list mapped.</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
 | 
			
		||||
		[LinqTunnel]
 | 
			
		||||
		public static IEnumerable<T2> Map<T, T2>([NotNull] this IEnumerable<T> self,
 | 
			
		||||
			[NotNull] Func<T, int, T2> mapper)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(self));
 | 
			
		||||
			if (mapper == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(mapper));
 | 
			
		||||
 | 
			
		||||
			static IEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, T2> mapper)
 | 
			
		||||
			{
 | 
			
		||||
				using IEnumerator<T> enumerator = self.GetEnumerator();
 | 
			
		||||
				int index = 0;
 | 
			
		||||
 | 
			
		||||
				while (enumerator.MoveNext())
 | 
			
		||||
				{
 | 
			
		||||
					yield return mapper(enumerator.Current, index);
 | 
			
		||||
					index++;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return Generator(self, mapper);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A map where the mapping function is asynchronous.
 | 
			
		||||
		/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The IEnumerable to map.</param>
 | 
			
		||||
		/// <param name="mapper">The asynchronous function that will map each items.</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in <paramref name="self"/>.</typeparam>
 | 
			
		||||
		/// <typeparam name="T2">The type of items in the returned list.</typeparam>
 | 
			
		||||
		/// <returns>The list mapped as an AsyncEnumerable.</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">The list or the mapper can't be null.</exception>
 | 
			
		||||
		[LinqTunnel]
 | 
			
		||||
		public static IAsyncEnumerable<T2> MapAsync<T, T2>([NotNull] this IEnumerable<T> self,
 | 
			
		||||
			[NotNull] Func<T, int, Task<T2>> mapper)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(self));
 | 
			
		||||
			if (mapper == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(mapper));
 | 
			
		||||
 | 
			
		||||
			static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, Task<T2>> mapper)
 | 
			
		||||
			{
 | 
			
		||||
				using IEnumerator<T> enumerator = self.GetEnumerator();
 | 
			
		||||
				int index = 0;
 | 
			
		||||
 | 
			
		||||
				while (enumerator.MoveNext())
 | 
			
		||||
				{
 | 
			
		||||
					yield return await mapper(enumerator.Current, index);
 | 
			
		||||
					index++;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return Generator(self, mapper);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// An asynchronous version of Select.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The IEnumerable to map</param>
 | 
			
		||||
		/// <param name="mapper">The asynchronous function that will map each items</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in <paramref name="self"/></typeparam>
 | 
			
		||||
		/// <typeparam name="T2">The type of items in the returned list</typeparam>
 | 
			
		||||
		/// <returns>The list mapped as an AsyncEnumerable</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
 | 
			
		||||
		[LinqTunnel]
 | 
			
		||||
		public static IAsyncEnumerable<T2> SelectAsync<T, T2>([NotNull] this IEnumerable<T> self,
 | 
			
		||||
			[NotNull] Func<T, Task<T2>> mapper)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(self));
 | 
			
		||||
			if (mapper == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(mapper));
 | 
			
		||||
 | 
			
		||||
			static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, Task<T2>> mapper)
 | 
			
		||||
			{
 | 
			
		||||
				using IEnumerator<T> enumerator = self.GetEnumerator();
 | 
			
		||||
 | 
			
		||||
				while (enumerator.MoveNext())
 | 
			
		||||
					yield return await mapper(enumerator.Current);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return Generator(self, mapper);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Convert an AsyncEnumerable to a List by waiting for every item.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The async list</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
 | 
			
		||||
		/// <returns>A task that will return a simple list</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">The list can't be null</exception>
 | 
			
		||||
		[LinqTunnel]
 | 
			
		||||
		public static Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(self));
 | 
			
		||||
 | 
			
		||||
			static async Task<List<T>> ToList(IAsyncEnumerable<T> self)
 | 
			
		||||
			{
 | 
			
		||||
				List<T> ret = new();
 | 
			
		||||
				await foreach (T i in self)
 | 
			
		||||
					ret.Add(i);
 | 
			
		||||
				return ret;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return ToList(self);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// If the enumerable is empty, execute an action.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The enumerable to check</param>
 | 
			
		||||
		/// <param name="action">The action to execute is the list is empty</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items inside the list</typeparam>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">The iterable and the action can't be null.</exception>
 | 
			
		||||
		/// <returns>The iterator proxied, there is no dual iterations.</returns>
 | 
			
		||||
		[LinqTunnel]
 | 
			
		||||
		public static IEnumerable<T> IfEmpty<T>([NotNull] this IEnumerable<T> self, [NotNull] Action action)
 | 
			
		||||
		public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(self));
 | 
			
		||||
			if (action == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(action));
 | 
			
		||||
 | 
			
		||||
			static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
 | 
			
		||||
			{
 | 
			
		||||
				using IEnumerator<T> enumerator = self.GetEnumerator();
 | 
			
		||||
@ -190,111 +63,12 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
 | 
			
		||||
		/// <param name="action">The action to execute for each arguments</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in the list</typeparam>
 | 
			
		||||
		public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
 | 
			
		||||
		public static void ForEach<T>(this IEnumerable<T>? self, Action<T> action)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				return;
 | 
			
		||||
			foreach (T i in self)
 | 
			
		||||
				action(i);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A foreach used as a function with a little specificity: the list can be null.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
 | 
			
		||||
		/// <param name="action">The action to execute for each arguments</param>
 | 
			
		||||
		public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				return;
 | 
			
		||||
			foreach (object i in self)
 | 
			
		||||
				action(i);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A foreach used as a function with a little specificity: the list can be null.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
 | 
			
		||||
		/// <param name="action">The action to execute for each arguments</param>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> action)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				return;
 | 
			
		||||
			foreach (object i in self)
 | 
			
		||||
				await action(i);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A foreach used as a function with a little specificity: the list can be null.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
 | 
			
		||||
		/// <param name="action">The asynchronous action to execute for each arguments</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in the list.</typeparam>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				return;
 | 
			
		||||
			foreach (T i in self)
 | 
			
		||||
				await action(i);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A foreach used as a function with a little specificity: the list can be null.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="self">The async list to enumerate. If this is null, the function result in a no-op</param>
 | 
			
		||||
		/// <param name="action">The action to execute for each arguments</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in the list.</typeparam>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
 | 
			
		||||
		{
 | 
			
		||||
			if (self == null)
 | 
			
		||||
				return;
 | 
			
		||||
			await foreach (T i in self)
 | 
			
		||||
				action(i);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Split a list in a small chunk of data.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="list">The list to split</param>
 | 
			
		||||
		/// <param name="countPerList">The number of items in each chunk</param>
 | 
			
		||||
		/// <typeparam name="T">The type of data in the initial list.</typeparam>
 | 
			
		||||
		/// <returns>A list of chunks</returns>
 | 
			
		||||
		[LinqTunnel]
 | 
			
		||||
		public static IEnumerable<List<T>> BatchBy<T>(this List<T> list, int countPerList)
 | 
			
		||||
		{
 | 
			
		||||
			for (int i = 0; i < list.Count; i += countPerList)
 | 
			
		||||
				yield return list.GetRange(i, Math.Min(list.Count - i, countPerList));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Split a list in a small chunk of data.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="list">The list to split</param>
 | 
			
		||||
		/// <param name="countPerList">The number of items in each chunk</param>
 | 
			
		||||
		/// <typeparam name="T">The type of data in the initial list.</typeparam>
 | 
			
		||||
		/// <returns>A list of chunks</returns>
 | 
			
		||||
		[LinqTunnel]
 | 
			
		||||
		public static IEnumerable<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
 | 
			
		||||
		{
 | 
			
		||||
			T[] ret = new T[countPerList];
 | 
			
		||||
			int i = 0;
 | 
			
		||||
 | 
			
		||||
			using IEnumerator<T> 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,13 +17,10 @@
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public static class Merger
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Merge two lists, can keep duplicates or remove them.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="first">The first enumerable to merge</param>
 | 
			
		||||
		/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
 | 
			
		||||
		/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
 | 
			
		||||
		/// <typeparam name="T">The type of items in the lists to merge.</typeparam>
 | 
			
		||||
		/// <returns>The two list merged as an array</returns>
 | 
			
		||||
		[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
 | 
			
		||||
		public static T[] MergeLists<T>([CanBeNull] IEnumerable<T> first,
 | 
			
		||||
			[CanBeNull] IEnumerable<T> second,
 | 
			
		||||
			[CanBeNull] Func<T, T, bool> isEqual = null)
 | 
			
		||||
		{
 | 
			
		||||
			if (first == null)
 | 
			
		||||
				return second?.ToArray();
 | 
			
		||||
			if (second == null)
 | 
			
		||||
				return first.ToArray();
 | 
			
		||||
			if (isEqual == null)
 | 
			
		||||
				return first.Concat(second).ToArray();
 | 
			
		||||
			List<T> list = first.ToList();
 | 
			
		||||
			return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="first">The first dictionary to merge</param>
 | 
			
		||||
		/// <param name="second">The second dictionary to merge</param>
 | 
			
		||||
		/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
 | 
			
		||||
		/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
 | 
			
		||||
		/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
 | 
			
		||||
		/// <seealso cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2},out bool)"/>
 | 
			
		||||
		[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
 | 
			
		||||
		public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
 | 
			
		||||
			[CanBeNull] IDictionary<T, T2> second)
 | 
			
		||||
		{
 | 
			
		||||
			return MergeDictionaries(first, second, out bool _);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="first">The first dictionary to merge</param>
 | 
			
		||||
		/// <param name="second">The second dictionary to merge</param>
 | 
			
		||||
		/// <param name="hasChanged">
 | 
			
		||||
		/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
 | 
			
		||||
		/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
 | 
			
		||||
		/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
 | 
			
		||||
		[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
 | 
			
		||||
		public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
 | 
			
		||||
			[CanBeNull] IDictionary<T, T2> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// The only difference in this function compared to
 | 
			
		||||
		/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2}, out bool)"/>
 | 
			
		||||
		/// is the way <paramref name="hasChanged"/> is calculated and the order of the arguments.
 | 
			
		||||
		/// <code>
 | 
			
		||||
		/// MergeDictionaries(first, second);
 | 
			
		||||
		/// </code>
 | 
			
		||||
		/// will do the same thing as
 | 
			
		||||
		/// <code>
 | 
			
		||||
		/// CompleteDictionaries(second, first, out bool _);
 | 
			
		||||
		/// </code>
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="first">The first dictionary to merge</param>
 | 
			
		||||
		/// <param name="second">The second dictionary to merge</param>
 | 
			
		||||
		/// <param name="hasChanged">
 | 
			
		||||
@ -138,8 +45,8 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// set to those of <paramref name="first"/>.
 | 
			
		||||
		/// </returns>
 | 
			
		||||
		[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
 | 
			
		||||
		public static IDictionary<T, T2> CompleteDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
 | 
			
		||||
			[CanBeNull] IDictionary<T, T2> second,
 | 
			
		||||
		public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(IDictionary<T, T2>? first,
 | 
			
		||||
			IDictionary<T, T2>? 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
 | 
			
		||||
		/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="first">The object to assign</param>
 | 
			
		||||
		/// <param name="second">The object containing new values</param>
 | 
			
		||||
		/// <typeparam name="T">Fields of T will be used</typeparam>
 | 
			
		||||
		/// <returns><paramref name="first"/></returns>
 | 
			
		||||
		public static T Assign<T>(T first, T second)
 | 
			
		||||
		{
 | 
			
		||||
			Type type = typeof(T);
 | 
			
		||||
			IEnumerable<PropertyInfo> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 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
 | 
			
		||||
		/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2})"/>
 | 
			
		||||
		/// for more details).
 | 
			
		||||
		/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// This does the opposite of <see cref="Merge{T}"/>.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <example>
 | 
			
		||||
		/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
 | 
			
		||||
		/// </example>
 | 
			
		||||
@ -208,19 +83,16 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <typeparam name="T">Fields of T will be completed</typeparam>
 | 
			
		||||
		/// <returns><paramref name="first"/></returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">If first is null</exception>
 | 
			
		||||
		public static T Complete<T>([NotNull] T first,
 | 
			
		||||
			[CanBeNull] T second,
 | 
			
		||||
			[InstantHandle] Func<PropertyInfo, bool> where = null)
 | 
			
		||||
		public static T Complete<T>(T first,
 | 
			
		||||
			T? second,
 | 
			
		||||
			[InstantHandle] Func<PropertyInfo, bool>? where = null)
 | 
			
		||||
		{
 | 
			
		||||
			if (first == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(first));
 | 
			
		||||
			if (second == null)
 | 
			
		||||
				return first;
 | 
			
		||||
 | 
			
		||||
			Type type = typeof(T);
 | 
			
		||||
			IEnumerable<PropertyInfo> 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<DefaultValueAttribute>()?.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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// This will set missing values of <paramref name="first"/> to the corresponding values of <paramref name="second"/>.
 | 
			
		||||
		/// Enumerable will be merged (concatenated) and Dictionaries too.
 | 
			
		||||
		/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <example>
 | 
			
		||||
		/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}
 | 
			
		||||
		/// </example>
 | 
			
		||||
		/// <param name="first">
 | 
			
		||||
		/// The object to complete
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="second">
 | 
			
		||||
		/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="where">
 | 
			
		||||
		/// Filter fields that will be merged
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <typeparam name="T">Fields of T will be merged</typeparam>
 | 
			
		||||
		/// <returns><paramref name="first"/></returns>
 | 
			
		||||
		[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
 | 
			
		||||
		public static T Merge<T>([CanBeNull] T first,
 | 
			
		||||
			[CanBeNull] T second,
 | 
			
		||||
			[InstantHandle] Func<PropertyInfo, bool> where = null)
 | 
			
		||||
		{
 | 
			
		||||
			if (first == null)
 | 
			
		||||
				return second;
 | 
			
		||||
			if (second == null)
 | 
			
		||||
				return first;
 | 
			
		||||
 | 
			
		||||
			Type type = typeof(T);
 | 
			
		||||
			IEnumerable<PropertyInfo> 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<object>(
 | 
			
		||||
						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<IResource, IResource, bool> equalityComparer = enumerableType.IsAssignableTo(typeof(IResource))
 | 
			
		||||
						? (x, y) => x.Slug == y.Slug
 | 
			
		||||
						: null;
 | 
			
		||||
					property.SetValue(first, Utility.RunGenericMethod<object>(
 | 
			
		||||
						typeof(Merger),
 | 
			
		||||
						nameof(MergeLists),
 | 
			
		||||
						enumerableType,
 | 
			
		||||
						oldValue, newValue, equalityComparer));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (first is IOnMerge merge)
 | 
			
		||||
				merge.OnMerge(second);
 | 
			
		||||
			return first;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Set every fields of <paramref name="obj"/> to the default value.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="obj">The object to nullify</param>
 | 
			
		||||
		/// <typeparam name="T">Fields of T will be nullified</typeparam>
 | 
			
		||||
		/// <returns><paramref name="obj"/></returns>
 | 
			
		||||
		public static T Nullify<T>(T obj)
 | 
			
		||||
		{
 | 
			
		||||
			Type type = typeof(T);
 | 
			
		||||
			foreach (PropertyInfo property in type.GetProperties())
 | 
			
		||||
			{
 | 
			
		||||
				if (!property.CanWrite || property.GetCustomAttribute<ComputedAttribute>() != null)
 | 
			
		||||
					continue;
 | 
			
		||||
				property.SetValue(obj, property.PropertyType.GetClrDefault());
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return obj;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Map the result of a task to another result.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="task">The task to map.</param>
 | 
			
		||||
		/// <param name="map">The mapper method, it take the task's result as a parameter and should return the new result.</param>
 | 
			
		||||
		/// <typeparam name="T">The type of returns of the given task</typeparam>
 | 
			
		||||
		/// <typeparam name="TResult">The resulting task after the mapping method</typeparam>
 | 
			
		||||
		/// <returns>A task wrapping the initial task and mapping the initial result.</returns>
 | 
			
		||||
		/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
 | 
			
		||||
		public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
 | 
			
		||||
		{
 | 
			
		||||
			return task.ContinueWith(x =>
 | 
			
		||||
			{
 | 
			
		||||
				if (x.IsFaulted)
 | 
			
		||||
					x.Exception!.InnerException!.ReThrow();
 | 
			
		||||
				if (x.IsCanceled)
 | 
			
		||||
					throw new TaskCanceledException();
 | 
			
		||||
				return map(x.Result);
 | 
			
		||||
			}, TaskContinuationOptions.ExecuteSynchronously);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A method to return the a default value from a task if the initial task is null.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="value">The initial task</param>
 | 
			
		||||
		/// <typeparam name="T">The type that the task will return</typeparam>
 | 
			
		||||
		/// <returns>A non-null task.</returns>
 | 
			
		||||
		[NotNull]
 | 
			
		||||
		public static Task<T> DefaultIfNull<T>([CanBeNull] Task<T> value)
 | 
			
		||||
		{
 | 
			
		||||
			return value ?? Task.FromResult<T>(default);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -41,8 +41,6 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// <returns>True if the expression is a member, false otherwise</returns>
 | 
			
		||||
		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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get the value of a member (property or field)
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="member">The member value</param>
 | 
			
		||||
		/// <param name="obj">The owner of this member</param>
 | 
			
		||||
		/// <returns>The value boxed as an object</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">if <paramref name="member"/> or <paramref name="obj"/> is null.</exception>
 | 
			
		||||
		/// <exception cref="ArgumentException">The member is not a field or a property.</exception>
 | 
			
		||||
		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}).")
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Slugify a string (Replace spaces by -, Uniformize accents)
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="str">The string to slugify</param>
 | 
			
		||||
		/// <returns>The slug version of the given string</returns>
 | 
			
		||||
		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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get the default value of a type.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="type">The type to get the default value</param>
 | 
			
		||||
		/// <returns>The default value of the given type.</returns>
 | 
			
		||||
		public static object GetClrDefault(this Type type)
 | 
			
		||||
		{
 | 
			
		||||
			return type.IsValueType
 | 
			
		||||
				? Activator.CreateInstance(type)
 | 
			
		||||
				: null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="type">The starting type</param>
 | 
			
		||||
		/// <param name="self">The starting type</param>
 | 
			
		||||
		/// <returns>A list of types</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException"><paramref name="type"/> can't be null</exception>
 | 
			
		||||
		public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
 | 
			
		||||
		public static IEnumerable<Type> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Check if <paramref name="obj"/> inherit from a generic type <paramref name="genericType"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="obj">Does this object's type is a <paramref name="genericType"/></param>
 | 
			
		||||
		/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
 | 
			
		||||
		/// <returns>True if obj inherit from genericType. False otherwise</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
 | 
			
		||||
		public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType)
 | 
			
		||||
		{
 | 
			
		||||
			if (obj == null)
 | 
			
		||||
				throw new ArgumentNullException(nameof(obj));
 | 
			
		||||
			return IsOfGenericType(obj.GetType(), genericType);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Check if <paramref name="type"/> inherit from a generic type <paramref name="genericType"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="type">The type to check</param>
 | 
			
		||||
		/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
 | 
			
		||||
		/// <returns>True if obj inherit from genericType. False otherwise</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
 | 
			
		||||
		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
 | 
			
		||||
		/// <param name="type">The type to check</param>
 | 
			
		||||
		/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
 | 
			
		||||
		/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
 | 
			
		||||
		/// <exception cref="ArgumentNullException"><paramref name="type"/> and <paramref name="genericType"/> can't be null</exception>
 | 
			
		||||
		/// <exception cref="ArgumentException"><paramref name="genericType"/> must be a generic type</exception>
 | 
			
		||||
		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
 | 
			
		||||
		/// <exception cref="ArgumentException">No method match the given constraints.</exception>
 | 
			
		||||
		/// <returns>The method handle of the matching method.</returns>
 | 
			
		||||
		[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 <see cref="Type"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <example>
 | 
			
		||||
		/// To run <see cref="Merger.MergeLists{T}"/> 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:
 | 
			
		||||
		/// <code lang="C#">
 | 
			
		||||
		/// Utility.RunGenericMethod<object>(
 | 
			
		||||
@ -294,12 +220,11 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// </typeparam>
 | 
			
		||||
		/// <exception cref="ArgumentException">No method match the given constraints.</exception>
 | 
			
		||||
		/// <returns>The return of the method you wanted to run.</returns>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
 | 
			
		||||
		public static T RunGenericMethod<T>(
 | 
			
		||||
			[NotNull] Type owner,
 | 
			
		||||
			[NotNull] string methodName,
 | 
			
		||||
			[NotNull] Type type,
 | 
			
		||||
		public static T? RunGenericMethod<T>(
 | 
			
		||||
			Type owner,
 | 
			
		||||
			string methodName,
 | 
			
		||||
			Type type,
 | 
			
		||||
			params object[] args)
 | 
			
		||||
		{
 | 
			
		||||
			return RunGenericMethod<T>(owner, methodName, new[] { type }, args);
 | 
			
		||||
@ -311,7 +236,7 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <example>
 | 
			
		||||
		/// To run <see cref="Merger.MergeLists{T}"/> 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:
 | 
			
		||||
		/// <code>
 | 
			
		||||
		/// Utility.RunGenericMethod<object>(
 | 
			
		||||
@ -330,102 +255,17 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// </typeparam>
 | 
			
		||||
		/// <exception cref="ArgumentException">No method match the given constraints.</exception>
 | 
			
		||||
		/// <returns>The return of the method you wanted to run.</returns>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
 | 
			
		||||
		[PublicAPI]
 | 
			
		||||
		public static T RunGenericMethod<T>(
 | 
			
		||||
			[NotNull] Type owner,
 | 
			
		||||
			[NotNull] string methodName,
 | 
			
		||||
			[NotNull] Type[] types,
 | 
			
		||||
			params object[] args)
 | 
			
		||||
		public static T? RunGenericMethod<T>(
 | 
			
		||||
			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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Run a generic method for a runtime <see cref="Type"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <example>
 | 
			
		||||
		/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
 | 
			
		||||
		/// you could do:
 | 
			
		||||
		/// <code>
 | 
			
		||||
		/// Utility.RunGenericMethod<object>(
 | 
			
		||||
		///     typeof(Utility),
 | 
			
		||||
		///     nameof(MergeLists),
 | 
			
		||||
		///     enumerableType,
 | 
			
		||||
		///     oldValue, newValue, equalityComparer)
 | 
			
		||||
		/// </code>
 | 
			
		||||
		/// </example>
 | 
			
		||||
		/// <param name="instance">The <c>this</c> of the method to run.</param>
 | 
			
		||||
		/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
 | 
			
		||||
		/// <param name="type">The generic type to run the method with.</param>
 | 
			
		||||
		/// <param name="args">The list of arguments of the method</param>
 | 
			
		||||
		/// <typeparam name="T">
 | 
			
		||||
		/// The return type of the method. You can put <see cref="object"/> for an unknown one.
 | 
			
		||||
		/// </typeparam>
 | 
			
		||||
		/// <exception cref="ArgumentException">No method match the given constraints.</exception>
 | 
			
		||||
		/// <returns>The return of the method you wanted to run.</returns>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
 | 
			
		||||
		public static T RunGenericMethod<T>(
 | 
			
		||||
			[NotNull] object instance,
 | 
			
		||||
			[NotNull] string methodName,
 | 
			
		||||
			[NotNull] Type type,
 | 
			
		||||
			params object[] args)
 | 
			
		||||
		{
 | 
			
		||||
			return RunGenericMethod<T>(instance, methodName, new[] { type }, args);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Run a generic method for a multiple runtime <see cref="Type"/>.
 | 
			
		||||
		/// If your generic method only needs one type, see
 | 
			
		||||
		/// <see cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <example>
 | 
			
		||||
		/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
 | 
			
		||||
		/// you could do:
 | 
			
		||||
		/// <code>
 | 
			
		||||
		/// Utility.RunGenericMethod<object>(
 | 
			
		||||
		///     typeof(Utility),
 | 
			
		||||
		///     nameof(MergeLists),
 | 
			
		||||
		///     enumerableType,
 | 
			
		||||
		///     oldValue, newValue, equalityComparer)
 | 
			
		||||
		/// </code>
 | 
			
		||||
		/// </example>
 | 
			
		||||
		/// <param name="instance">The <c>this</c> of the method to run.</param>
 | 
			
		||||
		/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
 | 
			
		||||
		/// <param name="types">The list of generic types to run the method with.</param>
 | 
			
		||||
		/// <param name="args">The list of arguments of the method</param>
 | 
			
		||||
		/// <typeparam name="T">
 | 
			
		||||
		/// The return type of the method. You can put <see cref="object"/> for an unknown one.
 | 
			
		||||
		/// </typeparam>
 | 
			
		||||
		/// <exception cref="ArgumentException">No method match the given constraints.</exception>
 | 
			
		||||
		/// <returns>The return of the method you wanted to run.</returns>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
 | 
			
		||||
		/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
 | 
			
		||||
		public static T RunGenericMethod<T>(
 | 
			
		||||
			[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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -446,25 +286,11 @@ namespace Kyoo.Utils
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="ex">The exception to rethrow.</param>
 | 
			
		||||
		[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();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get a friendly type name (supporting generics)
 | 
			
		||||
		/// For example a list of string will be displayed as List<string> and not as List`1.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="type">The type to use</param>
 | 
			
		||||
		/// <returns>The friendly name of the type</returns>
 | 
			
		||||
		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}>";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ namespace Kyoo.Authentication
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@ namespace Kyoo.Authentication
 | 
			
		||||
				: string.Empty;
 | 
			
		||||
			List<Claim> 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")
 | 
			
		||||
				},
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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<string, string>()
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -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<ActionResult<User>> PatchMe(User user)
 | 
			
		||||
		public async Task<ActionResult<User>> 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)
 | 
			
		||||
			{
 | 
			
		||||
 | 
			
		||||
@ -39,15 +39,15 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IBaseRepository[] _repositories;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public ILibraryRepository LibraryRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public ILibraryItemRepository LibraryItemRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public ICollectionRepository CollectionRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public IMovieRepository MovieRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public IShowRepository ShowRepository { get; }
 | 
			
		||||
 | 
			
		||||
@ -63,12 +63,6 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public IStudioRepository StudioRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public IGenreRepository GenreRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public IProviderRepository ProviderRepository { get; }
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public IUserRepository UserRepository { get; }
 | 
			
		||||
 | 
			
		||||
@ -80,16 +74,14 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		public LibraryManager(IEnumerable<IBaseRepository> repositories)
 | 
			
		||||
		{
 | 
			
		||||
			_repositories = repositories.ToArray();
 | 
			
		||||
			LibraryRepository = GetRepository<Library>() as ILibraryRepository;
 | 
			
		||||
			LibraryItemRepository = GetRepository<LibraryItem>() as ILibraryItemRepository;
 | 
			
		||||
			LibraryItemRepository = GetRepository<ILibraryItem>() as ILibraryItemRepository;
 | 
			
		||||
			CollectionRepository = GetRepository<Collection>() as ICollectionRepository;
 | 
			
		||||
			MovieRepository = GetRepository<Movie>() as IMovieRepository;
 | 
			
		||||
			ShowRepository = GetRepository<Show>() as IShowRepository;
 | 
			
		||||
			SeasonRepository = GetRepository<Season>() as ISeasonRepository;
 | 
			
		||||
			EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository;
 | 
			
		||||
			PeopleRepository = GetRepository<People>() as IPeopleRepository;
 | 
			
		||||
			StudioRepository = GetRepository<Studio>() as IStudioRepository;
 | 
			
		||||
			GenreRepository = GetRepository<Genre>() as IGenreRepository;
 | 
			
		||||
			ProviderRepository = GetRepository<Provider>() as IProviderRepository;
 | 
			
		||||
			UserRepository = GetRepository<User>() 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<Collection>(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<Show>(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<Season>(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<Episode>(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<Studio>(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<People>(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}.")
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
		{
 | 
			
		||||
			return LibraryItemRepository.GetFromLibrary(id, where, sort, limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
		{
 | 
			
		||||
			return LibraryItemRepository.GetFromLibrary(slug, where, sort, limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
 | 
			
		||||
			Expression<Func<PeopleRole, bool>> where = null,
 | 
			
		||||
@ -455,20 +396,6 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
			return PeopleRepository.GetFromPeople(slug, where, sort, limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task AddShowLink(int showID, int? libraryID, int? collectionID)
 | 
			
		||||
		{
 | 
			
		||||
			return ShowRepository.AddShowLink(showID, libraryID, collectionID);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null,
 | 
			
		||||
			Sort<T> sort = default,
 | 
			
		||||
@ -507,10 +434,17 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<T> Edit<T>(T item, bool resetOld)
 | 
			
		||||
		public Task<T> Edit<T>(T item)
 | 
			
		||||
			where T : class, IResource
 | 
			
		||||
		{
 | 
			
		||||
			return GetRepository<T>().Edit(item, resetOld);
 | 
			
		||||
			return GetRepository<T>().Edit(item);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<T> Patch<T>(int id, Func<T, Task<bool>> patch)
 | 
			
		||||
			where T : class, IResource
 | 
			
		||||
		{
 | 
			
		||||
			return GetRepository<T>().Patch(id, patch);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
 | 
			
		||||
@ -37,11 +37,6 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A provider repository to handle externalID creation and deletion
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IProviderRepository _providers;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<Collection> DefaultSort => new Sort<Collection>.By(nameof(Collection.Name));
 | 
			
		||||
 | 
			
		||||
@ -49,22 +44,22 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// Create a new <see cref="CollectionRepository"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle to use</param>
 | 
			
		||||
		/// /// <param name="providers">A provider repository</param>
 | 
			
		||||
		public CollectionRepository(DatabaseContext database, IProviderRepository providers)
 | 
			
		||||
			: base(database)
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		public CollectionRepository(DatabaseContext database, IThumbnailsManager thumbs)
 | 
			
		||||
			: base(database, thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
			_providers = providers;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<Collection>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
				_database.Collections
 | 
			
		||||
					.Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%"))
 | 
			
		||||
					.Take(20)
 | 
			
		||||
				).ToListAsync();
 | 
			
		||||
				).ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf).ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -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<Provider>(id.Provider.Slug)
 | 
			
		||||
						?? await _providers.CreateIfNotExists(id.Provider);
 | 
			
		||||
					id.ProviderID = id.Provider.ID;
 | 
			
		||||
				}
 | 
			
		||||
				_database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
 | 
			
		||||
@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A provider repository to handle externalID creation and deletion
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IProviderRepository _providers;
 | 
			
		||||
 | 
			
		||||
		private readonly IShowRepository _shows;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -59,20 +54,19 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle to use.</param>
 | 
			
		||||
		/// <param name="shows">A show repository</param>
 | 
			
		||||
		/// <param name="providers">A provider repository</param>
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		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<Episode> episodes = _database.Episodes.AsTracking().Where(x => x.ShowID == show.ID).ToList();
 | 
			
		||||
				List<Episode> 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
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<Episode> 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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -119,15 +113,15 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<Episode> 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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
 | 
			
		||||
		{
 | 
			
		||||
			return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
 | 
			
		||||
				&& x.AbsoluteNumber == absoluteNumber);
 | 
			
		||||
				&& x.AbsoluteNumber == absoluteNumber).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -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<Episode>(x => x.Title, $"%{query}%"))
 | 
			
		||||
					.Where(_database.Like<Episode>(x => x.Name, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
			foreach (Episode ep in ret)
 | 
			
		||||
			{
 | 
			
		||||
				ep.Show.Episodes = null;
 | 
			
		||||
				SetBackingImage(ep);
 | 
			
		||||
			}
 | 
			
		||||
			return ret;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<Episode> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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<Provider>(id.Provider.Slug)
 | 
			
		||||
						?? await _providers.CreateIfNotExists(id.Provider);
 | 
			
		||||
					id.ProviderID = id.Provider.ID;
 | 
			
		||||
				}
 | 
			
		||||
				_database.MetadataIds<Episode>().AttachRange(resource.ExternalIDs);
 | 
			
		||||
				resource.ShowId = resource.Show.Id;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A local repository for genres.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class GenreRepository : LocalRepository<Genre>, IGenreRepository
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The database handle
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<Genre> DefaultSort => new Sort<Genre>.By(x => x.Slug);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="GenreRepository"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle</param>
 | 
			
		||||
		public GenreRepository(DatabaseContext database)
 | 
			
		||||
			: base(database)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<Genre>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
				_database.Genres
 | 
			
		||||
					.Where(_database.Like<Genre>(x => x.Name, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override async Task Validate(Genre resource)
 | 
			
		||||
		{
 | 
			
		||||
			resource.Slug ??= Utility.ToSlug(resource.Name);
 | 
			
		||||
			await base.Validate(resource);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<Genre> Create(Genre obj)
 | 
			
		||||
		{
 | 
			
		||||
			await base.Create(obj);
 | 
			
		||||
			_database.Entry(obj).State = EntityState.Added;
 | 
			
		||||
			await _database.SaveChangesAsync(() => Get(obj.Slug));
 | 
			
		||||
			OnResourceCreated(obj);
 | 
			
		||||
			return obj;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A local repository to handle library items.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class LibraryItemRepository : LocalRepository<LibraryItem>, ILibraryItemRepository
 | 
			
		||||
	public class LibraryItemRepository : LocalRepository<ILibraryItem>, ILibraryItemRepository
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The database handle
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A lazy loaded library repository to validate queries (check if a library does exist)
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly Lazy<ILibraryRepository> _libraries;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<LibraryItem> DefaultSort => new Sort<LibraryItem>.By(x => x.Title);
 | 
			
		||||
		protected override Sort<ILibraryItem> DefaultSort => new Sort<ILibraryItem>.By(x => x.Name);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="LibraryItemRepository"/>.
 | 
			
		||||
		/// Create a new <see cref="ILibraryItemRepository"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database instance</param>
 | 
			
		||||
		/// <param name="libraries">A lazy loaded library repository</param>
 | 
			
		||||
		public LibraryItemRepository(DatabaseContext database,
 | 
			
		||||
			Lazy<ILibraryRepository> libraries)
 | 
			
		||||
			: base(database)
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		public LibraryItemRepository(DatabaseContext database, IThumbnailsManager thumbs)
 | 
			
		||||
			: base(database, thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
			_libraries = libraries;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<LibraryItem> GetOrDefault(int id)
 | 
			
		||||
		public override async Task<ILibraryItem> GetOrDefault(int id)
 | 
			
		||||
		{
 | 
			
		||||
			return _database.LibraryItems.FirstOrDefaultAsync(x => x.ID == id);
 | 
			
		||||
			return await _database.LibraryItems.SingleOrDefaultAsync(x => x.Id == id).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<LibraryItem> GetOrDefault(string slug)
 | 
			
		||||
		public override async Task<ILibraryItem> GetOrDefault(string slug)
 | 
			
		||||
		{
 | 
			
		||||
			return _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug);
 | 
			
		||||
			return await _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
		public override async Task<ILibraryItem> GetOrDefault(Expression<Func<ILibraryItem, bool>> where, Sort<ILibraryItem> sortBy = default)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(_database.LibraryItems, sortBy).FirstOrDefaultAsync(where).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<ILibraryItem>> GetAll(Expression<Func<ILibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<ILibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
		{
 | 
			
		||||
			return ApplyFilters(_database.LibraryItems, where, sort, limit);
 | 
			
		||||
			return (await ApplyFilters(_database.LibraryItems, where, sort, limit))
 | 
			
		||||
				.Select(SetBackingImageSelf).ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null)
 | 
			
		||||
		public override Task<int> GetCount(Expression<Func<ILibraryItem, bool>> where = null)
 | 
			
		||||
		{
 | 
			
		||||
			IQueryable<LibraryItem> query = _database.LibraryItems;
 | 
			
		||||
			IQueryable<ILibraryItem> query = _database.LibraryItems;
 | 
			
		||||
			if (where != null)
 | 
			
		||||
				query = query.Where(where);
 | 
			
		||||
			return query.CountAsync();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<LibraryItem>> Search(string query)
 | 
			
		||||
		public override async Task<ICollection<ILibraryItem>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
				_database.LibraryItems
 | 
			
		||||
					.Where(_database.Like<LibraryItem>(x => x.Title, $"%{query}%"))
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
					_database.LibraryItems
 | 
			
		||||
					.Where(_database.Like<LibraryItem>(x => x.Name, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
				.ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf)
 | 
			
		||||
				.ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<LibraryItem> Create(LibraryItem obj)
 | 
			
		||||
		public override Task<ILibraryItem> Create(ILibraryItem obj)
 | 
			
		||||
			=> throw new InvalidOperationException();
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj)
 | 
			
		||||
		public override Task<ILibraryItem> CreateIfNotExists(ILibraryItem obj)
 | 
			
		||||
			=> throw new InvalidOperationException();
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<LibraryItem> Edit(LibraryItem obj, bool resetOld)
 | 
			
		||||
		public override Task<ILibraryItem> Edit(ILibraryItem edited)
 | 
			
		||||
			=> throw new InvalidOperationException();
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task<ILibraryItem> Patch(int id, Func<ILibraryItem, Task<bool>> patch)
 | 
			
		||||
			=> throw new InvalidOperationException();
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -121,58 +127,7 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
			=> throw new InvalidOperationException();
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override Task Delete(LibraryItem obj)
 | 
			
		||||
		public override Task Delete(ILibraryItem obj)
 | 
			
		||||
			=> throw new InvalidOperationException();
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get a basic queryable for a library with the right mapping from shows and collections.
 | 
			
		||||
		/// Shows contained in a collection are excluded.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="selector">Only items that are part of a library that match this predicate will be returned.</param>
 | 
			
		||||
		/// <returns>A queryable containing items that are part of a library matching the selector.</returns>
 | 
			
		||||
		private IQueryable<LibraryItem> _LibraryRelatedQuery(Expression<Func<Library, bool>> 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));
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public async Task<ICollection<LibraryItem>> GetFromLibrary(int id,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<LibraryItem> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public async Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> where = null,
 | 
			
		||||
			Sort<LibraryItem> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<LibraryItem> 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A local repository to handle libraries.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class LibraryRepository : LocalRepository<Library>, ILibraryRepository
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The database handle
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A provider repository to handle externalID creation and deletion
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IProviderRepository _providers;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<Library> DefaultSort => new Sort<Library>.By(x => x.ID);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="LibraryRepository"/> instance.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle</param>
 | 
			
		||||
		/// <param name="providers">The provider repository</param>
 | 
			
		||||
		public LibraryRepository(DatabaseContext database, IProviderRepository providers)
 | 
			
		||||
			: base(database)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
			_providers = providers;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<Library>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
				_database.Libraries
 | 
			
		||||
					.Where(_database.Like<Library>(x => x.Name + " " + x.Slug, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<Library> Create(Library obj)
 | 
			
		||||
		{
 | 
			
		||||
			await base.Create(obj);
 | 
			
		||||
			_database.Entry(obj).State = EntityState.Added;
 | 
			
		||||
			await _database.SaveChangesAsync(() => Get(obj.Slug));
 | 
			
		||||
			OnResourceCreated(obj);
 | 
			
		||||
			return obj;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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<Provider>(x.Slug)
 | 
			
		||||
						?? await _providers.CreateIfNotExists(x)
 | 
			
		||||
					)
 | 
			
		||||
					.ToListAsync();
 | 
			
		||||
				_database.AttachRange(resource.Providers);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -45,6 +45,11 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		protected DbContext Database { get; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The thumbnail manager used to store images.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IThumbnailsManager _thumbs;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The default sort order that will be used for this resource's type.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -54,9 +59,11 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// Create a new base <see cref="LocalRepository{T}"/> with the given database handle.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">A database connection to load resources of type <typeparamref name="T"/></param>
 | 
			
		||||
		protected LocalRepository(DbContext database)
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		protected LocalRepository(DbContext database, IThumbnailsManager thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			Database = database;
 | 
			
		||||
			_thumbs = thumbs;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
@ -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<Expression, Expression, BinaryExpression> _GetComparisonExpression(
 | 
			
		||||
@ -144,7 +151,7 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// <param name="reference">The reference item (the AfterID query)</param>
 | 
			
		||||
		/// <param name="next">True if the following page should be returned, false for the previous.</param>
 | 
			
		||||
		/// <returns>An expression ready to be added to a Where close of a sorted query to handle the AfterID</returns>
 | 
			
		||||
		protected Expression<Func<T, bool>> KeysetPaginatate(
 | 
			
		||||
		protected Expression<Func<T, bool>> KeysetPaginate(
 | 
			
		||||
			Sort<T> 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<Sort<T>.By> _GetSortsBy(Sort<T> sort)
 | 
			
		||||
			IEnumerable<Sort<T>.By> GetSortsBy(Sort<T> sort)
 | 
			
		||||
			{
 | 
			
		||||
				return sort switch
 | 
			
		||||
				{
 | 
			
		||||
					Sort<T>.Default => _GetSortsBy(DefaultSort),
 | 
			
		||||
					Sort<T>.Default => GetSortsBy(DefaultSort),
 | 
			
		||||
					Sort<T>.By @sortBy => new[] { sortBy },
 | 
			
		||||
					Sort<T>.Conglomerate(var list) => list.SelectMany(_GetSortsBy),
 | 
			
		||||
					Sort<T>.Conglomerate(var list) => list.SelectMany(GetSortsBy),
 | 
			
		||||
					_ => Array.Empty<Sort<T>.By>(),
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Don't forget that every sorts must end with a ID sort (to differenciate equalities).
 | 
			
		||||
			Sort<T>.By id = new(x => x.ID);
 | 
			
		||||
			IEnumerable<Sort<T>.By> sorts = _GetSortsBy(sort).Append(id);
 | 
			
		||||
			// Don't forget that every sorts must end with a ID sort (to differentiate equalities).
 | 
			
		||||
			Sort<T>.By id = new(x => x.Id);
 | 
			
		||||
			IEnumerable<Sort<T>.By> sorts = GetSortsBy(sort).Append(id);
 | 
			
		||||
 | 
			
		||||
			BinaryExpression filter = null;
 | 
			
		||||
			List<Sort<T>.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<T>.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<Func<T, bool>>(filter, x);
 | 
			
		||||
			return Expression.Lambda<Func<T, bool>>(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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -234,7 +263,8 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// <returns>The tracked resource with the given ID</returns>
 | 
			
		||||
		protected virtual async Task<T> GetWithTracking(int id)
 | 
			
		||||
		{
 | 
			
		||||
			T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
 | 
			
		||||
			T ret = await Database.Set<T>().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
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public virtual Task<T> GetOrDefault(int id)
 | 
			
		||||
		{
 | 
			
		||||
			return Database.Set<T>().FirstOrDefaultAsync(x => x.ID == id);
 | 
			
		||||
			return Database.Set<T>().FirstOrDefaultAsync(x => x.Id == id).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public virtual Task<T> GetOrDefault(string slug)
 | 
			
		||||
		{
 | 
			
		||||
			return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug);
 | 
			
		||||
			return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public virtual Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default)
 | 
			
		||||
		{
 | 
			
		||||
			return Sort(Database.Set<T>(), sortBy).FirstOrDefaultAsync(where);
 | 
			
		||||
			return Sort(Database.Set<T>(), sortBy).FirstOrDefaultAsync(where).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public abstract Task<ICollection<T>> Search(string query);
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
 | 
			
		||||
		public virtual async Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
 | 
			
		||||
			Sort<T> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
		{
 | 
			
		||||
			return ApplyFilters(Database.Set<T>(), where, sort, limit);
 | 
			
		||||
			return (await ApplyFilters(Database.Set<T>(), where, sort, limit))
 | 
			
		||||
				.Select(SetBackingImageSelf).ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -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
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public virtual async Task<T> 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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public virtual async Task<T> Edit(T edited, bool resetOld)
 | 
			
		||||
		public virtual async Task<T> 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<LoadableRelationAttribute>() == 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
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public virtual async Task<T> Patch(int id, Func<T, Task<bool>> 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();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// An overridable method to edit relation of a resource.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -419,12 +476,15 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// The new version of <paramref name="resource"/>.
 | 
			
		||||
		/// This item will be saved on the database and replace <paramref name="resource"/>
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="resetOld">
 | 
			
		||||
		/// A boolean to indicate if all values of resource should be discarded or not.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										142
									
								
								back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs
									
									
									
									
									
										Normal file
									
								
							@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A local repository to handle shows
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class MovieRepository : LocalRepository<Movie>, IMovieRepository
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The database handle
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A studio repository to handle creation/validation of related studios.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IStudioRepository _studios;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A people repository to handle creation/validation of related people.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IPeopleRepository _people;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<Movie> DefaultSort => new Sort<Movie>.By(x => x.Name);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="MovieRepository"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle to use</param>
 | 
			
		||||
		/// <param name="studios">A studio repository</param>
 | 
			
		||||
		/// <param name="people">A people repository</param>
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		public MovieRepository(DatabaseContext database,
 | 
			
		||||
			IStudioRepository studios,
 | 
			
		||||
			IPeopleRepository people,
 | 
			
		||||
			IThumbnailsManager thumbs)
 | 
			
		||||
			: base(database, thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
			_studios = studios;
 | 
			
		||||
			_people = people;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<Movie>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			query = $"%{query}%";
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
				_database.Movies
 | 
			
		||||
					.Where(_database.Like<Movie>(x => x.Name + " " + x.Slug, query))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf)
 | 
			
		||||
				.ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<Movie> Create(Movie obj)
 | 
			
		||||
		{
 | 
			
		||||
			await base.Create(obj);
 | 
			
		||||
			_database.Entry(obj).State = EntityState.Added;
 | 
			
		||||
			await _database.SaveChangesAsync(() => Get(obj.Slug));
 | 
			
		||||
			OnResourceCreated(obj);
 | 
			
		||||
			return obj;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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<People>(role.People.Slug)
 | 
			
		||||
						?? await _people.CreateIfNotExists(role.People);
 | 
			
		||||
					role.PeopleID = role.People.Id;
 | 
			
		||||
					_database.Entry(role).State = EntityState.Added;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task Delete(Movie obj)
 | 
			
		||||
		{
 | 
			
		||||
			_database.Remove(obj);
 | 
			
		||||
			await _database.SaveChangesAsync();
 | 
			
		||||
			await base.Delete(obj);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A provider repository to handle externalID creation and deletion
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IProviderRepository _providers;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A lazy loaded show repository to validate requests from shows.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -56,27 +51,28 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// Create a new <see cref="PeopleRepository"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle</param>
 | 
			
		||||
		/// <param name="providers">A provider repository</param>
 | 
			
		||||
		/// <param name="shows">A lazy loaded show repository</param>
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		public PeopleRepository(DatabaseContext database,
 | 
			
		||||
			IProviderRepository providers,
 | 
			
		||||
			Lazy<IShowRepository> shows)
 | 
			
		||||
			: base(database)
 | 
			
		||||
			Lazy<IShowRepository> shows,
 | 
			
		||||
			IThumbnailsManager thumbs)
 | 
			
		||||
			: base(database, thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
			_providers = providers;
 | 
			
		||||
			_shows = shows;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<People>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
				_database.People
 | 
			
		||||
					.Where(_database.Like<People>(x => x.Name, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
				.ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf)
 | 
			
		||||
				.ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -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<Provider>(id.Provider.Slug)
 | 
			
		||||
						?? await _providers.CreateIfNotExists(id.Provider);
 | 
			
		||||
					id.ProviderID = id.Provider.ID;
 | 
			
		||||
				}
 | 
			
		||||
				_database.MetadataIds<People>().AttachRange(resource.ExternalIDs);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (resource.Roles != null)
 | 
			
		||||
			{
 | 
			
		||||
				foreach (PeopleRole role in resource.Roles)
 | 
			
		||||
				{
 | 
			
		||||
					role.Show = _database.LocalEntity<Show>(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;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	///     A local repository to handle providers.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class ProviderRepository : LocalRepository<Provider>, IProviderRepository
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		///     The database handle
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		///     Create a new <see cref="ProviderRepository" />.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle</param>
 | 
			
		||||
		public ProviderRepository(DatabaseContext database)
 | 
			
		||||
			: base(database)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<Provider> DefaultSort => new Sort<Provider>.By(x => x.Slug);
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<Provider>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
				_database.Providers
 | 
			
		||||
					.Where(_database.Like<Provider>(x => x.Name, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<Provider> Create(Provider obj)
 | 
			
		||||
		{
 | 
			
		||||
			await base.Create(obj);
 | 
			
		||||
			_database.Entry(obj).State = EntityState.Added;
 | 
			
		||||
			await _database.SaveChangesAsync(() => Get(obj.Slug));
 | 
			
		||||
			OnResourceCreated(obj);
 | 
			
		||||
			return obj;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public async Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
 | 
			
		||||
			Sort<MetadataID> sort = default,
 | 
			
		||||
			Pagination limit = default)
 | 
			
		||||
			where T : class, IMetadata
 | 
			
		||||
		{
 | 
			
		||||
			return await _database.MetadataIds<T>()
 | 
			
		||||
				.Include(y => y.Provider)
 | 
			
		||||
				.Where(where)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A provider repository to handle externalID creation and deletion
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IProviderRepository _providers;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		protected override Sort<Season> DefaultSort => new Sort<Season>.By(x => x.SeasonNumber);
 | 
			
		||||
 | 
			
		||||
@ -51,19 +47,18 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle that will be used</param>
 | 
			
		||||
		/// <param name="shows">A shows repository</param>
 | 
			
		||||
		/// <param name="providers">A provider repository</param>
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		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<Season> seasons = _database.Seasons.AsTracking().Where(x => x.ShowID == show.ID).ToList();
 | 
			
		||||
				List<Season> 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
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public Task<Season> 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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public Task<Season> GetOrDefault(string showSlug, int seasonNumber)
 | 
			
		||||
		{
 | 
			
		||||
			return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
 | 
			
		||||
				&& x.SeasonNumber == seasonNumber);
 | 
			
		||||
				&& x.SeasonNumber == seasonNumber).Then(SetBackingImage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public override async Task<ICollection<Season>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
				_database.Seasons
 | 
			
		||||
					.Where(_database.Like<Season>(x => x.Title, $"%{query}%"))
 | 
			
		||||
					.Where(_database.Like<Season>(x => x.Name, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
				.ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf)
 | 
			
		||||
				.ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public override async Task<Season> 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<Provider>(id.Provider.Slug)
 | 
			
		||||
						?? await _providers.CreateIfNotExists(id.Provider);
 | 
			
		||||
					id.ProviderID = id.Provider.ID;
 | 
			
		||||
				}
 | 
			
		||||
				_database.MetadataIds<Season>().AttachRange(resource.ExternalIDs);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		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);
 | 
			
		||||
 | 
			
		||||
@ -47,18 +47,8 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IPeopleRepository _people;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A genres repository to handle creation/validation of related genres.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IGenreRepository _genres;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A provider repository to handle externalID creation and deletion
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IProviderRepository _providers;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<Show> DefaultSort => new Sort<Show>.By(x => x.Title);
 | 
			
		||||
		protected override Sort<Show> DefaultSort => new Sort<Show>.By(x => x.Name);
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="ShowRepository"/>.
 | 
			
		||||
@ -66,32 +56,30 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// <param name="database">The database handle to use</param>
 | 
			
		||||
		/// <param name="studios">A studio repository</param>
 | 
			
		||||
		/// <param name="people">A people repository</param>
 | 
			
		||||
		/// <param name="genres">A genres repository</param>
 | 
			
		||||
		/// <param name="providers">A provider repository</param>
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<Show>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			query = $"%{query}%";
 | 
			
		||||
			return await Sort(
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
				_database.Shows
 | 
			
		||||
					.Where(_database.Like<Show>(x => x.Title + " " + x.Slug, query))
 | 
			
		||||
					.Where(_database.Like<Show>(x => x.Name + " " + x.Slug, query))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
				.ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf)
 | 
			
		||||
				.ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -107,32 +95,13 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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<Provider>(id.Provider.Slug)
 | 
			
		||||
						?? await _providers.CreateIfNotExists(id.Provider);
 | 
			
		||||
					id.ProviderID = id.Provider.ID;
 | 
			
		||||
				}
 | 
			
		||||
				_database.MetadataIds<Show>().AttachRange(resource.ExternalIDs);
 | 
			
		||||
				resource.StudioId = resource.Studio.Id;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (resource.People != null)
 | 
			
		||||
@ -141,70 +110,34 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
				{
 | 
			
		||||
					role.People = _database.LocalEntity<People>(role.People.Slug)
 | 
			
		||||
						?? await _people.CreateIfNotExists(role.People);
 | 
			
		||||
					role.PeopleID = role.People.ID;
 | 
			
		||||
					role.PeopleID = role.People.Id;
 | 
			
		||||
					_database.Entry(role).State = EntityState.Added;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public async Task AddShowLink(int showID, int? libraryID, int? collectionID)
 | 
			
		||||
		{
 | 
			
		||||
			if (collectionID != null)
 | 
			
		||||
			{
 | 
			
		||||
				await _database.AddLinks<Collection, Show>(collectionID.Value, showID);
 | 
			
		||||
				await _database.SaveIfNoDuplicates();
 | 
			
		||||
 | 
			
		||||
				if (libraryID != null)
 | 
			
		||||
				{
 | 
			
		||||
					await _database.AddLinks<Library, Collection>(libraryID.Value, collectionID.Value);
 | 
			
		||||
					await _database.SaveIfNoDuplicates();
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (libraryID != null)
 | 
			
		||||
			{
 | 
			
		||||
				await _database.AddLinks<Library, Show>(libraryID.Value, showID);
 | 
			
		||||
				await _database.SaveIfNoDuplicates();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public Task<string> GetSlug(int showID)
 | 
			
		||||
		{
 | 
			
		||||
			return _database.Shows.Where(x => x.ID == showID)
 | 
			
		||||
			return _database.Shows.Where(x => x.Id == showID)
 | 
			
		||||
				.Select(x => x.Slug)
 | 
			
		||||
				.FirstOrDefaultAsync();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
@ -38,11 +37,6 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly DatabaseContext _database;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A provider repository to handle externalID creation and deletion
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly IProviderRepository _providers;
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override Sort<Studio> DefaultSort => new Sort<Studio>.By(x => x.Name);
 | 
			
		||||
 | 
			
		||||
@ -50,23 +44,24 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// Create a new <see cref="StudioRepository"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle</param>
 | 
			
		||||
		/// <param name="providers">A provider repository</param>
 | 
			
		||||
		public StudioRepository(DatabaseContext database, IProviderRepository providers)
 | 
			
		||||
			: base(database)
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		public StudioRepository(DatabaseContext database, IThumbnailsManager thumbs)
 | 
			
		||||
			: base(database, thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
			_providers = providers;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<Studio>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
				_database.Studios
 | 
			
		||||
					.Where(_database.Like<Studio>(x => x.Name, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
				.ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf)
 | 
			
		||||
				.ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -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<Provider>(id.Provider.Slug)
 | 
			
		||||
						?? await _providers.CreateIfNotExists(id.Provider);
 | 
			
		||||
					id.ProviderID = id.Provider.ID;
 | 
			
		||||
				}
 | 
			
		||||
				_database.MetadataIds<Studio>().AttachRange(resource.ExternalIDs);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
@ -44,8 +43,9 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// Create a new <see cref="UserRepository"/>
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="database">The database handle to use</param>
 | 
			
		||||
		public UserRepository(DatabaseContext database)
 | 
			
		||||
			: base(database)
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
			
		||||
		public UserRepository(DatabaseContext database, IThumbnailsManager thumbs)
 | 
			
		||||
			: base(database, thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			_database = database;
 | 
			
		||||
		}
 | 
			
		||||
@ -53,12 +53,14 @@ namespace Kyoo.Core.Controllers
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public override async Task<ICollection<User>> Search(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return await Sort(
 | 
			
		||||
			return (await Sort(
 | 
			
		||||
				_database.Users
 | 
			
		||||
					.Where(_database.Like<User>(x => x.Username, $"%{query}%"))
 | 
			
		||||
				)
 | 
			
		||||
				.Take(20)
 | 
			
		||||
				.ToListAsync();
 | 
			
		||||
				.ToListAsync())
 | 
			
		||||
				.Select(SetBackingImageSelf)
 | 
			
		||||
				.ToList();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
@ -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
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// An helper function to download an image.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="url">The distant url of the image</param>
 | 
			
		||||
		/// <param name="localPath">The local path of the image</param>
 | 
			
		||||
		/// <param name="what">What is currently downloaded (used for errors)</param>
 | 
			
		||||
		/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
 | 
			
		||||
		private async Task<bool> _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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public async Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
 | 
			
		||||
		public async Task DownloadImages<T>(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}");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Retrieve the local path of an image of the given item <b>without an extension</b>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="item">The item to retrieve the poster from.</param>
 | 
			
		||||
		/// <param name="imageId">The ID of the image. See <see cref="Images"/> for values.</param>
 | 
			
		||||
		/// <typeparam name="T">The type of the item</typeparam>
 | 
			
		||||
		/// <returns>The path of the image for the given resource, <b>even if it does not exists</b></returns>
 | 
			
		||||
		private static string _GetPrivateImagePath<T>(T item, int imageId)
 | 
			
		||||
		private static string _GetBaseImagePath<T>(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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		public string? GetImagePath<T>(T item, int imageId)
 | 
			
		||||
		public string GetImagePath<T>(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";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
 | 
			
		||||
			builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
 | 
			
		||||
 | 
			
		||||
			builder.RegisterRepository<ILibraryRepository, LibraryRepository>();
 | 
			
		||||
			builder.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>();
 | 
			
		||||
			builder.RegisterRepository<ICollectionRepository, CollectionRepository>();
 | 
			
		||||
			builder.RegisterRepository<IMovieRepository, MovieRepository>();
 | 
			
		||||
			builder.RegisterRepository<IShowRepository, ShowRepository>();
 | 
			
		||||
			builder.RegisterRepository<ISeasonRepository, SeasonRepository>();
 | 
			
		||||
			builder.RegisterRepository<IEpisodeRepository, EpisodeRepository>();
 | 
			
		||||
			builder.RegisterRepository<IPeopleRepository, PeopleRepository>();
 | 
			
		||||
			builder.RegisterRepository<IStudioRepository, StudioRepository>();
 | 
			
		||||
			builder.RegisterRepository<IGenreRepository, GenreRepository>();
 | 
			
		||||
			builder.RegisterRepository<IProviderRepository, ProviderRepository>();
 | 
			
		||||
			builder.RegisterRepository<IUserRepository, UserRepository>();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -74,6 +73,7 @@ namespace Kyoo.Core
 | 
			
		||||
				.AddNewtonsoftJson(x =>
 | 
			
		||||
				{
 | 
			
		||||
					x.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
 | 
			
		||||
					x.SerializerSettings.Converters.Add(new StringEnumConverter());
 | 
			
		||||
				})
 | 
			
		||||
				.AddDataAnnotations()
 | 
			
		||||
				.AddControllersAsServices()
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,12 @@
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="AspNetCore.Proxy" Version="4.4.0" />
 | 
			
		||||
		<PackageReference Include="Blurhash.SkiaSharp" Version="2.0.0" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.9" />
 | 
			
		||||
		<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
 | 
			
		||||
		<PackageReference Include="SkiaSharp" Version="2.88.3" />
 | 
			
		||||
		<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.3" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 | 
			
		||||
@ -16,12 +16,14 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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<ActionResult<T>> 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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -185,14 +187,15 @@ namespace Kyoo.Core.Api
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
		public async Task<ActionResult<T>> Patch([FromBody] T resource)
 | 
			
		||||
		public async Task<ActionResult<T>> 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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get the content type of a file using it's extension.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="path">The path of the file</param>
 | 
			
		||||
		/// <exception cref="NotImplementedException">The extension of the file is not known.</exception>
 | 
			
		||||
		/// <returns>The content type of the file</returns>
 | 
			
		||||
		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}");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get image
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// Get an image for the specified item.
 | 
			
		||||
		/// List of commonly available images:<br/>
 | 
			
		||||
		///  - Poster: Image 0, also available at /poster<br/>
 | 
			
		||||
		///  - Thumbnail: Image 1, also available at /thumbnail<br/>
 | 
			
		||||
		///  - Logo: Image 3, also available at /logo<br/>
 | 
			
		||||
		/// <br/>
 | 
			
		||||
		/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
 | 
			
		||||
		/// <param name="image">The number of the image to retrieve.</param>
 | 
			
		||||
		/// <returns>The image asked.</returns>
 | 
			
		||||
		/// <response code="404">No item exist with the specific identifier or the image does not exists on kyoo.</response>
 | 
			
		||||
		private async Task<IActionResult> _GetImage(Identifier identifier, int image)
 | 
			
		||||
		private async Task<IActionResult> _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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -110,17 +78,18 @@ namespace Kyoo.Core.Api
 | 
			
		||||
		/// Get the poster for the specified item.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
 | 
			
		||||
		/// <param name="quality">The quality of the image to retrieve.</param>
 | 
			
		||||
		/// <returns>The image asked.</returns>
 | 
			
		||||
		/// <response code="404">
 | 
			
		||||
		/// No item exist with the specific identifier or the image does not exists on kyoo.
 | 
			
		||||
		/// </response>
 | 
			
		||||
		[HttpGet("{identifier:id}/poster", Order = AlternativeRoute)]
 | 
			
		||||
		[HttpGet("{identifier:id}/poster")]
 | 
			
		||||
		[PartialPermission(Kind.Read)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
		public Task<IActionResult> GetPoster(Identifier identifier)
 | 
			
		||||
		public Task<IActionResult> GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
 | 
			
		||||
		{
 | 
			
		||||
			return _GetImage(identifier, Images.Poster);
 | 
			
		||||
			return _GetImage(identifier, "poster", quality);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -130,17 +99,18 @@ namespace Kyoo.Core.Api
 | 
			
		||||
		/// Get the logo for the specified item.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
 | 
			
		||||
		/// <param name="quality">The quality of the image to retrieve.</param>
 | 
			
		||||
		/// <returns>The image asked.</returns>
 | 
			
		||||
		/// <response code="404">
 | 
			
		||||
		/// No item exist with the specific identifier or the image does not exists on kyoo.
 | 
			
		||||
		/// </response>
 | 
			
		||||
		[HttpGet("{identifier:id}/logo", Order = AlternativeRoute)]
 | 
			
		||||
		[HttpGet("{identifier:id}/logo")]
 | 
			
		||||
		[PartialPermission(Kind.Read)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
		public Task<IActionResult> GetLogo(Identifier identifier)
 | 
			
		||||
		public Task<IActionResult> GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality)
 | 
			
		||||
		{
 | 
			
		||||
			return _GetImage(identifier, Images.Logo);
 | 
			
		||||
			return _GetImage(identifier, "logo", quality);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -150,24 +120,16 @@ namespace Kyoo.Core.Api
 | 
			
		||||
		/// Get the thumbnail for the specified item.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
 | 
			
		||||
		/// <param name="quality">The quality of the image to retrieve.</param>
 | 
			
		||||
		/// <returns>The image asked.</returns>
 | 
			
		||||
		/// <response code="404">
 | 
			
		||||
		/// No item exist with the specific identifier or the image does not exists on kyoo.
 | 
			
		||||
		/// </response>
 | 
			
		||||
		[HttpGet("{identifier:id}/thumbnail")]
 | 
			
		||||
		[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
 | 
			
		||||
		[HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)]
 | 
			
		||||
		public Task<IActionResult> GetBackdrop(Identifier identifier)
 | 
			
		||||
		public Task<IActionResult> GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
 | 
			
		||||
		{
 | 
			
		||||
			return _GetImage(identifier, Images.Thumbnail);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public override async Task<ActionResult<T>> Create([FromBody] T resource)
 | 
			
		||||
		{
 | 
			
		||||
			// TODO: Remove this method and use a websocket API to do that.
 | 
			
		||||
			ActionResult<T> ret = await base.Create(resource);
 | 
			
		||||
			await _thumbs.DownloadImages(ret.Value);
 | 
			
		||||
			return ret;
 | 
			
		||||
			return _GetImage(identifier, "thumbnail", quality);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,7 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
 | 
			
		||||
		{
 | 
			
		||||
			IList<JsonProperty> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// A custom <see cref="IValueProvider"/> that uses the
 | 
			
		||||
		/// <see cref="IThumbnails"/>.<see cref="IThumbnails.Images"/> as a value.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private class ThumbnailProvider : IValueProvider
 | 
			
		||||
		{
 | 
			
		||||
			/// <summary>
 | 
			
		||||
			/// The index/ID of the image to retrieve/set.
 | 
			
		||||
			/// </summary>
 | 
			
		||||
			private readonly int _imageIndex;
 | 
			
		||||
 | 
			
		||||
			/// <summary>
 | 
			
		||||
			/// Create a new <see cref="ThumbnailProvider"/>.
 | 
			
		||||
			/// </summary>
 | 
			
		||||
			/// <param name="imageIndex">The index/ID of the image to retrieve/set.</param>
 | 
			
		||||
			public ThumbnailProvider(int imageIndex)
 | 
			
		||||
			{
 | 
			
		||||
				_imageIndex = imageIndex;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/// <inheritdoc />
 | 
			
		||||
			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;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/// <inheritdoc />
 | 
			
		||||
			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();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// Information about one or multiple <see cref="Genre"/>.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[Route("genres")]
 | 
			
		||||
	[Route("genre", Order = AlternativeRoute)]
 | 
			
		||||
	[ApiController]
 | 
			
		||||
	[PartialPermission(nameof(Genre))]
 | 
			
		||||
	[ApiDefinition("Genres", Group = MetadataGroup)]
 | 
			
		||||
	public class GenreApi : CrudApi<Genre>
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The library manager used to modify or retrieve information about the data store.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly ILibraryManager _libraryManager;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="GenreApi"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="libraryManager">
 | 
			
		||||
		/// The library manager used to modify or retrieve information about the data store.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		public GenreApi(ILibraryManager libraryManager)
 | 
			
		||||
			: base(libraryManager.GenreRepository)
 | 
			
		||||
		{
 | 
			
		||||
			_libraryManager = libraryManager;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get shows with genre
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// Lists the shows that have the selected genre.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Genre"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort shows by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of shows to return and where to start.</param>
 | 
			
		||||
		/// <returns>A page of shows.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No genre with the given ID could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<Show>>> GetShows(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Show> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Genre>(x => x.Genres)),
 | 
			
		||||
				Sort<Show>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Genre>()) == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -83,7 +83,7 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Show> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioID, x => x.Studio.Slug)),
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio.Slug)),
 | 
			
		||||
				Sort<Show>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
@ -93,40 +93,5 @@ namespace Kyoo.Core.Api
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get libraries containing this collection
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// Lists the libraries that contain the collection with the given id or slug.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort libraries by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of libraries to return.</param>
 | 
			
		||||
		/// <returns>A page of libraries.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No collection with the given ID or slug could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Library> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.IsContainedIn<Library, Collection>(x => x.Collections)),
 | 
			
		||||
				Sort<Library>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// Information about one or multiple <see cref="Library"/>.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[Route("libraries")]
 | 
			
		||||
	[Route("library", Order = AlternativeRoute)]
 | 
			
		||||
	[ApiController]
 | 
			
		||||
	[ResourceView]
 | 
			
		||||
	[PartialPermission(nameof(Library), Group = Group.Admin)]
 | 
			
		||||
	[ApiDefinition("Library", Group = ResourcesGroup)]
 | 
			
		||||
	public class LibraryApi : CrudApi<Library>
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The library manager used to modify or retrieve information in the data store.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly ILibraryManager _libraryManager;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="EpisodeApi"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="libraryManager">
 | 
			
		||||
		/// The library manager used to modify or retrieve information in the data store.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		public LibraryApi(ILibraryManager libraryManager)
 | 
			
		||||
			: base(libraryManager.LibraryRepository)
 | 
			
		||||
		{
 | 
			
		||||
			_libraryManager = libraryManager;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get shows
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// List the shows that are part of this library.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort shows by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of shows to return.</param>
 | 
			
		||||
		/// <returns>A page of shows.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No library with the given ID or slug could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<Show>>> GetShows(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Show> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Library>(x => x.Libraries)),
 | 
			
		||||
				Sort<Show>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get collections
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// List the collections that are part of this library.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort collections by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of collections to return.</param>
 | 
			
		||||
		/// <returns>A page of collections.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No library with the given ID or slug could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Collection> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Library>(x => x.Libraries)),
 | 
			
		||||
				Sort<Collection>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get items
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort items by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of items to return.</param>
 | 
			
		||||
		/// <returns>A page of items.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No library with the given ID or slug could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<LibraryItem>>> GetItems(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			Expression<Func<LibraryItem, bool>> whereQuery = ApiHelper.ParseWhere<LibraryItem>(where);
 | 
			
		||||
			Sort<LibraryItem> sort = Sort<LibraryItem>.From(sortBy);
 | 
			
		||||
 | 
			
		||||
			ICollection<LibraryItem> resources = await identifier.Match(
 | 
			
		||||
				id => _libraryManager.GetItemsFromLibrary(id, whereQuery, sort, pagination),
 | 
			
		||||
				slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination)
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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<ActionResult<Page<LibraryItem>>> GetAll(
 | 
			
		||||
		public async Task<ActionResult<Page<ILibraryItem>>> GetAll(
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<LibraryItem> resources = await _libraryItems.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere<LibraryItem>(where),
 | 
			
		||||
				Sort<LibraryItem>.From(sortBy),
 | 
			
		||||
			ICollection<ILibraryItem> resources = await _libraryItems.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere<ILibraryItem>(where),
 | 
			
		||||
				Sort<ILibraryItem>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										149
									
								
								back/src/Kyoo.Core/Views/Resources/MovieApi.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								back/src/Kyoo.Core/Views/Resources/MovieApi.cs
									
									
									
									
									
										Normal file
									
								
							@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// Information about one or multiple <see cref="Movie"/>.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[Route("movies")]
 | 
			
		||||
	[Route("movie", Order = AlternativeRoute)]
 | 
			
		||||
	[ApiController]
 | 
			
		||||
	[PartialPermission(nameof(Show))]
 | 
			
		||||
	[ApiDefinition("Shows", Group = ResourcesGroup)]
 | 
			
		||||
	public class MovieApi : CrudThumbsApi<Movie>
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The library manager used to modify or retrieve information in the data store.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly ILibraryManager _libraryManager;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="ShowApi"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="libraryManager">
 | 
			
		||||
		/// The library manager used to modify or retrieve information about the data store.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
			
		||||
		public MovieApi(ILibraryManager libraryManager,
 | 
			
		||||
			IThumbnailsManager thumbs)
 | 
			
		||||
			: base(libraryManager.MovieRepository, thumbs)
 | 
			
		||||
		{
 | 
			
		||||
			_libraryManager = libraryManager;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// /// <summary>
 | 
			
		||||
		// /// Get staff
 | 
			
		||||
		// /// </summary>
 | 
			
		||||
		// /// <remarks>
 | 
			
		||||
		// /// List staff members that made this show.
 | 
			
		||||
		// /// </remarks>
 | 
			
		||||
		// /// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
 | 
			
		||||
		// /// <param name="sortBy">A key to sort staff members by.</param>
 | 
			
		||||
		// /// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		// /// <param name="pagination">The number of people to return.</param>
 | 
			
		||||
		// /// <returns>A page of people.</returns>
 | 
			
		||||
		// /// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		// /// <response code="404">No show with the given ID or slug could be found.</response>
 | 
			
		||||
		// [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<ActionResult<Page<PeopleRole>>> GetPeople(Identifier identifier,
 | 
			
		||||
		// 	[FromQuery] string sortBy,
 | 
			
		||||
		// 	[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
		// 	[FromQuery] Pagination pagination)
 | 
			
		||||
		// {
 | 
			
		||||
		// 	Expression<Func<PeopleRole, bool>> whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
 | 
			
		||||
		// 	Sort<PeopleRole> sort = Sort<PeopleRole>.From(sortBy);
 | 
			
		||||
		//
 | 
			
		||||
		// 	ICollection<PeopleRole> resources = await identifier.Match(
 | 
			
		||||
		// 		id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination),
 | 
			
		||||
		// 		slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination)
 | 
			
		||||
		// 	);
 | 
			
		||||
		// 	return Page(resources, pagination.Limit);
 | 
			
		||||
		// }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get studio that made the show
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// Get the studio that made the show.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
 | 
			
		||||
		/// <returns>The studio that made the show.</returns>
 | 
			
		||||
		/// <response code="404">No show with the given ID or slug could be found.</response>
 | 
			
		||||
		[HttpGet("{identifier:id}/studio")]
 | 
			
		||||
		[PartialPermission(Kind.Read)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
		public async Task<ActionResult<Studio>> GetStudio(Identifier identifier)
 | 
			
		||||
		{
 | 
			
		||||
			return await _libraryManager.Get(identifier.IsContainedIn<Studio, Movie>(x => x.Movies));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get collections containing this show
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// List the collections that contain this show.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort collections by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of collections to return.</param>
 | 
			
		||||
		/// <returns>A page of collections.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No show with the given ID or slug could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Collection> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Movie>(x => x.Movies)),
 | 
			
		||||
				Sort<Collection>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Movie>()) == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -76,10 +76,11 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			{
 | 
			
		||||
				Query = query,
 | 
			
		||||
				Collections = await _libraryManager.Search<Collection>(query),
 | 
			
		||||
				Items = await _libraryManager.Search<ILibraryItem>(query),
 | 
			
		||||
				Movies = await _libraryManager.Search<Movie>(query),
 | 
			
		||||
				Shows = await _libraryManager.Search<Show>(query),
 | 
			
		||||
				Episodes = await _libraryManager.Search<Episode>(query),
 | 
			
		||||
				People = await _libraryManager.Search<People>(query),
 | 
			
		||||
				Genres = await _libraryManager.Search<Genre>(query),
 | 
			
		||||
				Studios = await _libraryManager.Search<Studio>(query)
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
@ -133,9 +134,9 @@ namespace Kyoo.Core.Api
 | 
			
		||||
		[Permission(nameof(Show), Kind.Read)]
 | 
			
		||||
		[ApiDefinition("Items")]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
		public Task<ICollection<LibraryItem>> SearchItems(string query)
 | 
			
		||||
		public Task<ICollection<ILibraryItem>> SearchItems(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return _libraryManager.Search<LibraryItem>(query);
 | 
			
		||||
			return _libraryManager.Search<ILibraryItem>(query);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -175,24 +176,6 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			return _libraryManager.Search<People>(query);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Search genres
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// Search for genres
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="query">The query to search for.</param>
 | 
			
		||||
		/// <returns>A list of genres found for the specified query.</returns>
 | 
			
		||||
		[HttpGet("genres")]
 | 
			
		||||
		[HttpGet("genre", Order = AlternativeRoute)]
 | 
			
		||||
		[Permission(nameof(Genre), Kind.Read)]
 | 
			
		||||
		[ApiDefinition("Genres")]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
		public Task<ICollection<Genre>> SearchGenres(string query)
 | 
			
		||||
		{
 | 
			
		||||
			return _libraryManager.Search<Genre>(query);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Search studios
 | 
			
		||||
		/// </summary>
 | 
			
		||||
 | 
			
		||||
@ -84,7 +84,7 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Episode> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonID, x => x.Season.Slug)),
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season.Slug)),
 | 
			
		||||
				Sort<Episode>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
@ -37,8 +37,6 @@ namespace Kyoo.Core.Api
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		public override async Task<ActionResult<Show>> Create([FromBody] Show resource)
 | 
			
		||||
		{
 | 
			
		||||
			ActionResult<Show> 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get seasons of this show
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -106,7 +86,7 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Season> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)),
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowId, x => x.Show.Slug)),
 | 
			
		||||
				Sort<Season>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
@ -141,7 +121,7 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Episode> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowID, x => x.Show.Slug)),
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show.Slug)),
 | 
			
		||||
				Sort<Episode>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
@ -185,41 +165,6 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get genres of this show
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// List the genres that represent this show.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort genres by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of genres to return.</param>
 | 
			
		||||
		/// <returns>A page of genres.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No show with the given ID or slug could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<Genre>>> GetGenres(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Genre> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.IsContainedIn<Genre, Show>(x => x.Shows)),
 | 
			
		||||
				Sort<Genre>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get studio that made the show
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -238,42 +183,6 @@ namespace Kyoo.Core.Api
 | 
			
		||||
			return await _libraryManager.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get libraries containing this show
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// 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.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
 | 
			
		||||
		/// <param name="sortBy">A key to sort libraries by.</param>
 | 
			
		||||
		/// <param name="where">An optional list of filters.</param>
 | 
			
		||||
		/// <param name="pagination">The number of libraries to return.</param>
 | 
			
		||||
		/// <returns>A page of libraries.</returns>
 | 
			
		||||
		/// <response code="400">The filters or the sort parameters are invalid.</response>
 | 
			
		||||
		/// <response code="404">No show with the given ID or slug could be found.</response>
 | 
			
		||||
		[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<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
 | 
			
		||||
			[FromQuery] string sortBy,
 | 
			
		||||
			[FromQuery] Dictionary<string, string> where,
 | 
			
		||||
			[FromQuery] Pagination pagination)
 | 
			
		||||
		{
 | 
			
		||||
			ICollection<Library> resources = await _libraryManager.GetAll(
 | 
			
		||||
				ApiHelper.ParseWhere(where, identifier.IsContainedIn<Library, Show>(x => x.Shows)),
 | 
			
		||||
				Sort<Library>.From(sortBy),
 | 
			
		||||
				pagination
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return Page(resources, pagination.Limit);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get collections containing this show
 | 
			
		||||
		/// </summary>
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// Retrieve information of an <see cref="Episode"/> as a <see cref="WatchItem"/>.
 | 
			
		||||
	/// 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.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[Route("watch")]
 | 
			
		||||
	[Route("watchitem", Order = AlternativeRoute)]
 | 
			
		||||
	[ApiController]
 | 
			
		||||
	[ApiDefinition("Watch Items", Group = WatchGroup)]
 | 
			
		||||
	public class WatchApi : ControllerBase
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The library manager used to modify or retrieve information in the data store.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly ILibraryManager _libraryManager;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The http client to reach transcoder.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly HttpClient _client;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Create a new <see cref="WatchApi"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="libraryManager">
 | 
			
		||||
		/// The library manager used to modify or retrieve information in the data store.
 | 
			
		||||
		/// </param>
 | 
			
		||||
		/// <param name="client">The http client to reach transcoder.</param>
 | 
			
		||||
		public WatchApi(ILibraryManager libraryManager, HttpClient client)
 | 
			
		||||
		{
 | 
			
		||||
			_libraryManager = libraryManager;
 | 
			
		||||
			_client = client;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get a watch item
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <remarks>
 | 
			
		||||
		/// Retrieve a watch item of an episode.
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
 | 
			
		||||
		/// <returns>A page of items.</returns>
 | 
			
		||||
		/// <response code="404">No episode with the given ID or slug could be found.</response>
 | 
			
		||||
		[HttpGet("{identifier:id}")]
 | 
			
		||||
		[Permission("watch", Kind.Read)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
		public async Task<ActionResult<WatchItem>> GetWatchItem(Identifier identifier)
 | 
			
		||||
		{
 | 
			
		||||
			Episode item = await identifier.Match(
 | 
			
		||||
				id => _libraryManager.GetOrDefault<Episode>(id),
 | 
			
		||||
				slug => _libraryManager.GetOrDefault<Episode>(slug)
 | 
			
		||||
			);
 | 
			
		||||
			if (item == null)
 | 
			
		||||
				return NotFound();
 | 
			
		||||
			return await WatchItem.FromEpisode(item, _libraryManager, _client);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
	/// </remarks>
 | 
			
		||||
	public abstract class DatabaseContext : DbContext
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// All libraries of Kyoo. See <see cref="Library"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<Library> Libraries { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// All collections of Kyoo. See <see cref="Collection"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<Collection> Collections { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// All movies of Kyoo. See <see cref="Movie"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<Movie> Movies { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// All shows of Kyoo. See <see cref="Show"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -65,11 +66,6 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<Episode> Episodes { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// All genres of Kyoo. See <see cref="Genres"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<Genre> Genres { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// All people of Kyoo. See <see cref="People"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -80,11 +76,6 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<Studio> Studios { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// All providers of Kyoo. See <see cref="Provider"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<Provider> Providers { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of registered users.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -95,11 +86,6 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<PeopleRole> PeopleRoles { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		public DbSet<WatchedEpisode> WatchedEpisodes { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The list of library items (shows and collections that are part of a library - or the global one).
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -108,17 +94,6 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		/// </remarks>
 | 
			
		||||
		public DbSet<LibraryItem> LibraryItems { get; set; }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get all metadataIDs (ExternalIDs) of a given resource. See <see cref="MetadataID"/>.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <typeparam name="T">The metadata of this type will be returned.</typeparam>
 | 
			
		||||
		/// <returns>A queryable of metadata ids for a type.</returns>
 | 
			
		||||
		public DbSet<MetadataID> MetadataIds<T>()
 | 
			
		||||
			where T : class, IMetadata
 | 
			
		||||
		{
 | 
			
		||||
			return Set<MetadataID>(MetadataName<T>());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Add a many to many link between two resources.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -153,14 +128,6 @@ namespace Kyoo.Postgresql
 | 
			
		||||
			: base(options)
 | 
			
		||||
		{ }
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get the name of the metadata table of the given type.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <typeparam name="T">The type related to the metadata</typeparam>
 | 
			
		||||
		/// <returns>The name of the table containing the metadata.</returns>
 | 
			
		||||
		protected abstract string MetadataName<T>()
 | 
			
		||||
			where T : IMetadata;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Get the name of the link table of the two given types.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -194,17 +161,33 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="modelBuilder">The database model builder</param>
 | 
			
		||||
		/// <typeparam name="T">The type to add metadata to.</typeparam>
 | 
			
		||||
		private void _HasMetadata<T>(ModelBuilder modelBuilder)
 | 
			
		||||
		private static void _HasMetadata<T>(ModelBuilder modelBuilder)
 | 
			
		||||
			where T : class, IMetadata
 | 
			
		||||
		{
 | 
			
		||||
			modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>())
 | 
			
		||||
				.HasKey(MetadataID.PrimaryKey);
 | 
			
		||||
			// TODO: Waiting for https://github.com/dotnet/efcore/issues/29825
 | 
			
		||||
			// modelBuilder.Entity<T>()
 | 
			
		||||
			// 	.OwnsOne(x => x.ExternalIDs, x =>
 | 
			
		||||
			// 	{
 | 
			
		||||
			// 		x.ToJson();
 | 
			
		||||
			// 	});
 | 
			
		||||
			modelBuilder.Entity<T>()
 | 
			
		||||
				.Property(x => x.ExternalId)
 | 
			
		||||
				.HasConversion(
 | 
			
		||||
					v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
 | 
			
		||||
					v => JsonSerializer.Deserialize<Dictionary<string, MetadataId>>(v, (JsonSerializerOptions)null)
 | 
			
		||||
				)
 | 
			
		||||
				.HasColumnType("json");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>())
 | 
			
		||||
				.HasOne<T>()
 | 
			
		||||
				.WithMany(x => x.ExternalIDs)
 | 
			
		||||
				.HasForeignKey(x => x.ResourceID)
 | 
			
		||||
				.OnDelete(DeleteBehavior.Cascade);
 | 
			
		||||
		private static void _HasImages<T>(ModelBuilder modelBuilder)
 | 
			
		||||
			where T : class, IThumbnails
 | 
			
		||||
		{
 | 
			
		||||
			modelBuilder.Entity<T>()
 | 
			
		||||
				.OwnsOne(x => x.Poster);
 | 
			
		||||
			modelBuilder.Entity<T>()
 | 
			
		||||
				.OwnsOne(x => x.Thumbnail);
 | 
			
		||||
			modelBuilder.Entity<T>()
 | 
			
		||||
				.OwnsOne(x => x.Logo);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -248,6 +231,10 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		{
 | 
			
		||||
			base.OnModelCreating(modelBuilder);
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<Episode>()
 | 
			
		||||
				.Ignore(x => x.PreviousEpisode)
 | 
			
		||||
				.Ignore(x => x.NextEpisode);
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<PeopleRole>()
 | 
			
		||||
				.Ignore(x => x.ForPeople);
 | 
			
		||||
 | 
			
		||||
@ -264,72 +251,68 @@ namespace Kyoo.Postgresql
 | 
			
		||||
				.WithOne(x => x.Season)
 | 
			
		||||
				.OnDelete(DeleteBehavior.Cascade);
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<Movie>()
 | 
			
		||||
				.HasOne(x => x.Studio)
 | 
			
		||||
				.WithMany(x => x.Movies)
 | 
			
		||||
				.OnDelete(DeleteBehavior.SetNull);
 | 
			
		||||
			modelBuilder.Entity<Show>()
 | 
			
		||||
				.HasOne(x => x.Studio)
 | 
			
		||||
				.WithMany(x => x.Shows)
 | 
			
		||||
				.OnDelete(DeleteBehavior.SetNull);
 | 
			
		||||
 | 
			
		||||
			_HasManyToMany<Library, Provider>(modelBuilder, x => x.Providers, x => x.Libraries);
 | 
			
		||||
			_HasManyToMany<Library, Collection>(modelBuilder, x => x.Collections, x => x.Libraries);
 | 
			
		||||
			_HasManyToMany<Library, Show>(modelBuilder, x => x.Shows, x => x.Libraries);
 | 
			
		||||
			_HasManyToMany<Collection, Movie>(modelBuilder, x => x.Movies, x => x.Collections);
 | 
			
		||||
			_HasManyToMany<Collection, Show>(modelBuilder, x => x.Shows, x => x.Collections);
 | 
			
		||||
			_HasManyToMany<Show, Genre>(modelBuilder, x => x.Genres, x => x.Shows);
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<User>()
 | 
			
		||||
				.HasMany(x => x.Watched)
 | 
			
		||||
				.WithMany("Users")
 | 
			
		||||
				.UsingEntity(x => x.ToTable(LinkName<User, Show>()));
 | 
			
		||||
 | 
			
		||||
			_HasMetadata<LibraryItem>(modelBuilder);
 | 
			
		||||
			_HasMetadata<Collection>(modelBuilder);
 | 
			
		||||
			_HasMetadata<Movie>(modelBuilder);
 | 
			
		||||
			_HasMetadata<Show>(modelBuilder);
 | 
			
		||||
			_HasMetadata<Season>(modelBuilder);
 | 
			
		||||
			_HasMetadata<Episode>(modelBuilder);
 | 
			
		||||
			_HasMetadata<People>(modelBuilder);
 | 
			
		||||
			_HasMetadata<Studio>(modelBuilder);
 | 
			
		||||
 | 
			
		||||
			_HasImages<LibraryItem>(modelBuilder);
 | 
			
		||||
			_HasImages<Collection>(modelBuilder);
 | 
			
		||||
			_HasImages<Movie>(modelBuilder);
 | 
			
		||||
			_HasImages<Show>(modelBuilder);
 | 
			
		||||
			_HasImages<Season>(modelBuilder);
 | 
			
		||||
			_HasImages<Episode>(modelBuilder);
 | 
			
		||||
			_HasImages<People>(modelBuilder);
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<User>().OwnsOne(x => x.Logo);
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<WatchedEpisode>()
 | 
			
		||||
				.HasKey(x => new { User = x.UserID, Episode = x.EpisodeID });
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<Season>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<Episode>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
			modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<Collection>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Genre>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Library>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<People>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Movie>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Show>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Studio>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Provider>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Season>()
 | 
			
		||||
				.HasIndex(x => new { x.ShowID, x.SeasonNumber })
 | 
			
		||||
				.HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber })
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Season>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
			modelBuilder.Entity<Episode>()
 | 
			
		||||
				.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<Episode>()
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
@ -338,8 +321,10 @@ namespace Kyoo.Postgresql
 | 
			
		||||
				.HasIndex(x => x.Slug)
 | 
			
		||||
				.IsUnique();
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<Movie>()
 | 
			
		||||
				.Ignore(x => x.Links);
 | 
			
		||||
			modelBuilder.Entity<LibraryItem>()
 | 
			
		||||
				.ToView("library_items");
 | 
			
		||||
				.Ignore(x => x.Links);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -352,7 +337,7 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		public T GetTemporaryObject<T>(T model)
 | 
			
		||||
			where T : class, IResource
 | 
			
		||||
		{
 | 
			
		||||
			T tmp = Set<T>().Local.FirstOrDefault(x => x.ID == model.ID);
 | 
			
		||||
			T tmp = Set<T>().Local.FirstOrDefault(x => x.Id == model.Id);
 | 
			
		||||
			if (tmp != null)
 | 
			
		||||
				return tmp;
 | 
			
		||||
			Entry(model).State = EntityState.Unchanged;
 | 
			
		||||
 | 
			
		||||
@ -18,4 +18,8 @@
 | 
			
		||||
		<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
 | 
			
		||||
		<FrameworkReference Include="Microsoft.AspNetCore.App" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
	  <Folder Include="Migrations\" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// The initial migration that build most of the database.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[DbContext(typeof(PostgresContext))]
 | 
			
		||||
	[Migration("20210801171613_Initial")]
 | 
			
		||||
	public partial class Initial : Migration
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_collections", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "genres",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_genres", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "libraries",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					paths = table.Column<string[]>(type: "text[]", nullable: true)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_libraries", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "people",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_people", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "providers",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_providers", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "studios",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_studios", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "users",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					username = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					email = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					password = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					permissions = table.Column<string[]>(type: "text[]", nullable: true),
 | 
			
		||||
					extra_data = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true),
 | 
			
		||||
					images = table.Column<Dictionary<int, string>>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					library_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					provider_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					data_id = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					link = table.Column<string>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					provider_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					provider_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					data_id = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					link = table.Column<string>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					title = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					aliases = table.Column<string[]>(type: "text[]", nullable: true),
 | 
			
		||||
					path = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					status = table.Column<Status>(type: "status", nullable: false),
 | 
			
		||||
					start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
 | 
			
		||||
					end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
 | 
			
		||||
					images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
 | 
			
		||||
					is_movie = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
					studio_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					provider_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					data_id = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					link = table.Column<string>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					watched_id = table.Column<int>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					people_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					type = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					role = table.Column<string>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					show_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					season_number = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					title = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					start_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
 | 
			
		||||
					end_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
 | 
			
		||||
					images = table.Column<Dictionary<int, string>>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					provider_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					data_id = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					link = table.Column<string>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					show_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					season_id = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					season_number = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					episode_number = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					absolute_number = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					path = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
 | 
			
		||||
					title = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					release_date = table.Column<DateTime>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					provider_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					data_id = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					link = table.Column<string>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					provider_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					data_id = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					link = table.Column<string>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					title = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					language = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					codec = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					is_default = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
					is_forced = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
					is_external = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
					path = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					type = table.Column<object>(type: "stream_type", nullable: false),
 | 
			
		||||
					episode_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					track_index = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					episode_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					watched_percentage = table.Column<int>(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");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		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");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Postgresql.Migrations
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// A migration that adds postgres triggers to update slugs.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[DbContext(typeof(PostgresContext))]
 | 
			
		||||
	[Migration("20210801171641_Triggers")]
 | 
			
		||||
	public partial class Triggers : Migration
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Postgresql.Migrations
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// Remove triggers
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	[DbContext(typeof(PostgresContext))]
 | 
			
		||||
	[Migration("20230724144449_RemoveTriggers")]
 | 
			
		||||
	public partial class RemoveTriggers : Migration
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		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<string>(
 | 
			
		||||
				name: "slug",
 | 
			
		||||
				table: "tracks",
 | 
			
		||||
				type: "text",
 | 
			
		||||
				nullable: false,
 | 
			
		||||
				defaultValue: string.Empty,
 | 
			
		||||
				oldClrType: typeof(string),
 | 
			
		||||
				oldType: "text",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<string>(
 | 
			
		||||
				name: "slug",
 | 
			
		||||
				table: "seasons",
 | 
			
		||||
				type: "text",
 | 
			
		||||
				nullable: false,
 | 
			
		||||
				defaultValue: string.Empty,
 | 
			
		||||
				oldClrType: typeof(string),
 | 
			
		||||
				oldType: "text",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<string>(
 | 
			
		||||
				name: "slug",
 | 
			
		||||
				table: "episodes",
 | 
			
		||||
				type: "text",
 | 
			
		||||
				nullable: false,
 | 
			
		||||
				defaultValue: string.Empty,
 | 
			
		||||
				oldClrType: typeof(string),
 | 
			
		||||
				oldType: "text",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc/>
 | 
			
		||||
		protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
		{
 | 
			
		||||
			migrationBuilder.AlterColumn<string>(
 | 
			
		||||
				name: "slug",
 | 
			
		||||
				table: "tracks",
 | 
			
		||||
				type: "text",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(string),
 | 
			
		||||
				oldType: "text");
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<string>(
 | 
			
		||||
				name: "slug",
 | 
			
		||||
				table: "seasons",
 | 
			
		||||
				type: "text",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(string),
 | 
			
		||||
				oldType: "text");
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<string>(
 | 
			
		||||
				name: "slug",
 | 
			
		||||
				table: "episodes",
 | 
			
		||||
				type: "text",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(string),
 | 
			
		||||
				oldType: "text");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Postgresql.Migrations
 | 
			
		||||
{
 | 
			
		||||
	/// <inheritdoc />
 | 
			
		||||
	[DbContext(typeof(PostgresContext))]
 | 
			
		||||
	[Migration("20230726100747_Timestamp")]
 | 
			
		||||
	public partial class Timestamp : Migration
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
		{
 | 
			
		||||
			MigrationHelper.DropLibraryItemsView(migrationBuilder);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "start_air",
 | 
			
		||||
				table: "shows",
 | 
			
		||||
				type: "timestamp with time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp without time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "end_air",
 | 
			
		||||
				table: "shows",
 | 
			
		||||
				type: "timestamp with time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp without time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "start_date",
 | 
			
		||||
				table: "seasons",
 | 
			
		||||
				type: "timestamp with time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp without time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "end_date",
 | 
			
		||||
				table: "seasons",
 | 
			
		||||
				type: "timestamp with time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp without time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
		{
 | 
			
		||||
			MigrationHelper.DropLibraryItemsView(migrationBuilder);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "start_air",
 | 
			
		||||
				table: "shows",
 | 
			
		||||
				type: "timestamp without time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp with time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "end_air",
 | 
			
		||||
				table: "shows",
 | 
			
		||||
				type: "timestamp without time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp with time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "start_date",
 | 
			
		||||
				table: "seasons",
 | 
			
		||||
				type: "timestamp without time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp with time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				name: "end_date",
 | 
			
		||||
				table: "seasons",
 | 
			
		||||
				type: "timestamp without time zone",
 | 
			
		||||
				nullable: true,
 | 
			
		||||
				oldClrType: typeof(DateTime),
 | 
			
		||||
				oldType: "timestamp with time zone",
 | 
			
		||||
				oldNullable: true);
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.AlterColumn<DateTime>(
 | 
			
		||||
				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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Postgresql.Migrations
 | 
			
		||||
{
 | 
			
		||||
	/// <inheritdoc />
 | 
			
		||||
	public partial class RemoveTracks : Migration
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					episode_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					codec = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					is_default = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
					is_external = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
					is_forced = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
					language = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					path = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					slug = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					title = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					track_index = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					type = table.Column<string>(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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1502
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1502
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										547
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										547
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025737_initial.cs
									
									
									
									
									
										Normal file
									
								
							@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using Kyoo.Abstractions.Models;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Postgresql.Migrations
 | 
			
		||||
{
 | 
			
		||||
	/// <inheritdoc />
 | 
			
		||||
	public partial class Initial : Migration
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					poster_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					logo_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					external_id = table.Column<string>(type: "json", nullable: false)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_collections", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "people",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					poster_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					logo_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					external_id = table.Column<string>(type: "json", nullable: false)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_people", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "studios",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					external_id = table.Column<string>(type: "json", nullable: false)
 | 
			
		||||
				},
 | 
			
		||||
				constraints: table =>
 | 
			
		||||
				{
 | 
			
		||||
					table.PrimaryKey("pk_studios", x => x.id);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			migrationBuilder.CreateTable(
 | 
			
		||||
				name: "users",
 | 
			
		||||
				columns: table => new
 | 
			
		||||
				{
 | 
			
		||||
					id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					username = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					email = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					password = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					permissions = table.Column<string[]>(type: "text[]", nullable: false),
 | 
			
		||||
					logo_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					logo_blurhash = table.Column<string>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					tagline = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					aliases = table.Column<string[]>(type: "text[]", nullable: false),
 | 
			
		||||
					path = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					tags = table.Column<string[]>(type: "text[]", nullable: false),
 | 
			
		||||
					genres = table.Column<Genre[]>(type: "genre[]", nullable: false),
 | 
			
		||||
					status = table.Column<Status>(type: "status", nullable: false),
 | 
			
		||||
					air_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
					poster_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					logo_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					trailer = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					external_id = table.Column<string>(type: "json", nullable: false),
 | 
			
		||||
					studio_id = table.Column<int>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					tagline = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					aliases = table.Column<string[]>(type: "text[]", nullable: false),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					tags = table.Column<string[]>(type: "text[]", nullable: false),
 | 
			
		||||
					genres = table.Column<Genre[]>(type: "genre[]", nullable: false),
 | 
			
		||||
					status = table.Column<Status>(type: "status", nullable: false),
 | 
			
		||||
					start_air = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
					end_air = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
					poster_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					logo_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					trailer = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					external_id = table.Column<string>(type: "json", nullable: false),
 | 
			
		||||
					studio_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					movie_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					watched_id = table.Column<int>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					people_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					movie_id = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					type = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					role = table.Column<string>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					season_number = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					start_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
					end_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
					poster_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					logo_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					external_id = table.Column<string>(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<int>(type: "integer", nullable: false)
 | 
			
		||||
						.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
			
		||||
					show_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					season_id = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					season_number = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					episode_number = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					absolute_number = table.Column<int>(type: "integer", nullable: true),
 | 
			
		||||
					path = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
					name = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					overview = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					release_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
					poster_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					logo_source = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
					external_id = table.Column<string>(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<int>(type: "integer", nullable: false),
 | 
			
		||||
					episode_id = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
					watched_percentage = table.Column<int>(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");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1502
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025743_items.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1502
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025743_items.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										64
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025743_items.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								back/src/Kyoo.Postgresql/Migrations/20230806025743_items.cs
									
									
									
									
									
										Normal file
									
								
							@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Postgresql.Migrations
 | 
			
		||||
{
 | 
			
		||||
	/// <inheritdoc />
 | 
			
		||||
	public partial class Items : Migration
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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
 | 
			
		||||
			");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
		{
 | 
			
		||||
			// language=PostgreSQL
 | 
			
		||||
			migrationBuilder.Sql(@"DROP VIEW library_items");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -33,11 +33,6 @@ namespace Kyoo.Postgresql
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class PostgresContext : DatabaseContext
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// The connection string to use.
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		private readonly string _connection;
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// Is this instance in debug mode?
 | 
			
		||||
		/// </summary>
 | 
			
		||||
@ -48,12 +43,13 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		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<Status>();
 | 
			
		||||
			NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
 | 
			
		||||
			NpgsqlConnection.GlobalTypeMapper.MapEnum<Genre>();
 | 
			
		||||
			NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemKind>();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <summary>
 | 
			
		||||
@ -78,7 +74,6 @@ namespace Kyoo.Postgresql
 | 
			
		||||
		/// <param name="debugMode">Is this instance in debug mode?</param>
 | 
			
		||||
		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<Status>();
 | 
			
		||||
			modelBuilder.HasPostgresEnum<ItemType>();
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<LibraryItem>()
 | 
			
		||||
				.ToView("library_items")
 | 
			
		||||
				.HasKey(x => x.ID);
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<User>()
 | 
			
		||||
				.Property(x => x.ExtraData)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
 | 
			
		||||
			modelBuilder.Entity<LibraryItem>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.Entity<Collection>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.Entity<Show>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.Entity<Season>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.Entity<Episode>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.Entity<People>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.Entity<Provider>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.Entity<User>()
 | 
			
		||||
				.Property(x => x.Images)
 | 
			
		||||
				.HasColumnType("jsonb");
 | 
			
		||||
			modelBuilder.HasPostgresEnum<Genre>();
 | 
			
		||||
			modelBuilder.HasPostgresEnum<ItemKind>();
 | 
			
		||||
 | 
			
		||||
			base.OnModelCreating(modelBuilder);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override string MetadataName<T>()
 | 
			
		||||
		{
 | 
			
		||||
			SnakeCaseNameRewriter rewriter = new(CultureInfo.InvariantCulture);
 | 
			
		||||
			return rewriter.RewriteName(typeof(T).Name + nameof(MetadataID));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		protected override string LinkName<T, T2>()
 | 
			
		||||
		{
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ namespace Kyoo.Swagger
 | 
			
		||||
		{
 | 
			
		||||
			context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
 | 
			
		||||
			OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
 | 
			
		||||
				.Aggregate(new OpenApiSecurityRequirement(), (agg, cur) =>
 | 
			
		||||
				.Aggregate(new OpenApiSecurityRequirement(), (agg, _) =>
 | 
			
		||||
				{
 | 
			
		||||
					agg[nameof(Kyoo)] = Array.Empty<string>();
 | 
			
		||||
					return agg;
 | 
			
		||||
@ -60,15 +60,15 @@ namespace Kyoo.Swagger
 | 
			
		||||
				perms = context.MethodInfo.GetCustomAttributes<PartialPermissionAttribute>()
 | 
			
		||||
					.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<string> permissions = _GetPermissionsList(agg, group);
 | 
			
		||||
						permissions.Add($"{type}.{kind.ToString().ToLower()}");
 | 
			
		||||
						ICollection<string> permissions = _GetPermissionsList(agg, group ?? Group.Overall);
 | 
			
		||||
						permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}");
 | 
			
		||||
						agg[nameof(Kyoo)] = permissions;
 | 
			
		||||
						return agg;
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
				{
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
using Kyoo.Abstractions.Models;
 | 
			
		||||
using NJsonSchema;
 | 
			
		||||
using NJsonSchema.Generation;
 | 
			
		||||
 | 
			
		||||
namespace Kyoo.Swagger
 | 
			
		||||
{
 | 
			
		||||
	/// <summary>
 | 
			
		||||
	/// An operation processor to add computed fields of <see cref="IThumbnails"/>.
 | 
			
		||||
	/// </summary>
 | 
			
		||||
	public class ThumbnailProcessor : ISchemaProcessor
 | 
			
		||||
	{
 | 
			
		||||
		/// <inheritdoc />
 | 
			
		||||
		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."
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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<IShowRepository>(() => LibraryManager.ShowRepository));
 | 
			
		||||
			ShowRepository show = new(_NewContext(), studio, people, genre, provider);
 | 
			
		||||
			SeasonRepository season = new(_NewContext(), show, provider);
 | 
			
		||||
			LibraryItemRepository libraryItem = new(_NewContext(),
 | 
			
		||||
				new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository));
 | 
			
		||||
			EpisodeRepository episode = new(_NewContext(), show, provider);
 | 
			
		||||
			UserRepository user = new(_NewContext());
 | 
			
		||||
			Mock<IThumbnailsManager> thumbs = new();
 | 
			
		||||
			CollectionRepository collection = new(_NewContext(), thumbs.Object);
 | 
			
		||||
			StudioRepository studio = new(_NewContext(), thumbs.Object);
 | 
			
		||||
			PeopleRepository people = new(_NewContext(),
 | 
			
		||||
				new Lazy<IShowRepository>(() => 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
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -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<T> : IDisposable, IAsyncDisposable
 | 
			
		||||
		where T : class, IResource, new()
 | 
			
		||||
		where T : class, IResource
 | 
			
		||||
	{
 | 
			
		||||
		protected readonly RepositoryActivator Repositories;
 | 
			
		||||
		private readonly IRepository<T> _repository;
 | 
			
		||||
@ -63,7 +62,7 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task GetByIdTest()
 | 
			
		||||
		{
 | 
			
		||||
			T value = await _repository.Get(TestSample.Get<T>().ID);
 | 
			
		||||
			T value = await _repository.Get(TestSample.Get<T>().Id);
 | 
			
		||||
			KAssert.DeepEqual(TestSample.Get<T>(), value);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -89,7 +88,7 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task DeleteByIdTest()
 | 
			
		||||
		{
 | 
			
		||||
			await _repository.Delete(TestSample.Get<T>().ID);
 | 
			
		||||
			await _repository.Delete(TestSample.Get<T>().Id);
 | 
			
		||||
			Assert.Equal(0, await _repository.GetCount());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -114,23 +113,11 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
			await _repository.Delete(TestSample.Get<T>());
 | 
			
		||||
 | 
			
		||||
			T expected = TestSample.Get<T>();
 | 
			
		||||
			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<ArgumentNullException>(() => _repository.Create(null!));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task CreateIfNotExistNullTest()
 | 
			
		||||
		{
 | 
			
		||||
			await Assert.ThrowsAsync<ArgumentNullException>(() => _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<T>()));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task EditNullTest()
 | 
			
		||||
		{
 | 
			
		||||
			await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Edit(null!, false));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task EditNonExistingTest()
 | 
			
		||||
		{
 | 
			
		||||
			await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T { ID = 56 }, false));
 | 
			
		||||
		}
 | 
			
		||||
		// [Fact]
 | 
			
		||||
		// public async Task EditNonExistingTest()
 | 
			
		||||
		// {
 | 
			
		||||
		//	 await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T { Id = 56 }));
 | 
			
		||||
		// }
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task GetExpressionIDTest()
 | 
			
		||||
		{
 | 
			
		||||
			KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.ID == TestSample.Get<T>().ID));
 | 
			
		||||
			KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.Id == TestSample.Get<T>().Id));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
@ -170,12 +151,6 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
			await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(x => x.Slug == "non-existing"));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task GetExpressionNullTest()
 | 
			
		||||
		{
 | 
			
		||||
			await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Get((Expression<Func<T, bool>>)null!));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task GetOrDefaultTest()
 | 
			
		||||
		{
 | 
			
		||||
 | 
			
		||||
@ -66,40 +66,29 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
			Assert.Equal("2!", ret.Slug);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task CreateWithoutNameTest()
 | 
			
		||||
		{
 | 
			
		||||
			Collection collection = TestSample.GetNew<Collection>();
 | 
			
		||||
			collection.Name = null;
 | 
			
		||||
			await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(collection));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task CreateWithExternalIdTest()
 | 
			
		||||
		{
 | 
			
		||||
			Collection collection = TestSample.GetNew<Collection>();
 | 
			
		||||
			collection.ExternalIDs = new[]
 | 
			
		||||
			collection.ExternalId = new Dictionary<string, MetadataId>
 | 
			
		||||
			{
 | 
			
		||||
				new MetadataID
 | 
			
		||||
				["1"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.Get<Provider>(),
 | 
			
		||||
					Link = "link",
 | 
			
		||||
					DataID = "id"
 | 
			
		||||
					DataId = "id"
 | 
			
		||||
				},
 | 
			
		||||
				new MetadataID
 | 
			
		||||
				["2"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.GetNew<Provider>(),
 | 
			
		||||
					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<Collection>().Slug);
 | 
			
		||||
			value.Name = "New Title";
 | 
			
		||||
			value.Images = new Dictionary<int, string>
 | 
			
		||||
			{
 | 
			
		||||
				[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<Collection>().Slug);
 | 
			
		||||
			value.ExternalIDs = new[]
 | 
			
		||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
			
		||||
			{
 | 
			
		||||
				new MetadataID
 | 
			
		||||
				["test"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.Get<Provider>(),
 | 
			
		||||
					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<Collection>().Slug);
 | 
			
		||||
			value.ExternalIDs = new List<MetadataID>
 | 
			
		||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
			
		||||
			{
 | 
			
		||||
				new()
 | 
			
		||||
				["toto"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.Get<Provider>(),
 | 
			
		||||
					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<Provider>(),
 | 
			
		||||
				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);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -55,12 +55,11 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
		{
 | 
			
		||||
			Episode episode = await _repository.Get(1);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().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<Show>().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<Show>().Slug}-s2e1", episode.Slug);
 | 
			
		||||
			episode = await _repository.Get(1);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
 | 
			
		||||
@ -86,12 +84,11 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
		{
 | 
			
		||||
			Episode episode = await _repository.Get(1);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
 | 
			
		||||
			episode = await _repository.Edit(new Episode
 | 
			
		||||
			episode = await Repositories.LibraryManager.Patch<Episode>(episode.Id, (x) =>
 | 
			
		||||
			{
 | 
			
		||||
				ID = 1,
 | 
			
		||||
				EpisodeNumber = 2,
 | 
			
		||||
				ShowID = 1
 | 
			
		||||
			}, false);
 | 
			
		||||
				x.EpisodeNumber = 2;
 | 
			
		||||
				return Task.FromResult(true);
 | 
			
		||||
			});
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
			
		||||
			episode = await _repository.Get(1);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().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<Show>().ID,
 | 
			
		||||
				SeasonNumber = 2,
 | 
			
		||||
				EpisodeNumber = 4
 | 
			
		||||
			});
 | 
			
		||||
			Episode model = TestSample.Get<Episode>();
 | 
			
		||||
			model.Id = 0;
 | 
			
		||||
			model.ShowId = TestSample.Get<Show>().Id;
 | 
			
		||||
			model.SeasonNumber = 2;
 | 
			
		||||
			model.EpisodeNumber = 4;
 | 
			
		||||
			Episode episode = await _repository.Create(model);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().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<Show>().Slug}-56", episode.Slug);
 | 
			
		||||
			episode = await _repository.Get(2);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().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<Show>().Slug}-s1e2", episode.Slug);
 | 
			
		||||
			episode = await _repository.Get(2);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().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<Show>().Slug}-12", episode.Slug);
 | 
			
		||||
			episode = await _repository.Get(1);
 | 
			
		||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Fact]
 | 
			
		||||
		public async Task MovieEpisodeTest()
 | 
			
		||||
		{
 | 
			
		||||
			Episode episode = await _repository.Create(TestSample.GetMovieEpisode());
 | 
			
		||||
			Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
 | 
			
		||||
			episode = await _repository.Get(3);
 | 
			
		||||
			Assert.Equal(TestSample.Get<Show>().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<Episode>();
 | 
			
		||||
			value.ExternalIDs = new[]
 | 
			
		||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
			
		||||
			{
 | 
			
		||||
				new MetadataID
 | 
			
		||||
				["2"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.Get<Provider>(),
 | 
			
		||||
					Link = "link",
 | 
			
		||||
					DataID = "id"
 | 
			
		||||
					DataId = "id"
 | 
			
		||||
				},
 | 
			
		||||
				new MetadataID
 | 
			
		||||
				["3"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.GetNew<Provider>(),
 | 
			
		||||
					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<Episode>().Slug);
 | 
			
		||||
			value.Title = "New Title";
 | 
			
		||||
			value.Images = new Dictionary<int, string>
 | 
			
		||||
			{
 | 
			
		||||
				[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<Episode>().Slug);
 | 
			
		||||
			value.ExternalIDs = new[]
 | 
			
		||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
			
		||||
			{
 | 
			
		||||
				new MetadataID
 | 
			
		||||
				["1"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.Get<Provider>(),
 | 
			
		||||
					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<Episode>().Slug);
 | 
			
		||||
			value.ExternalIDs = new List<MetadataID>
 | 
			
		||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
			
		||||
			{
 | 
			
		||||
				new()
 | 
			
		||||
				["toto"] = new()
 | 
			
		||||
				{
 | 
			
		||||
					Provider = TestSample.Get<Provider>(),
 | 
			
		||||
					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<Provider>(),
 | 
			
		||||
				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<Episode>();
 | 
			
		||||
			value.Id = 0;
 | 
			
		||||
			value.Name = "This is a test super title";
 | 
			
		||||
			value.EpisodeNumber = 56;
 | 
			
		||||
			await _repository.Create(value);
 | 
			
		||||
			ICollection<Episode> ret = await _repository.Search(query);
 | 
			
		||||
			value.Show = TestSample.Get<Show>();
 | 
			
		||||
@ -342,9 +294,9 @@ namespace Kyoo.Tests.Database
 | 
			
		||||
			await _repository.Delete(TestSample.Get<Episode>());
 | 
			
		||||
 | 
			
		||||
			Episode expected = TestSample.Get<Episode>();
 | 
			
		||||
			expected.ID = 0;
 | 
			
		||||
			expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).ID;
 | 
			
		||||
			expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).ID;
 | 
			
		||||
			expected.Id = 0;
 | 
			
		||||
			expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
 | 
			
		||||
			expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).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<Episode>();
 | 
			
		||||
			KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<Episode>()));
 | 
			
		||||
			await _repository.Delete(TestSample.Get<Episode>());
 | 
			
		||||
			expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).ID;
 | 
			
		||||
			expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).ID;
 | 
			
		||||
			expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
 | 
			
		||||
			expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
 | 
			
		||||
			KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user