mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05:00 
			
		
		
		
	Add csharpier as a code formatter
This commit is contained in:
		
							parent
							
								
									baa78b9417
								
							
						
					
					
						commit
						7e6e56a366
					
				@ -7,6 +7,12 @@
 | 
				
			|||||||
      "commands": [
 | 
					      "commands": [
 | 
				
			||||||
        "dotnet-ef"
 | 
					        "dotnet-ef"
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "csharpier": {
 | 
				
			||||||
 | 
					      "version": "0.26.4",
 | 
				
			||||||
 | 
					      "commands": [
 | 
				
			||||||
 | 
					        "dotnet-csharpier"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -100,11 +100,13 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// <param name="reverse">Reverse the sort.</param>
 | 
							/// <param name="reverse">Reverse the sort.</param>
 | 
				
			||||||
		/// <param name="afterId">Select the first element after this id if it was in a list.</param>
 | 
							/// <param name="afterId">Select the first element after this id if it was in a list.</param>
 | 
				
			||||||
		/// <returns>The resource found</returns>
 | 
							/// <returns>The resource found</returns>
 | 
				
			||||||
		Task<T?> GetOrDefault(Filter<T>? filter,
 | 
							Task<T?> GetOrDefault(
 | 
				
			||||||
 | 
								Filter<T>? filter,
 | 
				
			||||||
			Include<T>? include = default,
 | 
								Include<T>? include = default,
 | 
				
			||||||
			Sort<T>? sortBy = default,
 | 
								Sort<T>? sortBy = default,
 | 
				
			||||||
			bool reverse = false,
 | 
								bool reverse = false,
 | 
				
			||||||
			Guid? afterId = default);
 | 
								Guid? afterId = default
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Search for resources with the database.
 | 
							/// Search for resources with the database.
 | 
				
			||||||
@ -122,10 +124,12 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// <param name="include">The related fields to include.</param>
 | 
							/// <param name="include">The related fields to include.</param>
 | 
				
			||||||
		/// <param name="limit">How pagination should be done (where to start and how many to return)</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>
 | 
							/// <returns>A list of resources that match every filters</returns>
 | 
				
			||||||
		Task<ICollection<T>> GetAll(Filter<T>? filter = null,
 | 
							Task<ICollection<T>> GetAll(
 | 
				
			||||||
 | 
								Filter<T>? filter = null,
 | 
				
			||||||
			Sort<T>? sort = default,
 | 
								Sort<T>? sort = default,
 | 
				
			||||||
			Include<T>? include = default,
 | 
								Include<T>? include = default,
 | 
				
			||||||
			Pagination? limit = default);
 | 
								Pagination? limit = default
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Get the number of resources that match the filter's predicate.
 | 
							/// Get the number of resources that match the filter's predicate.
 | 
				
			||||||
@ -166,8 +170,8 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="obj">The resource newly created.</param>
 | 
							/// <param name="obj">The resource newly created.</param>
 | 
				
			||||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
							/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
				
			||||||
		protected static Task OnResourceCreated(T obj)
 | 
							protected static Task OnResourceCreated(T obj) =>
 | 
				
			||||||
			=> OnCreated?.Invoke(obj) ?? Task.CompletedTask;
 | 
								OnCreated?.Invoke(obj) ?? Task.CompletedTask;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Edit a resource and replace every property
 | 
							/// Edit a resource and replace every property
 | 
				
			||||||
@ -199,8 +203,8 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="obj">The resource newly edited.</param>
 | 
							/// <param name="obj">The resource newly edited.</param>
 | 
				
			||||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
							/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
				
			||||||
		protected static Task OnResourceEdited(T obj)
 | 
							protected static Task OnResourceEdited(T obj) =>
 | 
				
			||||||
			=> OnEdited?.Invoke(obj) ?? Task.CompletedTask;
 | 
								OnEdited?.Invoke(obj) ?? Task.CompletedTask;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Delete a resource by it's ID
 | 
							/// Delete a resource by it's ID
 | 
				
			||||||
@ -243,8 +247,8 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="obj">The resource newly deleted.</param>
 | 
							/// <param name="obj">The resource newly deleted.</param>
 | 
				
			||||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
							/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
				
			||||||
		protected static Task OnResourceDeleted(T obj)
 | 
							protected static Task OnResourceDeleted(T obj) =>
 | 
				
			||||||
			=> OnDeleted?.Invoke(obj) ?? Task.CompletedTask;
 | 
								OnDeleted?.Invoke(obj) ?? Task.CompletedTask;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
 | 
				
			|||||||
@ -35,10 +35,12 @@ public interface ISearchManager
 | 
				
			|||||||
	/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
						/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
				
			||||||
	/// <param name="include">The related fields to include.</param>
 | 
						/// <param name="include">The related fields to include.</param>
 | 
				
			||||||
	/// <returns>A list of resources that match every filters</returns>
 | 
						/// <returns>A list of resources that match every filters</returns>
 | 
				
			||||||
	public Task<SearchPage<ILibraryItem>.SearchResult> SearchItems(string? query,
 | 
						public Task<SearchPage<ILibraryItem>.SearchResult> SearchItems(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<ILibraryItem> sortBy,
 | 
							Sort<ILibraryItem> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<ILibraryItem>? include = default);
 | 
							Include<ILibraryItem>? include = default
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
	/// Search for movies.
 | 
						/// Search for movies.
 | 
				
			||||||
@ -48,10 +50,12 @@ public interface ISearchManager
 | 
				
			|||||||
	/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
						/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
				
			||||||
	/// <param name="include">The related fields to include.</param>
 | 
						/// <param name="include">The related fields to include.</param>
 | 
				
			||||||
	/// <returns>A list of resources that match every filters</returns>
 | 
						/// <returns>A list of resources that match every filters</returns>
 | 
				
			||||||
	public Task<SearchPage<Movie>.SearchResult> SearchMovies(string? query,
 | 
						public Task<SearchPage<Movie>.SearchResult> SearchMovies(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Movie> sortBy,
 | 
							Sort<Movie> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Movie>? include = default);
 | 
							Include<Movie>? include = default
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
	/// Search for shows.
 | 
						/// Search for shows.
 | 
				
			||||||
@ -61,10 +65,12 @@ public interface ISearchManager
 | 
				
			|||||||
	/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
						/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
				
			||||||
	/// <param name="include">The related fields to include.</param>
 | 
						/// <param name="include">The related fields to include.</param>
 | 
				
			||||||
	/// <returns>A list of resources that match every filters</returns>
 | 
						/// <returns>A list of resources that match every filters</returns>
 | 
				
			||||||
	public Task<SearchPage<Show>.SearchResult> SearchShows(string? query,
 | 
						public Task<SearchPage<Show>.SearchResult> SearchShows(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Show> sortBy,
 | 
							Sort<Show> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Show>? include = default);
 | 
							Include<Show>? include = default
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
	/// Search for collections.
 | 
						/// Search for collections.
 | 
				
			||||||
@ -74,10 +80,12 @@ public interface ISearchManager
 | 
				
			|||||||
	/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
						/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
				
			||||||
	/// <param name="include">The related fields to include.</param>
 | 
						/// <param name="include">The related fields to include.</param>
 | 
				
			||||||
	/// <returns>A list of resources that match every filters</returns>
 | 
						/// <returns>A list of resources that match every filters</returns>
 | 
				
			||||||
	public Task<SearchPage<Collection>.SearchResult> SearchCollections(string? query,
 | 
						public Task<SearchPage<Collection>.SearchResult> SearchCollections(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Collection> sortBy,
 | 
							Sort<Collection> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Collection>? include = default);
 | 
							Include<Collection>? include = default
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
	/// Search for episodes.
 | 
						/// Search for episodes.
 | 
				
			||||||
@ -87,10 +95,12 @@ public interface ISearchManager
 | 
				
			|||||||
	/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
						/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
				
			||||||
	/// <param name="include">The related fields to include.</param>
 | 
						/// <param name="include">The related fields to include.</param>
 | 
				
			||||||
	/// <returns>A list of resources that match every filters</returns>
 | 
						/// <returns>A list of resources that match every filters</returns>
 | 
				
			||||||
	public Task<SearchPage<Episode>.SearchResult> SearchEpisodes(string? query,
 | 
						public Task<SearchPage<Episode>.SearchResult> SearchEpisodes(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Episode> sortBy,
 | 
							Sort<Episode> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Episode>? include = default);
 | 
							Include<Episode>? include = default
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
	/// Search for studios.
 | 
						/// Search for studios.
 | 
				
			||||||
@ -100,8 +110,10 @@ public interface ISearchManager
 | 
				
			|||||||
	/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
						/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
 | 
				
			||||||
	/// <param name="include">The related fields to include.</param>
 | 
						/// <param name="include">The related fields to include.</param>
 | 
				
			||||||
	/// <returns>A list of resources that match every filters</returns>
 | 
						/// <returns>A list of resources that match every filters</returns>
 | 
				
			||||||
	public Task<SearchPage<Studio>.SearchResult> SearchStudios(string? query,
 | 
						public Task<SearchPage<Studio>.SearchResult> SearchStudios(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Studio> sortBy,
 | 
							Sort<Studio> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Studio>? include = default);
 | 
							Include<Studio>? include = default
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -39,11 +39,17 @@ public interface IWatchStatusRepository
 | 
				
			|||||||
	Task<ICollection<IWatchlist>> GetAll(
 | 
						Task<ICollection<IWatchlist>> GetAll(
 | 
				
			||||||
		Filter<IWatchlist>? filter = default,
 | 
							Filter<IWatchlist>? filter = default,
 | 
				
			||||||
		Include<IWatchlist>? include = default,
 | 
							Include<IWatchlist>? include = default,
 | 
				
			||||||
		Pagination? limit = default);
 | 
							Pagination? limit = default
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Task<MovieWatchStatus?> GetMovieStatus(Guid movieId, Guid userId);
 | 
						Task<MovieWatchStatus?> GetMovieStatus(Guid movieId, Guid userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Task<MovieWatchStatus?> SetMovieStatus(Guid movieId, Guid userId, WatchStatus status, int? watchedTime);
 | 
						Task<MovieWatchStatus?> SetMovieStatus(
 | 
				
			||||||
 | 
							Guid movieId,
 | 
				
			||||||
 | 
							Guid userId,
 | 
				
			||||||
 | 
							WatchStatus status,
 | 
				
			||||||
 | 
							int? watchedTime
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Task DeleteMovieStatus(Guid movieId, Guid userId);
 | 
						Task DeleteMovieStatus(Guid movieId, Guid userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,7 +63,12 @@ public interface IWatchStatusRepository
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	/// <param name="watchedTime">Where the user has stopped watching. Only usable if Status
 | 
						/// <param name="watchedTime">Where the user has stopped watching. Only usable if Status
 | 
				
			||||||
	/// is <see cref="WatchStatus.Watching"/></param>
 | 
						/// is <see cref="WatchStatus.Watching"/></param>
 | 
				
			||||||
	Task<EpisodeWatchStatus?> SetEpisodeStatus(Guid episodeId, Guid userId, WatchStatus status, int? watchedTime);
 | 
						Task<EpisodeWatchStatus?> SetEpisodeStatus(
 | 
				
			||||||
 | 
							Guid episodeId,
 | 
				
			||||||
 | 
							Guid userId,
 | 
				
			||||||
 | 
							WatchStatus status,
 | 
				
			||||||
 | 
							int? watchedTime
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Task DeleteEpisodeStatus(Guid episodeId, Guid userId);
 | 
						Task DeleteEpisodeStatus(Guid episodeId, Guid userId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,6 @@
 | 
				
			|||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
					// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Diagnostics.CodeAnalysis;
 | 
					 | 
				
			||||||
using Microsoft.Extensions.DependencyInjection;
 | 
					using Microsoft.Extensions.DependencyInjection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Kyoo.Abstractions.Controllers
 | 
					namespace Kyoo.Abstractions.Controllers
 | 
				
			||||||
@ -26,8 +25,6 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
	/// A list of constant priorities used for <see cref="IStartupAction"/>'s <see cref="IStartupAction.Priority"/>.
 | 
						/// A list of constant priorities used for <see cref="IStartupAction"/>'s <see cref="IStartupAction.Priority"/>.
 | 
				
			||||||
	/// It also contains helper methods for creating new <see cref="StartupAction"/>.
 | 
						/// It also contains helper methods for creating new <see cref="StartupAction"/>.
 | 
				
			||||||
	/// </summary>
 | 
						/// </summary>
 | 
				
			||||||
	[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name",
 | 
					 | 
				
			||||||
		Justification = "StartupAction is nested and the name SA is short to improve readability in plugin's startup.")]
 | 
					 | 
				
			||||||
	public static class SA
 | 
						public static class SA
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -72,8 +69,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// <param name="action">The action to run</param>
 | 
							/// <param name="action">The action to run</param>
 | 
				
			||||||
		/// <param name="priority">The priority of the new action</param>
 | 
							/// <param name="priority">The priority of the new action</param>
 | 
				
			||||||
		/// <returns>A new <see cref="StartupAction"/></returns>
 | 
							/// <returns>A new <see cref="StartupAction"/></returns>
 | 
				
			||||||
		public static StartupAction New(Action action, int priority)
 | 
							public static StartupAction New(Action action, int priority) => new(action, priority);
 | 
				
			||||||
			=> new(action, priority);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Create a new <see cref="StartupAction"/>.
 | 
							/// Create a new <see cref="StartupAction"/>.
 | 
				
			||||||
@ -83,8 +79,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// <typeparam name="T">A dependency that this action will use.</typeparam>
 | 
							/// <typeparam name="T">A dependency that this action will use.</typeparam>
 | 
				
			||||||
		/// <returns>A new <see cref="StartupAction"/></returns>
 | 
							/// <returns>A new <see cref="StartupAction"/></returns>
 | 
				
			||||||
		public static StartupAction<T> New<T>(Action<T> action, int priority)
 | 
							public static StartupAction<T> New<T>(Action<T> action, int priority)
 | 
				
			||||||
			where T : notnull
 | 
								where T : notnull => new(action, priority);
 | 
				
			||||||
			=> new(action, priority);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Create a new <see cref="StartupAction"/>.
 | 
							/// Create a new <see cref="StartupAction"/>.
 | 
				
			||||||
@ -96,8 +91,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// <returns>A new <see cref="StartupAction"/></returns>
 | 
							/// <returns>A new <see cref="StartupAction"/></returns>
 | 
				
			||||||
		public static StartupAction<T, T2> New<T, T2>(Action<T, T2> action, int priority)
 | 
							public static StartupAction<T, T2> New<T, T2>(Action<T, T2> action, int priority)
 | 
				
			||||||
			where T : notnull
 | 
								where T : notnull
 | 
				
			||||||
			where T2 : notnull
 | 
								where T2 : notnull => new(action, priority);
 | 
				
			||||||
			=> new(action, priority);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Create a new <see cref="StartupAction"/>.
 | 
							/// Create a new <see cref="StartupAction"/>.
 | 
				
			||||||
@ -108,11 +102,13 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
		/// <typeparam name="T2">A second dependency that this action will use.</typeparam>
 | 
							/// <typeparam name="T2">A second dependency that this action will use.</typeparam>
 | 
				
			||||||
		/// <typeparam name="T3">A third dependency that this action will use.</typeparam>
 | 
							/// <typeparam name="T3">A third dependency that this action will use.</typeparam>
 | 
				
			||||||
		/// <returns>A new <see cref="StartupAction"/></returns>
 | 
							/// <returns>A new <see cref="StartupAction"/></returns>
 | 
				
			||||||
		public static StartupAction<T, T2, T3> New<T, T2, T3>(Action<T, T2, T3> action, int priority)
 | 
							public static StartupAction<T, T2, T3> New<T, T2, T3>(
 | 
				
			||||||
 | 
								Action<T, T2, T3> action,
 | 
				
			||||||
 | 
								int priority
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			where T : notnull
 | 
								where T : notnull
 | 
				
			||||||
			where T2 : notnull
 | 
								where T2 : notnull
 | 
				
			||||||
			where T3 : notnull
 | 
								where T3 : notnull => new(action, priority);
 | 
				
			||||||
			=> new(action, priority);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// A <see cref="IStartupAction"/> with no dependencies.
 | 
							/// A <see cref="IStartupAction"/> with no dependencies.
 | 
				
			||||||
@ -209,10 +205,7 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
			/// <inheritdoc />
 | 
								/// <inheritdoc />
 | 
				
			||||||
			public void Run(IServiceProvider provider)
 | 
								public void Run(IServiceProvider provider)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				_action.Invoke(
 | 
									_action.Invoke(provider.GetRequiredService<T>(), provider.GetRequiredService<T2>());
 | 
				
			||||||
					provider.GetRequiredService<T>(),
 | 
					 | 
				
			||||||
					provider.GetRequiredService<T2>()
 | 
					 | 
				
			||||||
				);
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -48,7 +48,6 @@ namespace Kyoo.Abstractions.Models.Exceptions
 | 
				
			|||||||
		/// <param name="info">Serialization infos</param>
 | 
							/// <param name="info">Serialization infos</param>
 | 
				
			||||||
		/// <param name="context">The serialization context</param>
 | 
							/// <param name="context">The serialization context</param>
 | 
				
			||||||
		protected DuplicatedItemException(SerializationInfo info, StreamingContext context)
 | 
							protected DuplicatedItemException(SerializationInfo info, StreamingContext context)
 | 
				
			||||||
			: base(info, context)
 | 
								: base(info, context) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -37,8 +37,7 @@ namespace Kyoo.Abstractions.Models.Exceptions
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="message">The message of the exception</param>
 | 
							/// <param name="message">The message of the exception</param>
 | 
				
			||||||
		public ItemNotFoundException(string message)
 | 
							public ItemNotFoundException(string message)
 | 
				
			||||||
			: base(message)
 | 
								: base(message) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The serialization constructor
 | 
							/// The serialization constructor
 | 
				
			||||||
@ -46,7 +45,6 @@ namespace Kyoo.Abstractions.Models.Exceptions
 | 
				
			|||||||
		/// <param name="info">Serialization infos</param>
 | 
							/// <param name="info">Serialization infos</param>
 | 
				
			||||||
		/// <param name="context">The serialization context</param>
 | 
							/// <param name="context">The serialization context</param>
 | 
				
			||||||
		protected ItemNotFoundException(SerializationInfo info, StreamingContext context)
 | 
							protected ItemNotFoundException(SerializationInfo info, StreamingContext context)
 | 
				
			||||||
			: base(info, context)
 | 
								: base(info, context) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -25,15 +25,12 @@ namespace Kyoo.Abstractions.Models.Exceptions
 | 
				
			|||||||
	public class UnauthorizedException : Exception
 | 
						public class UnauthorizedException : Exception
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		public UnauthorizedException()
 | 
							public UnauthorizedException()
 | 
				
			||||||
			: base("User not authenticated or token invalid.")
 | 
								: base("User not authenticated or token invalid.") { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public UnauthorizedException(string message)
 | 
							public UnauthorizedException(string message)
 | 
				
			||||||
			: base(message)
 | 
								: base(message) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protected UnauthorizedException(SerializationInfo info, StreamingContext context)
 | 
							protected UnauthorizedException(SerializationInfo info, StreamingContext context)
 | 
				
			||||||
			: base(info, context)
 | 
								: base(info, context) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -24,5 +24,4 @@ namespace Kyoo.Abstractions.Models;
 | 
				
			|||||||
/// A watch list item.
 | 
					/// A watch list item.
 | 
				
			||||||
/// </summary>
 | 
					/// </summary>
 | 
				
			||||||
[OneOf(Types = new[] { typeof(Show), typeof(Movie) })]
 | 
					[OneOf(Types = new[] { typeof(Show), typeof(Movie) })]
 | 
				
			||||||
public interface IWatchlist : IResource, IThumbnails, IMetadata, IAddedDate
 | 
					public interface IWatchlist : IResource, IThumbnails, IMetadata, IAddedDate { }
 | 
				
			||||||
{ }
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -67,7 +67,13 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <param name="previous">The link of the previous page.</param>
 | 
							/// <param name="previous">The link of the previous page.</param>
 | 
				
			||||||
		/// <param name="next">The link of the next page.</param>
 | 
							/// <param name="next">The link of the next page.</param>
 | 
				
			||||||
		/// <param name="first">The link of the first page.</param>
 | 
							/// <param name="first">The link of the first page.</param>
 | 
				
			||||||
		public Page(ICollection<T> items, string @this, string? previous, string? next, string first)
 | 
							public Page(
 | 
				
			||||||
 | 
								ICollection<T> items,
 | 
				
			||||||
 | 
								string @this,
 | 
				
			||||||
 | 
								string? previous,
 | 
				
			||||||
 | 
								string? next,
 | 
				
			||||||
 | 
								string first
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Items = items;
 | 
								Items = items;
 | 
				
			||||||
			This = @this;
 | 
								This = @this;
 | 
				
			||||||
@ -83,10 +89,7 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <param name="url">The base url of the resources available from this page.</param>
 | 
							/// <param name="url">The base url of the resources available from this page.</param>
 | 
				
			||||||
		/// <param name="query">The list of query strings of the current page</param>
 | 
							/// <param name="query">The list of query strings of the current page</param>
 | 
				
			||||||
		/// <param name="limit">The number of items requested for the current page.</param>
 | 
							/// <param name="limit">The number of items requested for the current page.</param>
 | 
				
			||||||
		public Page(ICollection<T> items,
 | 
							public Page(ICollection<T> items, string url, Dictionary<string, string> query, int limit)
 | 
				
			||||||
			string url,
 | 
					 | 
				
			||||||
			Dictionary<string, string> query,
 | 
					 | 
				
			||||||
			int limit)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Items = items;
 | 
								Items = items;
 | 
				
			||||||
			This = url + query.ToQueryString();
 | 
								This = url + query.ToQueryString();
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		public Guid Id { get; set; }
 | 
							public Guid Id { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		[MaxLength(256)] public string Slug { get; set; }
 | 
							[MaxLength(256)]
 | 
				
			||||||
 | 
							public string Slug { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The name of this collection.
 | 
							/// The name of this collection.
 | 
				
			||||||
@ -64,12 +65,14 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of movies contained in this collection.
 | 
							/// The list of movies contained in this collection.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Movie>? Movies { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Movie>? Movies { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of shows contained in this collection.
 | 
							/// The list of shows contained in this collection.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Show>? Shows { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Show>? Shows { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
							public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
	public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, INews
 | 
						public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, INews
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// Use absolute numbers by default and fallback to season/episodes if it does not exists.
 | 
							// Use absolute numbers by default and fallback to season/episodes if it does not exists.
 | 
				
			||||||
		public static Sort DefaultSort => new Sort<Episode>.Conglomerate(
 | 
							public static Sort DefaultSort =>
 | 
				
			||||||
 | 
								new Sort<Episode>.Conglomerate(
 | 
				
			||||||
				new Sort<Episode>.By(x => x.AbsoluteNumber),
 | 
									new Sort<Episode>.By(x => x.AbsoluteNumber),
 | 
				
			||||||
				new Sort<Episode>.By(x => x.SeasonNumber),
 | 
									new Sort<Episode>.By(x => x.SeasonNumber),
 | 
				
			||||||
				new Sort<Episode>.By(x => x.EpisodeNumber)
 | 
									new Sort<Episode>.By(x => x.EpisodeNumber)
 | 
				
			||||||
@ -51,10 +52,14 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
			get
 | 
								get
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (ShowSlug != null || Show?.Slug != null)
 | 
									if (ShowSlug != null || Show?.Slug != null)
 | 
				
			||||||
					return GetSlug(ShowSlug ?? Show!.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
 | 
										return GetSlug(
 | 
				
			||||||
 | 
											ShowSlug ?? Show!.Slug,
 | 
				
			||||||
 | 
											SeasonNumber,
 | 
				
			||||||
 | 
											EpisodeNumber,
 | 
				
			||||||
 | 
											AbsoluteNumber
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
 | 
									return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			[UsedImplicitly]
 | 
								[UsedImplicitly]
 | 
				
			||||||
			private set
 | 
								private set
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -85,7 +90,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
 | 
							/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public string? ShowSlug { private get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public string? ShowSlug { private get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the Show containing this episode.
 | 
							/// The ID of the Show containing this episode.
 | 
				
			||||||
@ -95,7 +101,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The show that contains this episode.
 | 
							/// The show that contains this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
 | 
							[LoadableRelation(nameof(ShowId))]
 | 
				
			||||||
 | 
							public Show? Show { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the Season containing this episode.
 | 
							/// The ID of the Season containing this episode.
 | 
				
			||||||
@ -109,7 +116,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// This can be null if the season is unknown and the episode is only identified
 | 
							/// This can be null if the season is unknown and the episode is only identified
 | 
				
			||||||
		/// by it's <see cref="AbsoluteNumber"/>.
 | 
							/// by it's <see cref="AbsoluteNumber"/>.
 | 
				
			||||||
		/// </remarks>
 | 
							/// </remarks>
 | 
				
			||||||
		[LoadableRelation(nameof(SeasonId))] public Season? Season { get; set; }
 | 
							[LoadableRelation(nameof(SeasonId))]
 | 
				
			||||||
 | 
							public Season? Season { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The season in witch this episode is in.
 | 
							/// The season in witch this episode is in.
 | 
				
			||||||
@ -192,12 +200,15 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		)]
 | 
							)]
 | 
				
			||||||
		public Episode? PreviousEpisode { get; set; }
 | 
							public Episode? PreviousEpisode { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private Episode? _PreviousEpisode => Show!.Episodes!
 | 
							private Episode? _PreviousEpisode =>
 | 
				
			||||||
 | 
								Show!
 | 
				
			||||||
 | 
									.Episodes!
 | 
				
			||||||
				.OrderBy(x => x.AbsoluteNumber == null)
 | 
									.OrderBy(x => x.AbsoluteNumber == null)
 | 
				
			||||||
				.ThenByDescending(x => x.AbsoluteNumber)
 | 
									.ThenByDescending(x => x.AbsoluteNumber)
 | 
				
			||||||
				.ThenByDescending(x => x.SeasonNumber)
 | 
									.ThenByDescending(x => x.SeasonNumber)
 | 
				
			||||||
				.ThenByDescending(x => x.EpisodeNumber)
 | 
									.ThenByDescending(x => x.EpisodeNumber)
 | 
				
			||||||
			.FirstOrDefault(x =>
 | 
									.FirstOrDefault(
 | 
				
			||||||
 | 
										x =>
 | 
				
			||||||
						x.AbsoluteNumber < AbsoluteNumber
 | 
											x.AbsoluteNumber < AbsoluteNumber
 | 
				
			||||||
						|| x.SeasonNumber < SeasonNumber
 | 
											|| x.SeasonNumber < SeasonNumber
 | 
				
			||||||
						|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber)
 | 
											|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber)
 | 
				
			||||||
@ -229,17 +240,21 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		)]
 | 
							)]
 | 
				
			||||||
		public Episode? NextEpisode { get; set; }
 | 
							public Episode? NextEpisode { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private Episode? _NextEpisode => Show!.Episodes!
 | 
							private Episode? _NextEpisode =>
 | 
				
			||||||
 | 
								Show!
 | 
				
			||||||
 | 
									.Episodes!
 | 
				
			||||||
				.OrderBy(x => x.AbsoluteNumber)
 | 
									.OrderBy(x => x.AbsoluteNumber)
 | 
				
			||||||
				.ThenBy(x => x.SeasonNumber)
 | 
									.ThenBy(x => x.SeasonNumber)
 | 
				
			||||||
				.ThenBy(x => x.EpisodeNumber)
 | 
									.ThenBy(x => x.EpisodeNumber)
 | 
				
			||||||
			.FirstOrDefault(x =>
 | 
									.FirstOrDefault(
 | 
				
			||||||
 | 
										x =>
 | 
				
			||||||
						x.AbsoluteNumber > AbsoluteNumber
 | 
											x.AbsoluteNumber > AbsoluteNumber
 | 
				
			||||||
						|| x.SeasonNumber > SeasonNumber
 | 
											|| x.SeasonNumber > SeasonNumber
 | 
				
			||||||
						|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber)
 | 
											|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber)
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		[SerializeIgnore] public ICollection<EpisodeWatchStatus>? Watched { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<EpisodeWatchStatus>? Watched { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Metadata of what an user as started/planned to watch.
 | 
							/// Metadata of what an user as started/planned to watch.
 | 
				
			||||||
@ -257,7 +272,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Links to watch this episode.
 | 
							/// Links to watch this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		public VideoLinks Links => new()
 | 
							public VideoLinks Links =>
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				Direct = $"/video/episode/{Slug}/direct",
 | 
									Direct = $"/video/episode/{Slug}/direct",
 | 
				
			||||||
				Hls = $"/video/episode/{Slug}/master.m3u8",
 | 
									Hls = $"/video/episode/{Slug}/master.m3u8",
 | 
				
			||||||
@ -280,10 +296,12 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// If you don't know it or this is a movie, use null
 | 
							/// If you don't know it or this is a movie, use null
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <returns>The slug corresponding to the given arguments</returns>
 | 
							/// <returns>The slug corresponding to the given arguments</returns>
 | 
				
			||||||
		public static string GetSlug(string showSlug,
 | 
							public static string GetSlug(
 | 
				
			||||||
 | 
								string showSlug,
 | 
				
			||||||
			int? seasonNumber,
 | 
								int? seasonNumber,
 | 
				
			||||||
			int? episodeNumber,
 | 
								int? episodeNumber,
 | 
				
			||||||
			int? absoluteNumber = null)
 | 
								int? absoluteNumber = null
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return seasonNumber switch
 | 
								return seasonNumber switch
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
 | 
				
			|||||||
@ -82,7 +82,11 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/// <inheritdoc />
 | 
								/// <inheritdoc />
 | 
				
			||||||
			public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
 | 
								public override object ConvertFrom(
 | 
				
			||||||
 | 
									ITypeDescriptorContext? context,
 | 
				
			||||||
 | 
									CultureInfo? culture,
 | 
				
			||||||
 | 
									object value
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (value is not string source)
 | 
									if (value is not string source)
 | 
				
			||||||
					return base.ConvertFrom(context, culture, value)!;
 | 
										return base.ConvertFrom(context, culture, value)!;
 | 
				
			||||||
@ -90,7 +94,10 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/// <inheritdoc />
 | 
								/// <inheritdoc />
 | 
				
			||||||
			public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
 | 
								public override bool CanConvertTo(
 | 
				
			||||||
 | 
									ITypeDescriptorContext? context,
 | 
				
			||||||
 | 
									Type? destinationType
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return false;
 | 
									return false;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,16 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
	/// A series or a movie.
 | 
						/// A series or a movie.
 | 
				
			||||||
	/// </summary>
 | 
						/// </summary>
 | 
				
			||||||
	public class Movie : IQuery, IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem, INews, IWatchlist
 | 
						public class Movie
 | 
				
			||||||
 | 
							: IQuery,
 | 
				
			||||||
 | 
								IResource,
 | 
				
			||||||
 | 
								IMetadata,
 | 
				
			||||||
 | 
								IOnMerge,
 | 
				
			||||||
 | 
								IThumbnails,
 | 
				
			||||||
 | 
								IAddedDate,
 | 
				
			||||||
 | 
								ILibraryItem,
 | 
				
			||||||
 | 
								INews,
 | 
				
			||||||
 | 
								IWatchlist
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		public static Sort DefaultSort => new Sort<Movie>.By(x => x.Name);
 | 
							public static Sort DefaultSort => new Sort<Movie>.By(x => x.Name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -120,12 +129,14 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the Studio that made this show.
 | 
							/// The ID of the Studio that made this show.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid? StudioId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid? StudioId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The Studio that made this show.
 | 
							/// The Studio that made this show.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[LoadableRelation(nameof(StudioId))] public Studio? Studio { get; set; }
 | 
							[LoadableRelation(nameof(StudioId))]
 | 
				
			||||||
 | 
							public Studio? Studio { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// /// <summary>
 | 
							// /// <summary>
 | 
				
			||||||
		// /// The list of people that made this show.
 | 
							// /// The list of people that made this show.
 | 
				
			||||||
@ -135,18 +146,21 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of collections that contains this show.
 | 
							/// The list of collections that contains this show.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Collection>? Collections { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Collection>? Collections { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Links to watch this movie.
 | 
							/// Links to watch this movie.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		public VideoLinks Links => new()
 | 
							public VideoLinks Links =>
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				Direct = $"/video/movie/{Slug}/direct",
 | 
									Direct = $"/video/movie/{Slug}/direct",
 | 
				
			||||||
				Hls = $"/video/movie/{Slug}/master.m3u8",
 | 
									Hls = $"/video/movie/{Slug}/master.m3u8",
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		[SerializeIgnore] public ICollection<MovieWatchStatus>? Watched { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<MovieWatchStatus>? Watched { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Metadata of what an user as started/planned to watch.
 | 
							/// Metadata of what an user as started/planned to watch.
 | 
				
			||||||
 | 
				
			|||||||
@ -62,7 +62,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
 | 
							/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<PeopleRole>? Roles { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<PeopleRole>? Roles { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public People() { }
 | 
							public People() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -49,7 +49,6 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
					return $"{ShowId}-s{SeasonNumber}";
 | 
										return $"{ShowId}-s{SeasonNumber}";
 | 
				
			||||||
				return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
 | 
									return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			[UsedImplicitly]
 | 
								[UsedImplicitly]
 | 
				
			||||||
			[NotNull]
 | 
								[NotNull]
 | 
				
			||||||
			private set
 | 
								private set
 | 
				
			||||||
@ -57,7 +56,9 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
				Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)");
 | 
									Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!match.Success)
 | 
									if (!match.Success)
 | 
				
			||||||
					throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}");
 | 
										throw new ArgumentException(
 | 
				
			||||||
 | 
											"Invalid season slug. Format: {showSlug}-s{seasonNumber}"
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				ShowSlug = match.Groups["show"].Value;
 | 
									ShowSlug = match.Groups["show"].Value;
 | 
				
			||||||
				SeasonNumber = int.Parse(match.Groups["season"].Value);
 | 
									SeasonNumber = int.Parse(match.Groups["season"].Value);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -66,7 +67,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
 | 
							/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public string? ShowSlug { private get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public string? ShowSlug { private get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the Show containing this season.
 | 
							/// The ID of the Show containing this season.
 | 
				
			||||||
@ -76,7 +78,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The show that contains this season.
 | 
							/// The show that contains this season.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
 | 
							[LoadableRelation(nameof(ShowId))]
 | 
				
			||||||
 | 
							public Show? Show { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The number of this season. This can be set to 0 to indicate specials.
 | 
							/// The number of this season. This can be set to 0 to indicate specials.
 | 
				
			||||||
@ -121,7 +124,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of episodes that this season contains.
 | 
							/// The list of episodes that this season contains.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Episode>? Episodes { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Episode>? Episodes { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The number of episodes in this season.
 | 
							/// The number of episodes in this season.
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,15 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
	/// <summary>
 | 
						/// <summary>
 | 
				
			||||||
	/// A series or a movie.
 | 
						/// A series or a movie.
 | 
				
			||||||
	/// </summary>
 | 
						/// </summary>
 | 
				
			||||||
	public class Show : IQuery, IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem, IWatchlist
 | 
						public class Show
 | 
				
			||||||
 | 
							: IQuery,
 | 
				
			||||||
 | 
								IResource,
 | 
				
			||||||
 | 
								IMetadata,
 | 
				
			||||||
 | 
								IOnMerge,
 | 
				
			||||||
 | 
								IThumbnails,
 | 
				
			||||||
 | 
								IAddedDate,
 | 
				
			||||||
 | 
								ILibraryItem,
 | 
				
			||||||
 | 
								IWatchlist
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		public static Sort DefaultSort => new Sort<Show>.By(x => x.Name);
 | 
							public static Sort DefaultSort => new Sort<Show>.By(x => x.Name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -121,12 +129,14 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the Studio that made this show.
 | 
							/// The ID of the Studio that made this show.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid? StudioId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid? StudioId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The Studio that made this show.
 | 
							/// The Studio that made this show.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[LoadableRelation(nameof(StudioId))] public Studio? Studio { get; set; }
 | 
							[LoadableRelation(nameof(StudioId))]
 | 
				
			||||||
 | 
							public Studio? Studio { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// /// <summary>
 | 
							// /// <summary>
 | 
				
			||||||
		// /// The list of people that made this show.
 | 
							// /// The list of people that made this show.
 | 
				
			||||||
@ -136,19 +146,22 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The different seasons in this show. If this is a movie, this list is always null or empty.
 | 
							/// The different seasons in this show. If this is a movie, this list is always null or empty.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Season>? Seasons { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Season>? Seasons { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of episodes in this show.
 | 
							/// 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).
 | 
							/// 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.
 | 
							/// Having an episode is necessary to store metadata and tracks.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Episode>? Episodes { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Episode>? Episodes { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of collections that contains this show.
 | 
							/// The list of collections that contains this show.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Collection>? Collections { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Collection>? Collections { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The first episode of this show.
 | 
							/// The first episode of this show.
 | 
				
			||||||
@ -172,7 +185,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		)]
 | 
							)]
 | 
				
			||||||
		public Episode? FirstEpisode { get; set; }
 | 
							public Episode? FirstEpisode { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private Episode? _FirstEpisode => Episodes!
 | 
							private Episode? _FirstEpisode =>
 | 
				
			||||||
 | 
								Episodes!
 | 
				
			||||||
				.OrderBy(x => x.AbsoluteNumber)
 | 
									.OrderBy(x => x.AbsoluteNumber)
 | 
				
			||||||
				.ThenBy(x => x.SeasonNumber)
 | 
									.ThenBy(x => x.SeasonNumber)
 | 
				
			||||||
				.ThenBy(x => x.EpisodeNumber)
 | 
									.ThenBy(x => x.EpisodeNumber)
 | 
				
			||||||
@ -199,7 +213,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		private int _EpisodesCount => Episodes!.Count;
 | 
							private int _EpisodesCount => Episodes!.Count;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		[SerializeIgnore] public ICollection<ShowWatchStatus>? Watched { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<ShowWatchStatus>? Watched { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Metadata of what an user as started/planned to watch.
 | 
							/// Metadata of what an user as started/planned to watch.
 | 
				
			||||||
 | 
				
			|||||||
@ -48,12 +48,14 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of shows that are made by this studio.
 | 
							/// The list of shows that are made by this studio.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Show>? Shows { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Show>? Shows { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The list of movies that are made by this studio.
 | 
							/// The list of movies that are made by this studio.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public ICollection<Movie>? Movies { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public ICollection<Movie>? Movies { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
							public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
 | 
				
			||||||
 | 
				
			|||||||
@ -56,22 +56,26 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the user that started watching this episode.
 | 
							/// The ID of the user that started watching this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid UserId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid UserId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The user that started watching this episode.
 | 
							/// The user that started watching this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public User User { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public User User { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the movie started.
 | 
							/// The ID of the movie started.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid MovieId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid MovieId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The <see cref="Movie"/> started.
 | 
							/// The <see cref="Movie"/> started.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Movie Movie { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Movie Movie { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc/>
 | 
							/// <inheritdoc/>
 | 
				
			||||||
		public DateTime AddedDate { get; set; }
 | 
							public DateTime AddedDate { get; set; }
 | 
				
			||||||
@ -109,22 +113,26 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the user that started watching this episode.
 | 
							/// The ID of the user that started watching this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid UserId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid UserId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The user that started watching this episode.
 | 
							/// The user that started watching this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public User User { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public User User { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the episode started.
 | 
							/// The ID of the episode started.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid? EpisodeId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid? EpisodeId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The <see cref="Episode"/> started.
 | 
							/// The <see cref="Episode"/> started.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Episode Episode { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Episode Episode { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc/>
 | 
							/// <inheritdoc/>
 | 
				
			||||||
		public DateTime AddedDate { get; set; }
 | 
							public DateTime AddedDate { get; set; }
 | 
				
			||||||
@ -162,22 +170,26 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the user that started watching this episode.
 | 
							/// The ID of the user that started watching this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid UserId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid UserId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The user that started watching this episode.
 | 
							/// The user that started watching this episode.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public User User { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public User User { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the show started.
 | 
							/// The ID of the show started.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid ShowId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid ShowId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The <see cref="Show"/> started.
 | 
							/// The <see cref="Show"/> started.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Show Show { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Show Show { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc/>
 | 
							/// <inheritdoc/>
 | 
				
			||||||
		public DateTime AddedDate { get; set; }
 | 
							public DateTime AddedDate { get; set; }
 | 
				
			||||||
@ -200,7 +212,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The ID of the episode started.
 | 
							/// The ID of the episode started.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		[SerializeIgnore] public Guid? NextEpisodeId { get; set; }
 | 
							[SerializeIgnore]
 | 
				
			||||||
 | 
							public Guid? NextEpisodeId { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// The next <see cref="Episode"/> to watch.
 | 
							/// The next <see cref="Episode"/> to watch.
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,8 @@ namespace Kyoo.Abstractions.Models
 | 
				
			|||||||
			string @this,
 | 
								string @this,
 | 
				
			||||||
			string? previous,
 | 
								string? previous,
 | 
				
			||||||
			string? next,
 | 
								string? next,
 | 
				
			||||||
			string first)
 | 
								string first
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			: base(result.Items, @this, previous, next, first)
 | 
								: base(result.Items, @this, previous, next, first)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Query = result.Query;
 | 
								Query = result.Query;
 | 
				
			||||||
 | 
				
			|||||||
@ -53,24 +53,30 @@ public abstract record Filter
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		return filters
 | 
							return filters
 | 
				
			||||||
			.Where(x => x != null)
 | 
								.Where(x => x != null)
 | 
				
			||||||
			.Aggregate((Filter<T>?)null, (acc, filter) =>
 | 
								.Aggregate(
 | 
				
			||||||
 | 
									(Filter<T>?)null,
 | 
				
			||||||
 | 
									(acc, filter) =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					if (acc == null)
 | 
										if (acc == null)
 | 
				
			||||||
						return filter;
 | 
											return filter;
 | 
				
			||||||
					return new Filter<T>.And(acc, filter!);
 | 
										return new Filter<T>.And(acc, filter!);
 | 
				
			||||||
			});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static Filter<T>? Or<T>(params Filter<T>?[] filters)
 | 
						public static Filter<T>? Or<T>(params Filter<T>?[] filters)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return filters
 | 
							return filters
 | 
				
			||||||
			.Where(x => x != null)
 | 
								.Where(x => x != null)
 | 
				
			||||||
			.Aggregate((Filter<T>?)null, (acc, filter) =>
 | 
								.Aggregate(
 | 
				
			||||||
 | 
									(Filter<T>?)null,
 | 
				
			||||||
 | 
									(acc, filter) =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					if (acc == null)
 | 
										if (acc == null)
 | 
				
			||||||
						return filter;
 | 
											return filter;
 | 
				
			||||||
					return new Filter<T>.Or(acc, filter!);
 | 
										return new Filter<T>.Or(acc, filter!);
 | 
				
			||||||
			});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,8 +115,8 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public static class FilterParsers
 | 
						public static class FilterParsers
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		public static readonly Parser<Filter<T>> Filter =
 | 
							public static readonly Parser<Filter<T>> Filter = Parse
 | 
				
			||||||
			Parse.Ref(() => Bracket)
 | 
								.Ref(() => Bracket)
 | 
				
			||||||
			.Or(Parse.Ref(() => Not))
 | 
								.Or(Parse.Ref(() => Not))
 | 
				
			||||||
			.Or(Parse.Ref(() => Eq))
 | 
								.Or(Parse.Ref(() => Eq))
 | 
				
			||||||
			.Or(Parse.Ref(() => Ne))
 | 
								.Or(Parse.Ref(() => Ne))
 | 
				
			||||||
@ -120,8 +126,8 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
			.Or(Parse.Ref(() => Le))
 | 
								.Or(Parse.Ref(() => Le))
 | 
				
			||||||
			.Or(Parse.Ref(() => Has));
 | 
								.Or(Parse.Ref(() => Has));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public static readonly Parser<Filter<T>> CompleteFilter =
 | 
							public static readonly Parser<Filter<T>> CompleteFilter = Parse
 | 
				
			||||||
			Parse.Ref(() => Or)
 | 
								.Ref(() => Or)
 | 
				
			||||||
			.Or(Parse.Ref(() => And))
 | 
								.Or(Parse.Ref(() => And))
 | 
				
			||||||
			.Or(Filter);
 | 
								.Or(Filter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,22 +137,30 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
			from close in Parse.Char(')').Token()
 | 
								from close in Parse.Char(')').Token()
 | 
				
			||||||
			select filter;
 | 
								select filter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public static readonly Parser<IEnumerable<char>> AndOperator = Parse.IgnoreCase("and")
 | 
							public static readonly Parser<IEnumerable<char>> AndOperator = Parse
 | 
				
			||||||
 | 
								.IgnoreCase("and")
 | 
				
			||||||
			.Or(Parse.String("&&"))
 | 
								.Or(Parse.String("&&"))
 | 
				
			||||||
			.Token();
 | 
								.Token();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public static readonly Parser<IEnumerable<char>> OrOperator = Parse.IgnoreCase("or")
 | 
							public static readonly Parser<IEnumerable<char>> OrOperator = Parse
 | 
				
			||||||
 | 
								.IgnoreCase("or")
 | 
				
			||||||
			.Or(Parse.String("||"))
 | 
								.Or(Parse.String("||"))
 | 
				
			||||||
			.Token();
 | 
								.Token();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public static readonly Parser<Filter<T>> And = Parse.ChainOperator(AndOperator, Filter, (_, a, b) => new And(a, b));
 | 
							public static readonly Parser<Filter<T>> And = Parse.ChainOperator(
 | 
				
			||||||
 | 
								AndOperator,
 | 
				
			||||||
 | 
								Filter,
 | 
				
			||||||
 | 
								(_, a, b) => new And(a, b)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public static readonly Parser<Filter<T>> Or = Parse.ChainOperator(OrOperator, And.Or(Filter), (_, a, b) => new Or(a, b));
 | 
							public static readonly Parser<Filter<T>> Or = Parse.ChainOperator(
 | 
				
			||||||
 | 
								OrOperator,
 | 
				
			||||||
 | 
								And.Or(Filter),
 | 
				
			||||||
 | 
								(_, a, b) => new Or(a, b)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public static readonly Parser<Filter<T>> Not =
 | 
							public static readonly Parser<Filter<T>> Not =
 | 
				
			||||||
			from not in Parse.IgnoreCase("not")
 | 
								from not in Parse.IgnoreCase("not").Or(Parse.String("!")).Token()
 | 
				
			||||||
				.Or(Parse.String("!"))
 | 
					 | 
				
			||||||
				.Token()
 | 
					 | 
				
			||||||
			from filter in CompleteFilter
 | 
								from filter in CompleteFilter
 | 
				
			||||||
			select new Not(filter);
 | 
								select new Not(filter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -155,9 +169,7 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
			Type? nullable = Nullable.GetUnderlyingType(type);
 | 
								Type? nullable = Nullable.GetUnderlyingType(type);
 | 
				
			||||||
			if (nullable != null)
 | 
								if (nullable != null)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return
 | 
									return from value in _GetValueParser(nullable) select value;
 | 
				
			||||||
					from value in _GetValueParser(nullable)
 | 
					 | 
				
			||||||
					select value;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (type == typeof(int))
 | 
								if (type == typeof(int))
 | 
				
			||||||
@ -165,8 +177,7 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			if (type == typeof(float))
 | 
								if (type == typeof(float))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return
 | 
									return from a in Parse.Number
 | 
				
			||||||
					from a in Parse.Number
 | 
					 | 
				
			||||||
					from dot in Parse.Char('.')
 | 
										from dot in Parse.Char('.')
 | 
				
			||||||
					from b in Parse.Number
 | 
										from b in Parse.Number
 | 
				
			||||||
					select float.Parse($"{a}.{b}") as object;
 | 
										select float.Parse($"{a}.{b}") as object;
 | 
				
			||||||
@ -174,8 +185,10 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			if (type == typeof(Guid))
 | 
								if (type == typeof(Guid))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return
 | 
									return from guid in Parse.Regex(
 | 
				
			||||||
					from guid in Parse.Regex(@"[({]?[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12}[})]?", "Guid")
 | 
											@"[({]?[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12}[})]?",
 | 
				
			||||||
 | 
											"Guid"
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
					select Guid.Parse(guid) as object;
 | 
										select Guid.Parse(guid) as object;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -191,7 +204,11 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			if (type.IsEnum)
 | 
								if (type.IsEnum)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return Parse.LetterOrDigit.Many().Text().Then(x =>
 | 
									return Parse
 | 
				
			||||||
 | 
										.LetterOrDigit
 | 
				
			||||||
 | 
										.Many()
 | 
				
			||||||
 | 
										.Text()
 | 
				
			||||||
 | 
										.Then(x =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						if (Enum.TryParse(type, x, true, out object? value))
 | 
											if (Enum.TryParse(type, x, true, out object? value))
 | 
				
			||||||
							return Parse.Return(value);
 | 
												return Parse.Return(value);
 | 
				
			||||||
@ -201,8 +218,7 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			if (type == typeof(DateTime))
 | 
								if (type == typeof(DateTime))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return
 | 
									return from year in Parse.Digit.Repeat(4).Text().Select(int.Parse)
 | 
				
			||||||
					from year in Parse.Digit.Repeat(4).Text().Select(int.Parse)
 | 
					 | 
				
			||||||
					from yd in Parse.Char('-')
 | 
										from yd in Parse.Char('-')
 | 
				
			||||||
					from mouth in Parse.Digit.Repeat(2).Text().Select(int.Parse)
 | 
										from mouth in Parse.Digit.Repeat(2).Text().Select(int.Parse)
 | 
				
			||||||
					from md in Parse.Char('-')
 | 
										from md in Parse.Char('-')
 | 
				
			||||||
@ -211,43 +227,57 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (typeof(IEnumerable).IsAssignableFrom(type))
 | 
								if (typeof(IEnumerable).IsAssignableFrom(type))
 | 
				
			||||||
				return ParseHelper.Error<object>("Can't filter a list with a default comparator, use the 'has' filter.");
 | 
									return ParseHelper.Error<object>(
 | 
				
			||||||
 | 
										"Can't filter a list with a default comparator, use the 'has' filter."
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			return ParseHelper.Error<object>("Unfilterable field found");
 | 
								return ParseHelper.Error<object>("Unfilterable field found");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private static Parser<Filter<T>> _GetOperationParser(
 | 
							private static Parser<Filter<T>> _GetOperationParser(
 | 
				
			||||||
			Parser<object> op,
 | 
								Parser<object> op,
 | 
				
			||||||
			Func<string, object, Filter<T>> apply,
 | 
								Func<string, object, Filter<T>> apply,
 | 
				
			||||||
			Func<Type, Parser<object?>>? customTypeParser = null)
 | 
								Func<Type, Parser<object?>>? customTypeParser = null
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Parser<string> property = Parse.LetterOrDigit.AtLeastOnce().Text();
 | 
								Parser<string> property = Parse.LetterOrDigit.AtLeastOnce().Text();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return property.Then(prop =>
 | 
								return property.Then(prop =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
									Type[] types =
 | 
				
			||||||
 | 
										typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (string.Equals(prop, "kind", StringComparison.OrdinalIgnoreCase))
 | 
									if (string.Equals(prop, "kind", StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					return
 | 
										return from eq in op
 | 
				
			||||||
						from eq in op
 | 
					 | 
				
			||||||
						from val in types
 | 
											from val in types
 | 
				
			||||||
							.Select(x => Parse.IgnoreCase(x.Name).Text())
 | 
												.Select(x => Parse.IgnoreCase(x.Name).Text())
 | 
				
			||||||
							.Aggregate(null as Parser<string>, (acc, x) => acc == null ? x : Parse.Or(acc, x))
 | 
												.Aggregate(
 | 
				
			||||||
 | 
													null as Parser<string>,
 | 
				
			||||||
 | 
													(acc, x) => acc == null ? x : Parse.Or(acc, x)
 | 
				
			||||||
 | 
												)
 | 
				
			||||||
						select apply("kind", val);
 | 
											select apply("kind", val);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				PropertyInfo? propInfo = types
 | 
									PropertyInfo? propInfo = types
 | 
				
			||||||
					.Select(x => x.GetProperty(prop, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance))
 | 
										.Select(
 | 
				
			||||||
 | 
											x =>
 | 
				
			||||||
 | 
												x.GetProperty(
 | 
				
			||||||
 | 
													prop,
 | 
				
			||||||
 | 
													BindingFlags.IgnoreCase
 | 
				
			||||||
 | 
														| BindingFlags.Public
 | 
				
			||||||
 | 
														| BindingFlags.Instance
 | 
				
			||||||
 | 
												)
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
					.FirstOrDefault();
 | 
										.FirstOrDefault();
 | 
				
			||||||
				if (propInfo == null)
 | 
									if (propInfo == null)
 | 
				
			||||||
					return ParseHelper.Error<Filter<T>>($"The given filter '{prop}' is invalid.");
 | 
										return ParseHelper.Error<Filter<T>>($"The given filter '{prop}' is invalid.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				Parser<object?> value = customTypeParser != null
 | 
									Parser<object?> value =
 | 
				
			||||||
 | 
										customTypeParser != null
 | 
				
			||||||
						? customTypeParser(propInfo.PropertyType)
 | 
											? customTypeParser(propInfo.PropertyType)
 | 
				
			||||||
						: _GetValueParser(propInfo.PropertyType);
 | 
											: _GetValueParser(propInfo.PropertyType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return
 | 
									return from eq in op
 | 
				
			||||||
					from eq in op
 | 
					 | 
				
			||||||
					from val in value
 | 
										from val in value
 | 
				
			||||||
					select apply(propInfo.Name, val);
 | 
										select apply(propInfo.Name, val);
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
@ -261,7 +291,10 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
				Type? inner = Nullable.GetUnderlyingType(type);
 | 
									Type? inner = Nullable.GetUnderlyingType(type);
 | 
				
			||||||
				if (inner == null)
 | 
									if (inner == null)
 | 
				
			||||||
					return _GetValueParser(type);
 | 
										return _GetValueParser(type);
 | 
				
			||||||
				return Parse.String("null").Token().Return((object?)null)
 | 
									return Parse
 | 
				
			||||||
 | 
										.String("null")
 | 
				
			||||||
 | 
										.Token()
 | 
				
			||||||
 | 
										.Return((object?)null)
 | 
				
			||||||
					.Or(_GetValueParser(inner));
 | 
										.Or(_GetValueParser(inner));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
@ -274,7 +307,10 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
				Type? inner = Nullable.GetUnderlyingType(type);
 | 
									Type? inner = Nullable.GetUnderlyingType(type);
 | 
				
			||||||
				if (inner == null)
 | 
									if (inner == null)
 | 
				
			||||||
					return _GetValueParser(type);
 | 
										return _GetValueParser(type);
 | 
				
			||||||
				return Parse.String("null").Token().Return((object?)null)
 | 
									return Parse
 | 
				
			||||||
 | 
										.String("null")
 | 
				
			||||||
 | 
										.Token()
 | 
				
			||||||
 | 
										.Return((object?)null)
 | 
				
			||||||
					.Or(_GetValueParser(inner));
 | 
										.Or(_GetValueParser(inner));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
@ -305,7 +341,9 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
			(Type type) =>
 | 
								(Type type) =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
 | 
									if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
 | 
				
			||||||
					return _GetValueParser(type.GetElementType() ?? type.GenericTypeArguments.First());
 | 
										return _GetValueParser(
 | 
				
			||||||
 | 
											type.GetElementType() ?? type.GenericTypeArguments.First()
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				return ParseHelper.Error<object>("Can't use 'has' on a non-list.");
 | 
									return ParseHelper.Error<object>("Can't use 'has' on a non-list.");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
@ -321,7 +359,9 @@ public abstract record Filter<T> : Filter
 | 
				
			|||||||
			IResult<Filter<T>> ret = FilterParsers.CompleteFilter.End().TryParse(filter);
 | 
								IResult<Filter<T>> ret = FilterParsers.CompleteFilter.End().TryParse(filter);
 | 
				
			||||||
			if (ret.WasSuccessful)
 | 
								if (ret.WasSuccessful)
 | 
				
			||||||
				return ret.Value;
 | 
									return ret.Value;
 | 
				
			||||||
			throw new ValidationException($"Could not parse filter argument: {ret.Message}. Not parsed: {filter[ret.Remainder.Position..]}");
 | 
								throw new ValidationException(
 | 
				
			||||||
 | 
									$"Could not parse filter argument: {ret.Message}. Not parsed: {filter[ret.Remainder.Position..]}"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		catch (ParseException ex)
 | 
							catch (ParseException ex)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
				
			|||||||
@ -82,9 +82,7 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
		/// </example>
 | 
							/// </example>
 | 
				
			||||||
		public T Match<T>(Func<Guid, T> idFunc, Func<string, T> slugFunc)
 | 
							public T Match<T>(Func<Guid, T> idFunc, Func<string, T> slugFunc)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return _id.HasValue
 | 
								return _id.HasValue ? idFunc(_id.Value) : slugFunc(_slug!);
 | 
				
			||||||
				? idFunc(_id.Value)
 | 
					 | 
				
			||||||
				: slugFunc(_slug!);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -99,12 +97,19 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
		/// identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)
 | 
							/// identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)
 | 
				
			||||||
		/// </code>
 | 
							/// </code>
 | 
				
			||||||
		/// </example>
 | 
							/// </example>
 | 
				
			||||||
		public Filter<T> Matcher<T>(Expression<Func<T, Guid>> idGetter,
 | 
							public Filter<T> Matcher<T>(
 | 
				
			||||||
			Expression<Func<T, string>> slugGetter)
 | 
								Expression<Func<T, Guid>> idGetter,
 | 
				
			||||||
 | 
								Expression<Func<T, string>> slugGetter
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
 | 
								ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
 | 
				
			||||||
			BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
 | 
								BinaryExpression equal = Expression.Equal(
 | 
				
			||||||
			ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
 | 
									_id.HasValue ? idGetter.Body : slugGetter.Body,
 | 
				
			||||||
 | 
									self
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								ICollection<ParameterExpression> parameters = _id.HasValue
 | 
				
			||||||
 | 
									? idGetter.Parameters
 | 
				
			||||||
 | 
									: slugGetter.Parameters;
 | 
				
			||||||
			Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
 | 
								Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
 | 
				
			||||||
			return new Filter<T>.Lambda(lambda);
 | 
								return new Filter<T>.Lambda(lambda);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -118,12 +123,19 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
		/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
 | 
							/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
 | 
				
			||||||
		/// <typeparam name="T">The type to match against this identifier.</typeparam>
 | 
							/// <typeparam name="T">The type to match against this identifier.</typeparam>
 | 
				
			||||||
		/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
 | 
							/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
 | 
				
			||||||
		public Filter<T> Matcher<T>(Expression<Func<T, Guid?>> idGetter,
 | 
							public Filter<T> Matcher<T>(
 | 
				
			||||||
			Expression<Func<T, string>> slugGetter)
 | 
								Expression<Func<T, Guid?>> idGetter,
 | 
				
			||||||
 | 
								Expression<Func<T, string>> slugGetter
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
 | 
								ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
 | 
				
			||||||
			BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
 | 
								BinaryExpression equal = Expression.Equal(
 | 
				
			||||||
			ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
 | 
									_id.HasValue ? idGetter.Body : slugGetter.Body,
 | 
				
			||||||
 | 
									self
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								ICollection<ParameterExpression> parameters = _id.HasValue
 | 
				
			||||||
 | 
									? idGetter.Parameters
 | 
				
			||||||
 | 
									: slugGetter.Parameters;
 | 
				
			||||||
			Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
 | 
								Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
 | 
				
			||||||
			return new Filter<T>.Lambda(lambda);
 | 
								return new Filter<T>.Lambda(lambda);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -137,10 +149,7 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
		/// </returns>
 | 
							/// </returns>
 | 
				
			||||||
		public bool IsSame(IResource resource)
 | 
							public bool IsSame(IResource resource)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return Match(
 | 
								return Match(id => resource.Id == id, slug => resource.Slug == slug);
 | 
				
			||||||
				id => resource.Id == id,
 | 
					 | 
				
			||||||
				slug => resource.Slug == slug
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -161,9 +170,7 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
		private Expression<Func<T, bool>> _IsSameExpression<T>()
 | 
							private Expression<Func<T, bool>> _IsSameExpression<T>()
 | 
				
			||||||
			where T : IResource
 | 
								where T : IResource
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return _id.HasValue
 | 
								return _id.HasValue ? x => x.Id == _id.Value : x => x.Slug == _slug;
 | 
				
			||||||
				? x => x.Id == _id.Value
 | 
					 | 
				
			||||||
				: x => x.Slug == _slug;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -181,17 +188,23 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
				.Where(x => x.Name == nameof(Enumerable.Any))
 | 
									.Where(x => x.Name == nameof(Enumerable.Any))
 | 
				
			||||||
				.FirstOrDefault(x => x.GetParameters().Length == 2)!
 | 
									.FirstOrDefault(x => x.GetParameters().Length == 2)!
 | 
				
			||||||
				.MakeGenericMethod(typeof(T2));
 | 
									.MakeGenericMethod(typeof(T2));
 | 
				
			||||||
			MethodCallExpression call = Expression.Call(null, method, listGetter.Body, _IsSameExpression<T2>());
 | 
								MethodCallExpression call = Expression.Call(
 | 
				
			||||||
			Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(call, listGetter.Parameters);
 | 
									null,
 | 
				
			||||||
 | 
									method,
 | 
				
			||||||
 | 
									listGetter.Body,
 | 
				
			||||||
 | 
									_IsSameExpression<T2>()
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(
 | 
				
			||||||
 | 
									call,
 | 
				
			||||||
 | 
									listGetter.Parameters
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			return new Filter<T>.Lambda(lambda);
 | 
								return new Filter<T>.Lambda(lambda);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override string ToString()
 | 
							public override string ToString()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return _id.HasValue
 | 
								return _id.HasValue ? _id.Value.ToString() : _slug!;
 | 
				
			||||||
				? _id.Value.ToString()
 | 
					 | 
				
			||||||
				: _slug!;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -208,15 +221,17 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/// <inheritdoc />
 | 
								/// <inheritdoc />
 | 
				
			||||||
			public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
 | 
								public override object ConvertFrom(
 | 
				
			||||||
 | 
									ITypeDescriptorContext? context,
 | 
				
			||||||
 | 
									CultureInfo? culture,
 | 
				
			||||||
 | 
									object value
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (value is Guid id)
 | 
									if (value is Guid id)
 | 
				
			||||||
					return new Identifier(id);
 | 
										return new Identifier(id);
 | 
				
			||||||
				if (value is not string slug)
 | 
									if (value is not string slug)
 | 
				
			||||||
					return base.ConvertFrom(context, culture, value)!;
 | 
										return base.ConvertFrom(context, culture, value)!;
 | 
				
			||||||
				return Guid.TryParse(slug, out id)
 | 
									return Guid.TryParse(slug, out id) ? new Identifier(id) : new Identifier(slug);
 | 
				
			||||||
					? new Identifier(id)
 | 
					 | 
				
			||||||
					: new Identifier(slug);
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -36,7 +36,8 @@ public class Include
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
 | 
						public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public record CustomRelation(string Name, Type type, string Sql, string? On, Type Declaring) : Metadata(Name);
 | 
						public record CustomRelation(string Name, Type type, string Sql, string? On, Type Declaring)
 | 
				
			||||||
 | 
							: Metadata(Name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public record ProjectedRelation(string Name, string Sql) : Metadata(Name);
 | 
						public record ProjectedRelation(string Name, string Sql) : Metadata(Name);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -57,11 +58,22 @@ public class Include<T> : Include
 | 
				
			|||||||
	public Include(params string[] fields)
 | 
						public Include(params string[] fields)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
							Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
				
			||||||
		Metadatas = fields.SelectMany(key =>
 | 
							Metadatas = fields
 | 
				
			||||||
 | 
								.SelectMany(key =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				var relations = types
 | 
									var relations = types
 | 
				
			||||||
				.Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)!)
 | 
										.Select(
 | 
				
			||||||
				.Select(prop => (prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!))
 | 
											x =>
 | 
				
			||||||
 | 
												x.GetProperty(
 | 
				
			||||||
 | 
													key,
 | 
				
			||||||
 | 
													BindingFlags.IgnoreCase
 | 
				
			||||||
 | 
														| BindingFlags.Public
 | 
				
			||||||
 | 
														| BindingFlags.Instance
 | 
				
			||||||
 | 
												)!
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
										.Select(
 | 
				
			||||||
 | 
											prop => (prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!)
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
					.Where(x => x.prop != null && x.attr != null)
 | 
										.Where(x => x.prop != null && x.attr != null)
 | 
				
			||||||
					.ToList();
 | 
										.ToList();
 | 
				
			||||||
				if (!relations.Any())
 | 
									if (!relations.Any())
 | 
				
			||||||
@ -72,15 +84,23 @@ public class Include<T> : Include
 | 
				
			|||||||
						(PropertyInfo prop, LoadableRelationAttribute attr) = x;
 | 
											(PropertyInfo prop, LoadableRelationAttribute attr) = x;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						if (attr.RelationID != null)
 | 
											if (attr.RelationID != null)
 | 
				
			||||||
						return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID) as Metadata;
 | 
												return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID)
 | 
				
			||||||
 | 
													as Metadata;
 | 
				
			||||||
						if (attr.Sql != null)
 | 
											if (attr.Sql != null)
 | 
				
			||||||
						return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!);
 | 
												return new CustomRelation(
 | 
				
			||||||
 | 
													prop.Name,
 | 
				
			||||||
 | 
													prop.PropertyType,
 | 
				
			||||||
 | 
													attr.Sql,
 | 
				
			||||||
 | 
													attr.On,
 | 
				
			||||||
 | 
													prop.DeclaringType!
 | 
				
			||||||
 | 
												);
 | 
				
			||||||
						if (attr.Projected != null)
 | 
											if (attr.Projected != null)
 | 
				
			||||||
							return new ProjectedRelation(prop.Name, attr.Projected);
 | 
												return new ProjectedRelation(prop.Name, attr.Projected);
 | 
				
			||||||
						throw new NotImplementedException();
 | 
											throw new NotImplementedException();
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
					.Distinct();
 | 
										.Distinct();
 | 
				
			||||||
		}).ToArray();
 | 
								})
 | 
				
			||||||
 | 
								.ToArray();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static Include<T> From(string? fields)
 | 
						public static Include<T> From(string? fields)
 | 
				
			||||||
 | 
				
			|||||||
@ -51,7 +51,10 @@ namespace Kyoo.Abstractions.Models.Utils
 | 
				
			|||||||
		public RequestError(string[] errors)
 | 
							public RequestError(string[] errors)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (errors == null || !errors.Any())
 | 
								if (errors == null || !errors.Any())
 | 
				
			||||||
				throw new ArgumentException("Errors must be non null and not empty", nameof(errors));
 | 
									throw new ArgumentException(
 | 
				
			||||||
 | 
										"Errors must be non null and not empty",
 | 
				
			||||||
 | 
										nameof(errors)
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			Errors = errors;
 | 
								Errors = errors;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -111,12 +111,22 @@ namespace Kyoo.Abstractions.Controllers
 | 
				
			|||||||
				"desc" => true,
 | 
									"desc" => true,
 | 
				
			||||||
				"asc" => false,
 | 
									"asc" => false,
 | 
				
			||||||
				null => false,
 | 
									null => false,
 | 
				
			||||||
				_ => throw new ValidationException($"The sort order, if set, should be :asc or :desc but it was :{order}.")
 | 
									_
 | 
				
			||||||
 | 
										=> throw new ValidationException(
 | 
				
			||||||
 | 
											$"The sort order, if set, should be :asc or :desc but it was :{order}."
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
								Type[] types =
 | 
				
			||||||
 | 
									typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
				
			||||||
			PropertyInfo? property = types
 | 
								PropertyInfo? property = types
 | 
				
			||||||
				.Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance))
 | 
									.Select(
 | 
				
			||||||
 | 
										x =>
 | 
				
			||||||
 | 
											x.GetProperty(
 | 
				
			||||||
 | 
												key,
 | 
				
			||||||
 | 
												BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
				.FirstOrDefault(x => x != null);
 | 
									.FirstOrDefault(x => x != null);
 | 
				
			||||||
			if (property == null)
 | 
								if (property == null)
 | 
				
			||||||
				throw new ValidationException("The given sort key is not valid.");
 | 
									throw new ValidationException("The given sort key is not valid.");
 | 
				
			||||||
 | 
				
			|||||||
@ -37,11 +37,15 @@ namespace Kyoo.Abstractions
 | 
				
			|||||||
		/// If your repository implements a special interface, please use <see cref="RegisterRepository{T,T2}"/>
 | 
							/// If your repository implements a special interface, please use <see cref="RegisterRepository{T,T2}"/>
 | 
				
			||||||
		/// </remarks>
 | 
							/// </remarks>
 | 
				
			||||||
		/// <returns>The initial container.</returns>
 | 
							/// <returns>The initial container.</returns>
 | 
				
			||||||
		public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
 | 
							public static IRegistrationBuilder<
 | 
				
			||||||
			RegisterRepository<T>(this ContainerBuilder builder)
 | 
								T,
 | 
				
			||||||
 | 
								ConcreteReflectionActivatorData,
 | 
				
			||||||
 | 
								SingleRegistrationStyle
 | 
				
			||||||
 | 
							> RegisterRepository<T>(this ContainerBuilder builder)
 | 
				
			||||||
			where T : IBaseRepository
 | 
								where T : IBaseRepository
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return builder.RegisterType<T>()
 | 
								return builder
 | 
				
			||||||
 | 
									.RegisterType<T>()
 | 
				
			||||||
				.AsSelf()
 | 
									.AsSelf()
 | 
				
			||||||
				.As<IBaseRepository>()
 | 
									.As<IBaseRepository>()
 | 
				
			||||||
				.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))!)
 | 
									.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))!)
 | 
				
			||||||
@ -58,8 +62,11 @@ namespace Kyoo.Abstractions
 | 
				
			|||||||
		/// If your repository does not implements a special interface, please use <see cref="RegisterRepository{T}"/>
 | 
							/// If your repository does not implements a special interface, please use <see cref="RegisterRepository{T}"/>
 | 
				
			||||||
		/// </remarks>
 | 
							/// </remarks>
 | 
				
			||||||
		/// <returns>The initial container.</returns>
 | 
							/// <returns>The initial container.</returns>
 | 
				
			||||||
		public static IRegistrationBuilder<T2, ConcreteReflectionActivatorData, SingleRegistrationStyle>
 | 
							public static IRegistrationBuilder<
 | 
				
			||||||
			RegisterRepository<T, T2>(this ContainerBuilder builder)
 | 
								T2,
 | 
				
			||||||
 | 
								ConcreteReflectionActivatorData,
 | 
				
			||||||
 | 
								SingleRegistrationStyle
 | 
				
			||||||
 | 
							> RegisterRepository<T, T2>(this ContainerBuilder builder)
 | 
				
			||||||
			where T : notnull
 | 
								where T : notnull
 | 
				
			||||||
			where T2 : IBaseRepository, T
 | 
								where T2 : IBaseRepository, T
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
				
			|||||||
@ -50,8 +50,7 @@ namespace Kyoo.Utils
 | 
				
			|||||||
				do
 | 
									do
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					yield return enumerator.Current;
 | 
										yield return enumerator.Current;
 | 
				
			||||||
				}
 | 
									} while (enumerator.MoveNext());
 | 
				
			||||||
				while (enumerator.MoveNext());
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return Generator(self, action);
 | 
								return Generator(self, action);
 | 
				
			||||||
 | 
				
			|||||||
@ -38,13 +38,14 @@ public sealed class ExpressionArgumentReplacer : ExpressionVisitor
 | 
				
			|||||||
		return base.VisitParameter(node);
 | 
							return base.VisitParameter(node);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static Expression ReplaceParams(Expression expression, IEnumerable<ParameterExpression> epxParams, params ParameterExpression[] param)
 | 
						public static Expression ReplaceParams(
 | 
				
			||||||
 | 
							Expression expression,
 | 
				
			||||||
 | 
							IEnumerable<ParameterExpression> epxParams,
 | 
				
			||||||
 | 
							params ParameterExpression[] param
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		ExpressionArgumentReplacer replacer = new(
 | 
							ExpressionArgumentReplacer replacer =
 | 
				
			||||||
			epxParams
 | 
								new(epxParams.Zip(param).ToDictionary(x => x.First, x => x.Second as Expression));
 | 
				
			||||||
				.Zip(param)
 | 
					 | 
				
			||||||
				.ToDictionary(x => x.First, x => x.Second as Expression)
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
		return replacer.Visit(expression);
 | 
							return replacer.Visit(expression);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -45,9 +45,11 @@ namespace Kyoo.Utils
 | 
				
			|||||||
		/// set to those of <paramref name="first"/>.
 | 
							/// set to those of <paramref name="first"/>.
 | 
				
			||||||
		/// </returns>
 | 
							/// </returns>
 | 
				
			||||||
		[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
 | 
							[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
 | 
				
			||||||
		public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(IDictionary<T, T2>? first,
 | 
							public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(
 | 
				
			||||||
 | 
								IDictionary<T, T2>? first,
 | 
				
			||||||
			IDictionary<T, T2>? second,
 | 
								IDictionary<T, T2>? second,
 | 
				
			||||||
			out bool hasChanged)
 | 
								out bool hasChanged
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (first == null)
 | 
								if (first == null)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -58,7 +60,9 @@ namespace Kyoo.Utils
 | 
				
			|||||||
			hasChanged = false;
 | 
								hasChanged = false;
 | 
				
			||||||
			if (second == null)
 | 
								if (second == null)
 | 
				
			||||||
				return first;
 | 
									return first;
 | 
				
			||||||
			hasChanged = second.Any(x => !first.ContainsKey(x.Key) || 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)
 | 
								foreach ((T key, T2 value) in first)
 | 
				
			||||||
				second.TryAdd(key, value);
 | 
									second.TryAdd(key, value);
 | 
				
			||||||
			return second;
 | 
								return second;
 | 
				
			||||||
@ -83,17 +87,22 @@ namespace Kyoo.Utils
 | 
				
			|||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <typeparam name="T">Fields of T will be completed</typeparam>
 | 
							/// <typeparam name="T">Fields of T will be completed</typeparam>
 | 
				
			||||||
		/// <returns><paramref name="first"/></returns>
 | 
							/// <returns><paramref name="first"/></returns>
 | 
				
			||||||
		public static T Complete<T>(T first,
 | 
							public static T Complete<T>(
 | 
				
			||||||
 | 
								T first,
 | 
				
			||||||
			T? second,
 | 
								T? second,
 | 
				
			||||||
			[InstantHandle] Func<PropertyInfo, bool>? where = null)
 | 
								[InstantHandle] Func<PropertyInfo, bool>? where = null
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (second == null)
 | 
								if (second == null)
 | 
				
			||||||
				return first;
 | 
									return first;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			Type type = typeof(T);
 | 
								Type type = typeof(T);
 | 
				
			||||||
			IEnumerable<PropertyInfo> properties = type.GetProperties()
 | 
								IEnumerable<PropertyInfo> properties = type.GetProperties()
 | 
				
			||||||
				.Where(x => x is { CanRead: true, CanWrite: true }
 | 
									.Where(
 | 
				
			||||||
					&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
 | 
										x =>
 | 
				
			||||||
 | 
											x is { CanRead: true, CanWrite: true }
 | 
				
			||||||
 | 
											&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (where != null)
 | 
								if (where != null)
 | 
				
			||||||
				properties = properties.Where(where);
 | 
									properties = properties.Where(where);
 | 
				
			||||||
@ -107,19 +116,16 @@ namespace Kyoo.Utils
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
 | 
									if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))!
 | 
										Type[] dictionaryTypes = Utility
 | 
				
			||||||
 | 
											.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))!
 | 
				
			||||||
						.GenericTypeArguments;
 | 
											.GenericTypeArguments;
 | 
				
			||||||
					object?[] parameters =
 | 
										object?[] parameters = { property.GetValue(first), value, false };
 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						property.GetValue(first),
 | 
					 | 
				
			||||||
						value,
 | 
					 | 
				
			||||||
						false
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
					object newDictionary = Utility.RunGenericMethod<object>(
 | 
										object newDictionary = Utility.RunGenericMethod<object>(
 | 
				
			||||||
						typeof(Merger),
 | 
											typeof(Merger),
 | 
				
			||||||
						nameof(CompleteDictionaries),
 | 
											nameof(CompleteDictionaries),
 | 
				
			||||||
						dictionaryTypes,
 | 
											dictionaryTypes,
 | 
				
			||||||
						parameters)!;
 | 
											parameters
 | 
				
			||||||
 | 
										)!;
 | 
				
			||||||
					if ((bool)parameters[2]!)
 | 
										if ((bool)parameters[2]!)
 | 
				
			||||||
						property.SetValue(first, newDictionary);
 | 
											property.SetValue(first, newDictionary);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
				
			|||||||
@ -60,13 +60,17 @@ namespace Kyoo.Utils
 | 
				
			|||||||
				{
 | 
									{
 | 
				
			||||||
					case UnicodeCategory.UppercaseLetter:
 | 
										case UnicodeCategory.UppercaseLetter:
 | 
				
			||||||
					case UnicodeCategory.TitlecaseLetter:
 | 
										case UnicodeCategory.TitlecaseLetter:
 | 
				
			||||||
						if (previousCategory == UnicodeCategory.SpaceSeparator ||
 | 
											if (
 | 
				
			||||||
							previousCategory == UnicodeCategory.LowercaseLetter ||
 | 
												previousCategory == UnicodeCategory.SpaceSeparator
 | 
				
			||||||
							(previousCategory != UnicodeCategory.DecimalDigitNumber &&
 | 
												|| previousCategory == UnicodeCategory.LowercaseLetter
 | 
				
			||||||
							previousCategory != null &&
 | 
												|| (
 | 
				
			||||||
							currentIndex > 0 &&
 | 
													previousCategory != UnicodeCategory.DecimalDigitNumber
 | 
				
			||||||
							currentIndex + 1 < name.Length &&
 | 
													&& previousCategory != null
 | 
				
			||||||
							char.IsLower(name[currentIndex + 1])))
 | 
													&& currentIndex > 0
 | 
				
			||||||
 | 
													&& currentIndex + 1 < name.Length
 | 
				
			||||||
 | 
													&& char.IsLower(name[currentIndex + 1])
 | 
				
			||||||
 | 
												)
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							builder.Append('_');
 | 
												builder.Append('_');
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
@ -105,7 +109,10 @@ namespace Kyoo.Utils
 | 
				
			|||||||
		public static bool IsPropertyExpression(LambdaExpression ex)
 | 
							public static bool IsPropertyExpression(LambdaExpression ex)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return ex.Body is MemberExpression
 | 
								return ex.Body is MemberExpression
 | 
				
			||||||
				|| (ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression);
 | 
									|| (
 | 
				
			||||||
 | 
										ex.Body.NodeType == ExpressionType.Convert
 | 
				
			||||||
 | 
										&& ((UnaryExpression)ex.Body).Operand is MemberExpression
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -118,7 +125,8 @@ namespace Kyoo.Utils
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			if (!IsPropertyExpression(ex))
 | 
								if (!IsPropertyExpression(ex))
 | 
				
			||||||
				throw new ArgumentException($"{ex} is not a property expression.");
 | 
									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
 | 
										? ((UnaryExpression)ex.Body).Operand as MemberExpression
 | 
				
			||||||
					: ex.Body as MemberExpression;
 | 
										: ex.Body as MemberExpression;
 | 
				
			||||||
			return member!.Member.Name;
 | 
								return member!.Member.Name;
 | 
				
			||||||
@ -175,7 +183,8 @@ namespace Kyoo.Utils
 | 
				
			|||||||
			IEnumerable<Type> types = genericType.IsInterface
 | 
								IEnumerable<Type> types = genericType.IsInterface
 | 
				
			||||||
				? type.GetInterfaces()
 | 
									? type.GetInterfaces()
 | 
				
			||||||
				: type.GetInheritanceTree();
 | 
									: type.GetInheritanceTree();
 | 
				
			||||||
			return types.Prepend(type)
 | 
								return types
 | 
				
			||||||
 | 
									.Prepend(type)
 | 
				
			||||||
				.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
 | 
									.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -195,8 +204,11 @@ namespace Kyoo.Utils
 | 
				
			|||||||
			IEnumerable<Type> types = genericType.IsInterface
 | 
								IEnumerable<Type> types = genericType.IsInterface
 | 
				
			||||||
				? type.GetInterfaces()
 | 
									? type.GetInterfaces()
 | 
				
			||||||
				: type.GetInheritanceTree();
 | 
									: type.GetInheritanceTree();
 | 
				
			||||||
			return types.Prepend(type)
 | 
								return types
 | 
				
			||||||
				.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
 | 
									.Prepend(type)
 | 
				
			||||||
 | 
									.FirstOrDefault(
 | 
				
			||||||
 | 
										x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -221,11 +233,13 @@ namespace Kyoo.Utils
 | 
				
			|||||||
		/// <exception cref="ArgumentException">No method match the given constraints.</exception>
 | 
							/// <exception cref="ArgumentException">No method match the given constraints.</exception>
 | 
				
			||||||
		/// <returns>The method handle of the matching method.</returns>
 | 
							/// <returns>The method handle of the matching method.</returns>
 | 
				
			||||||
		[PublicAPI]
 | 
							[PublicAPI]
 | 
				
			||||||
		public static MethodInfo GetMethod(Type type,
 | 
							public static MethodInfo GetMethod(
 | 
				
			||||||
 | 
								Type type,
 | 
				
			||||||
			BindingFlags flag,
 | 
								BindingFlags flag,
 | 
				
			||||||
			string name,
 | 
								string name,
 | 
				
			||||||
			Type[] generics,
 | 
								Type[] generics,
 | 
				
			||||||
			object?[] args)
 | 
								object?[] args
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
 | 
								MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
 | 
				
			||||||
				.Where(x => x.Name == name)
 | 
									.Where(x => x.Name == name)
 | 
				
			||||||
@ -233,9 +247,11 @@ namespace Kyoo.Utils
 | 
				
			|||||||
				.Where(x => x.GetParameters().Length == args.Length)
 | 
									.Where(x => x.GetParameters().Length == args.Length)
 | 
				
			||||||
				.IfEmpty(() =>
 | 
									.IfEmpty(() =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					throw new ArgumentException($"A method named {name} with " +
 | 
										throw new ArgumentException(
 | 
				
			||||||
						$"{args.Length} arguments and {generics.Length} generic " +
 | 
											$"A method named {name} with "
 | 
				
			||||||
						$"types could not be found on {type.Name}.");
 | 
												+ $"{args.Length} arguments and {generics.Length} generic "
 | 
				
			||||||
 | 
												+ $"types could not be found on {type.Name}."
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				// TODO this won't work but I don't know why.
 | 
									// TODO this won't work but I don't know why.
 | 
				
			||||||
				// .Where(x =>
 | 
									// .Where(x =>
 | 
				
			||||||
@ -257,7 +273,9 @@ namespace Kyoo.Utils
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			if (methods.Length == 1)
 | 
								if (methods.Length == 1)
 | 
				
			||||||
				return methods[0];
 | 
									return methods[0];
 | 
				
			||||||
			throw new ArgumentException($"Multiple methods named {name} match the generics and parameters constraints.");
 | 
								throw new ArgumentException(
 | 
				
			||||||
 | 
									$"Multiple methods named {name} match the generics and parameters constraints."
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -288,7 +306,8 @@ namespace Kyoo.Utils
 | 
				
			|||||||
			Type owner,
 | 
								Type owner,
 | 
				
			||||||
			string methodName,
 | 
								string methodName,
 | 
				
			||||||
			Type type,
 | 
								Type type,
 | 
				
			||||||
			params object[] args)
 | 
								params object[] args
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return RunGenericMethod<T>(owner, methodName, new[] { type }, args);
 | 
								return RunGenericMethod<T>(owner, methodName, new[] { type }, args);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -323,10 +342,13 @@ namespace Kyoo.Utils
 | 
				
			|||||||
			Type owner,
 | 
								Type owner,
 | 
				
			||||||
			string methodName,
 | 
								string methodName,
 | 
				
			||||||
			Type[] types,
 | 
								Type[] types,
 | 
				
			||||||
			params object?[] args)
 | 
								params object?[] args
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (types.Length < 1)
 | 
								if (types.Length < 1)
 | 
				
			||||||
				throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
 | 
									throw new ArgumentException(
 | 
				
			||||||
 | 
										$"The {nameof(types)} array is empty. At least one type is needed."
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
 | 
								MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
 | 
				
			||||||
			return (T?)method.MakeGenericMethod(types).Invoke(null, args);
 | 
								return (T?)method.MakeGenericMethod(types).Invoke(null, args);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -61,22 +61,29 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void Configure(IServiceCollection services)
 | 
							public void Configure(IServiceCollection services)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			string secret = _configuration.GetValue("AUTHENTICATION_SECRET", AuthenticationOption.DefaultSecret)!;
 | 
								string secret = _configuration.GetValue(
 | 
				
			||||||
			PermissionOption permissions = new()
 | 
									"AUTHENTICATION_SECRET",
 | 
				
			||||||
 | 
									AuthenticationOption.DefaultSecret
 | 
				
			||||||
 | 
								)!;
 | 
				
			||||||
 | 
								PermissionOption permissions =
 | 
				
			||||||
 | 
									new()
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
				Default = _configuration.GetValue("UNLOGGED_PERMISSIONS", "overall.read")!.Split(','),
 | 
										Default = _configuration
 | 
				
			||||||
				NewUser = _configuration.GetValue("DEFAULT_PERMISSIONS", "overall.read")!.Split(','),
 | 
											.GetValue("UNLOGGED_PERMISSIONS", "overall.read")!
 | 
				
			||||||
 | 
											.Split(','),
 | 
				
			||||||
 | 
										NewUser = _configuration
 | 
				
			||||||
 | 
											.GetValue("DEFAULT_PERMISSIONS", "overall.read")!
 | 
				
			||||||
 | 
											.Split(','),
 | 
				
			||||||
					ApiKeys = _configuration.GetValue("KYOO_APIKEYS", string.Empty)!.Split(','),
 | 
										ApiKeys = _configuration.GetValue("KYOO_APIKEYS", string.Empty)!.Split(','),
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
			services.AddSingleton(permissions);
 | 
								services.AddSingleton(permissions);
 | 
				
			||||||
			services.AddSingleton(new AuthenticationOption()
 | 
								services.AddSingleton(
 | 
				
			||||||
			{
 | 
									new AuthenticationOption() { Secret = secret, Permissions = permissions, }
 | 
				
			||||||
				Secret = secret,
 | 
								);
 | 
				
			||||||
				Permissions = permissions,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// TODO handle direct-videos with bearers (probably add a cookie and a app.Use to translate that for videos)
 | 
								// TODO handle direct-videos with bearers (probably add a cookie and a app.Use to translate that for videos)
 | 
				
			||||||
			services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
 | 
								services
 | 
				
			||||||
 | 
									.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
 | 
				
			||||||
				.AddJwtBearer(options =>
 | 
									.AddJwtBearer(options =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					options.TokenValidationParameters = new TokenValidationParameters
 | 
										options.TokenValidationParameters = new TokenValidationParameters
 | 
				
			||||||
@ -91,7 +98,8 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
 | 
							public IEnumerable<IStartupAction> ConfigureSteps =>
 | 
				
			||||||
 | 
								new IStartupAction[]
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication),
 | 
									SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication),
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
				
			|||||||
@ -57,13 +57,22 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public IFilterMetadata Create(PermissionAttribute attribute)
 | 
							public IFilterMetadata Create(PermissionAttribute attribute)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return new PermissionValidatorFilter(attribute.Type, attribute.Kind, attribute.Group, _options);
 | 
								return new PermissionValidatorFilter(
 | 
				
			||||||
 | 
									attribute.Type,
 | 
				
			||||||
 | 
									attribute.Kind,
 | 
				
			||||||
 | 
									attribute.Group,
 | 
				
			||||||
 | 
									_options
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public IFilterMetadata Create(PartialPermissionAttribute attribute)
 | 
							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>
 | 
							/// <summary>
 | 
				
			||||||
@ -102,7 +111,8 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
				string permission,
 | 
									string permission,
 | 
				
			||||||
				Kind kind,
 | 
									Kind kind,
 | 
				
			||||||
				Group group,
 | 
									Group group,
 | 
				
			||||||
				PermissionOption options)
 | 
									PermissionOption options
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				_permission = permission;
 | 
									_permission = permission;
 | 
				
			||||||
				_kind = kind;
 | 
									_kind = kind;
 | 
				
			||||||
@ -116,7 +126,11 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
			/// <param name="partialInfo">The partial permission to validate.</param>
 | 
								/// <param name="partialInfo">The partial permission to validate.</param>
 | 
				
			||||||
			/// <param name="group">The group of the permission.</param>
 | 
								/// <param name="group">The group of the permission.</param>
 | 
				
			||||||
			/// <param name="options">The option containing default values.</param>
 | 
								/// <param name="options">The option containing default values.</param>
 | 
				
			||||||
			public PermissionValidatorFilter(object partialInfo, Group? group, PermissionOption options)
 | 
								public PermissionValidatorFilter(
 | 
				
			||||||
 | 
									object partialInfo,
 | 
				
			||||||
 | 
									Group? group,
 | 
				
			||||||
 | 
									PermissionOption options
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				switch (partialInfo)
 | 
									switch (partialInfo)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
@ -127,7 +141,9 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
						_permission = perm;
 | 
											_permission = perm;
 | 
				
			||||||
						break;
 | 
											break;
 | 
				
			||||||
					default:
 | 
										default:
 | 
				
			||||||
						throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind.");
 | 
											throw new ArgumentException(
 | 
				
			||||||
 | 
												$"{nameof(partialInfo)} can only be a permission string or a kind."
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (group != null)
 | 
									if (group != null)
 | 
				
			||||||
@ -158,13 +174,17 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
							context.HttpContext.Items["PermissionType"] = permission;
 | 
												context.HttpContext.Items["PermissionType"] = permission;
 | 
				
			||||||
							return;
 | 
												return;
 | 
				
			||||||
						default:
 | 
											default:
 | 
				
			||||||
							throw new ArgumentException("Multiple non-matching partial permission attribute " +
 | 
												throw new ArgumentException(
 | 
				
			||||||
								"are not supported.");
 | 
													"Multiple non-matching partial permission attribute "
 | 
				
			||||||
 | 
														+ "are not supported."
 | 
				
			||||||
 | 
												);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					if (permission == null || kind == null)
 | 
										if (permission == null || kind == null)
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						throw new ArgumentException("The permission type or kind is still missing after two partial " +
 | 
											throw new ArgumentException(
 | 
				
			||||||
							"permission attributes, this is unsupported.");
 | 
												"The permission type or kind is still missing after two partial "
 | 
				
			||||||
 | 
													+ "permission attributes, this is unsupported."
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -178,25 +198,43 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
				{
 | 
									{
 | 
				
			||||||
					ICollection<string> permissions = res.Principal.GetPermissions();
 | 
										ICollection<string> permissions = res.Principal.GetPermissions();
 | 
				
			||||||
					if (permissions.All(x => x != permStr && x != overallStr))
 | 
										if (permissions.All(x => x != permStr && x != overallStr))
 | 
				
			||||||
						context.Result = _ErrorResult($"Missing permission {permStr} or {overallStr}", StatusCodes.Status403Forbidden);
 | 
											context.Result = _ErrorResult(
 | 
				
			||||||
 | 
												$"Missing permission {permStr} or {overallStr}",
 | 
				
			||||||
 | 
												StatusCodes.Status403Forbidden
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				else if (res.None)
 | 
									else if (res.None)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					ICollection<string> permissions = _options.Default ?? Array.Empty<string>();
 | 
										ICollection<string> permissions = _options.Default ?? Array.Empty<string>();
 | 
				
			||||||
					if (permissions.All(x => x != permStr && x != overallStr))
 | 
										if (permissions.All(x => x != permStr && x != overallStr))
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						context.Result = _ErrorResult($"Unlogged user does not have permission {permStr} or {overallStr}", StatusCodes.Status401Unauthorized);
 | 
											context.Result = _ErrorResult(
 | 
				
			||||||
 | 
												$"Unlogged user does not have permission {permStr} or {overallStr}",
 | 
				
			||||||
 | 
												StatusCodes.Status401Unauthorized
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				else if (res.Failure != null)
 | 
									else if (res.Failure != null)
 | 
				
			||||||
					context.Result = _ErrorResult(res.Failure.Message, StatusCodes.Status403Forbidden);
 | 
										context.Result = _ErrorResult(
 | 
				
			||||||
 | 
											res.Failure.Message,
 | 
				
			||||||
 | 
											StatusCodes.Status403Forbidden
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				else
 | 
									else
 | 
				
			||||||
					context.Result = _ErrorResult("Authentication panic", StatusCodes.Status500InternalServerError);
 | 
										context.Result = _ErrorResult(
 | 
				
			||||||
 | 
											"Authentication panic",
 | 
				
			||||||
 | 
											StatusCodes.Status500InternalServerError
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			private AuthenticateResult _ApiKeyCheck(ActionContext context)
 | 
								private AuthenticateResult _ApiKeyCheck(ActionContext context)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (!context.HttpContext.Request.Headers.TryGetValue("X-API-Key", out StringValues apiKey))
 | 
									if (
 | 
				
			||||||
 | 
										!context
 | 
				
			||||||
 | 
											.HttpContext
 | 
				
			||||||
 | 
											.Request
 | 
				
			||||||
 | 
											.Headers
 | 
				
			||||||
 | 
											.TryGetValue("X-API-Key", out StringValues apiKey)
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
					return AuthenticateResult.NoResult();
 | 
										return AuthenticateResult.NoResult();
 | 
				
			||||||
				if (!_options.ApiKeys.Contains<string>(apiKey!))
 | 
									if (!_options.ApiKeys.Contains<string>(apiKey!))
 | 
				
			||||||
					return AuthenticateResult.Fail("Invalid API-Key.");
 | 
										return AuthenticateResult.Fail("Invalid API-Key.");
 | 
				
			||||||
@ -205,11 +243,16 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
						new ClaimsPrincipal(
 | 
											new ClaimsPrincipal(
 | 
				
			||||||
							new[]
 | 
												new[]
 | 
				
			||||||
							{
 | 
												{
 | 
				
			||||||
								new ClaimsIdentity(new[]
 | 
													new ClaimsIdentity(
 | 
				
			||||||
 | 
														new[]
 | 
				
			||||||
									{
 | 
														{
 | 
				
			||||||
										// TODO: Make permission configurable, for now every APIKEY as all permissions.
 | 
															// TODO: Make permission configurable, for now every APIKEY as all permissions.
 | 
				
			||||||
									new Claim(Claims.Permissions, string.Join(',', PermissionOption.Admin))
 | 
															new Claim(
 | 
				
			||||||
								})
 | 
																Claims.Permissions,
 | 
				
			||||||
 | 
																string.Join(',', PermissionOption.Admin)
 | 
				
			||||||
 | 
															)
 | 
				
			||||||
 | 
														}
 | 
				
			||||||
 | 
													)
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						),
 | 
											),
 | 
				
			||||||
						"apikey"
 | 
											"apikey"
 | 
				
			||||||
@ -219,10 +262,14 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			private async Task<AuthenticateResult> _JwtCheck(ActionContext context)
 | 
								private async Task<AuthenticateResult> _JwtCheck(ActionContext context)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				AuthenticateResult ret = await context.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
 | 
									AuthenticateResult ret = await context
 | 
				
			||||||
 | 
										.HttpContext
 | 
				
			||||||
 | 
										.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
 | 
				
			||||||
				// Change the failure message to make the API nice to use.
 | 
									// Change the failure message to make the API nice to use.
 | 
				
			||||||
				if (ret.Failure != null)
 | 
									if (ret.Failure != null)
 | 
				
			||||||
					return AuthenticateResult.Fail("Invalid JWT token. The token may have expired.");
 | 
										return AuthenticateResult.Fail(
 | 
				
			||||||
 | 
											"Invalid JWT token. The token may have expired."
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				return ret;
 | 
									return ret;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -55,10 +55,10 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
 | 
								SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
 | 
				
			||||||
			SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
 | 
								SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
 | 
				
			||||||
			string permissions = user.Permissions != null
 | 
								string permissions =
 | 
				
			||||||
				? string.Join(',', user.Permissions)
 | 
									user.Permissions != null ? string.Join(',', user.Permissions) : string.Empty;
 | 
				
			||||||
				: string.Empty;
 | 
								List<Claim> claims =
 | 
				
			||||||
			List<Claim> claims = new()
 | 
									new()
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					new Claim(Claims.Id, user.Id.ToString()),
 | 
										new Claim(Claims.Id, user.Id.ToString()),
 | 
				
			||||||
					new Claim(Claims.Name, user.Username),
 | 
										new Claim(Claims.Name, user.Username),
 | 
				
			||||||
@ -67,7 +67,8 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
				};
 | 
									};
 | 
				
			||||||
			if (user.Email != null)
 | 
								if (user.Email != null)
 | 
				
			||||||
				claims.Add(new Claim(Claims.Email, user.Email));
 | 
									claims.Add(new Claim(Claims.Email, user.Email));
 | 
				
			||||||
			JwtSecurityToken token = new(
 | 
								JwtSecurityToken token =
 | 
				
			||||||
 | 
									new(
 | 
				
			||||||
					signingCredentials: credential,
 | 
										signingCredentials: credential,
 | 
				
			||||||
					claims: claims,
 | 
										claims: claims,
 | 
				
			||||||
					expires: DateTime.UtcNow.Add(expireIn)
 | 
										expires: DateTime.UtcNow.Add(expireIn)
 | 
				
			||||||
@ -80,7 +81,8 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
 | 
								SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
 | 
				
			||||||
			SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
 | 
								SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
 | 
				
			||||||
			JwtSecurityToken token = new(
 | 
								JwtSecurityToken token =
 | 
				
			||||||
 | 
									new(
 | 
				
			||||||
					signingCredentials: credential,
 | 
										signingCredentials: credential,
 | 
				
			||||||
					claims: new[]
 | 
										claims: new[]
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
@ -102,14 +104,18 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
			ClaimsPrincipal principal;
 | 
								ClaimsPrincipal principal;
 | 
				
			||||||
			try
 | 
								try
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				principal = tokenHandler.ValidateToken(refreshToken, new TokenValidationParameters
 | 
									principal = tokenHandler.ValidateToken(
 | 
				
			||||||
 | 
										refreshToken,
 | 
				
			||||||
 | 
										new TokenValidationParameters
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						ValidateIssuer = false,
 | 
											ValidateIssuer = false,
 | 
				
			||||||
						ValidateAudience = false,
 | 
											ValidateAudience = false,
 | 
				
			||||||
						ValidateIssuerSigningKey = true,
 | 
											ValidateIssuerSigningKey = true,
 | 
				
			||||||
						ValidateLifetime = true,
 | 
											ValidateLifetime = true,
 | 
				
			||||||
						IssuerSigningKey = key
 | 
											IssuerSigningKey = key
 | 
				
			||||||
				}, out SecurityToken _);
 | 
										},
 | 
				
			||||||
 | 
										out SecurityToken _
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			catch (Exception)
 | 
								catch (Exception)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -117,7 +123,9 @@ namespace Kyoo.Authentication
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (principal.Claims.First(x => x.Type == Claims.Type).Value != "refresh")
 | 
								if (principal.Claims.First(x => x.Type == Claims.Type).Value != "refresh")
 | 
				
			||||||
				throw new SecurityTokenException("Invalid token type. The token should be a refresh token.");
 | 
									throw new SecurityTokenException(
 | 
				
			||||||
 | 
										"Invalid token type. The token should be a refresh token."
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			Claim identifier = principal.Claims.First(x => x.Type == Claims.Id);
 | 
								Claim identifier = principal.Claims.First(x => x.Type == Claims.Id);
 | 
				
			||||||
			if (Guid.TryParse(identifier.Value, out Guid id))
 | 
								if (Guid.TryParse(identifier.Value, out Guid id))
 | 
				
			||||||
				return id;
 | 
									return id;
 | 
				
			||||||
 | 
				
			|||||||
@ -40,9 +40,12 @@ namespace Kyoo.Authentication.Models
 | 
				
			|||||||
			get
 | 
								get
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return Enum.GetNames<Group>()
 | 
									return Enum.GetNames<Group>()
 | 
				
			||||||
					.SelectMany(group => Enum.GetNames<Kind>()
 | 
										.SelectMany(
 | 
				
			||||||
 | 
											group =>
 | 
				
			||||||
 | 
												Enum.GetNames<Kind>()
 | 
				
			||||||
								.Select(kind => $"{group}.{kind}".ToLowerInvariant())
 | 
													.Select(kind => $"{group}.{kind}".ToLowerInvariant())
 | 
				
			||||||
					).ToArray();
 | 
										)
 | 
				
			||||||
 | 
										.ToArray();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -66,7 +66,11 @@ namespace Kyoo.Authentication.Views
 | 
				
			|||||||
		/// <param name="users">The repository used to check if the user exists.</param>
 | 
							/// <param name="users">The repository used to check if the user exists.</param>
 | 
				
			||||||
		/// <param name="token">The token generator.</param>
 | 
							/// <param name="token">The token generator.</param>
 | 
				
			||||||
		/// <param name="permissions">The permission opitons.</param>
 | 
							/// <param name="permissions">The permission opitons.</param>
 | 
				
			||||||
		public AuthApi(IRepository<User> users, ITokenController token, PermissionOption permissions)
 | 
							public AuthApi(
 | 
				
			||||||
 | 
								IRepository<User> users,
 | 
				
			||||||
 | 
								ITokenController token,
 | 
				
			||||||
 | 
								PermissionOption permissions
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_users = users;
 | 
								_users = users;
 | 
				
			||||||
			_token = token;
 | 
								_token = token;
 | 
				
			||||||
@ -97,7 +101,9 @@ namespace Kyoo.Authentication.Views
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
 | 
				
			||||||
		public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
 | 
							public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			User? user = await _users.GetOrDefault(new Filter<User>.Eq(nameof(Abstractions.Models.User.Username), request.Username));
 | 
								User? user = await _users.GetOrDefault(
 | 
				
			||||||
 | 
									new Filter<User>.Eq(nameof(Abstractions.Models.User.Username), request.Username)
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			if (user == null || !BCryptNet.Verify(request.Password, user.Password))
 | 
								if (user == null || !BCryptNet.Verify(request.Password, user.Password))
 | 
				
			||||||
				return Forbid(new RequestError("The user and password does not match."));
 | 
									return Forbid(new RequestError("The user and password does not match."));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -28,11 +28,13 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
	public class IdentifierRouteConstraint : IRouteConstraint
 | 
						public class IdentifierRouteConstraint : IRouteConstraint
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public bool Match(HttpContext? httpContext,
 | 
							public bool Match(
 | 
				
			||||||
 | 
								HttpContext? httpContext,
 | 
				
			||||||
			IRouter? route,
 | 
								IRouter? route,
 | 
				
			||||||
			string routeKey,
 | 
								string routeKey,
 | 
				
			||||||
			RouteValueDictionary values,
 | 
								RouteValueDictionary values,
 | 
				
			||||||
			RouteDirection routeDirection)
 | 
								RouteDirection routeDirection
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return values.ContainsKey(routeKey);
 | 
								return values.ContainsKey(routeKey);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			IRepository<Episode> episodeRepository,
 | 
								IRepository<Episode> episodeRepository,
 | 
				
			||||||
			IRepository<People> peopleRepository,
 | 
								IRepository<People> peopleRepository,
 | 
				
			||||||
			IRepository<Studio> studioRepository,
 | 
								IRepository<Studio> studioRepository,
 | 
				
			||||||
			IRepository<User> userRepository)
 | 
								IRepository<User> userRepository
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			LibraryItems = libraryItemRepository;
 | 
								LibraryItems = libraryItemRepository;
 | 
				
			||||||
			News = newsRepository;
 | 
								News = newsRepository;
 | 
				
			||||||
 | 
				
			|||||||
@ -50,7 +50,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task<ICollection<Collection>> Search(string query, Include<Collection>? include = default)
 | 
							public override async Task<ICollection<Collection>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<Collection>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await AddIncludes(_database.Collections, include)
 | 
								return await AddIncludes(_database.Collections, include)
 | 
				
			||||||
				.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
 | 
									.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
 | 
				
			||||||
 | 
				
			|||||||
@ -65,22 +65,33 @@ public static class DapperHelper
 | 
				
			|||||||
			.Where(x => !x.Key.StartsWith('_'))
 | 
								.Where(x => !x.Key.StartsWith('_'))
 | 
				
			||||||
			// If first char is lower, assume manual sql instead of reflection.
 | 
								// If first char is lower, assume manual sql instead of reflection.
 | 
				
			||||||
			.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
 | 
								.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
 | 
				
			||||||
			.Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}")
 | 
								.Select(
 | 
				
			||||||
 | 
									x =>
 | 
				
			||||||
 | 
										$"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			.ToArray();
 | 
								.ToArray();
 | 
				
			||||||
		if (keys.Length == 1)
 | 
							if (keys.Length == 1)
 | 
				
			||||||
			return keys.First();
 | 
								return keys.First();
 | 
				
			||||||
		return $"coalesce({string.Join(", ", keys)})";
 | 
							return $"coalesce({string.Join(", ", keys)})";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static string ProcessSort<T>(Sort<T> sort, bool reverse, Dictionary<string, Type> config, bool recurse = false)
 | 
						public static string ProcessSort<T>(
 | 
				
			||||||
 | 
							Sort<T> sort,
 | 
				
			||||||
 | 
							bool reverse,
 | 
				
			||||||
 | 
							Dictionary<string, Type> config,
 | 
				
			||||||
 | 
							bool recurse = false
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
		where T : IQuery
 | 
							where T : IQuery
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		string ret = sort switch
 | 
							string ret = sort switch
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true),
 | 
								Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true),
 | 
				
			||||||
			Sort<T>.By(string key, bool desc) => $"{Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}",
 | 
								Sort<T>.By(string key, bool desc)
 | 
				
			||||||
			Sort<T>.Random(var seed) => $"md5('{seed}' || {Property("id", config)}) {(reverse ? "desc" : "asc")}",
 | 
									=> $"{Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}",
 | 
				
			||||||
			Sort<T>.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))),
 | 
								Sort<T>.Random(var seed)
 | 
				
			||||||
 | 
									=> $"md5('{seed}' || {Property("id", config)}) {(reverse ? "desc" : "asc")}",
 | 
				
			||||||
 | 
								Sort<T>.Conglomerate(var list)
 | 
				
			||||||
 | 
									=> string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))),
 | 
				
			||||||
			_ => throw new SwitchExpressionException(),
 | 
								_ => throw new SwitchExpressionException(),
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		if (recurse)
 | 
							if (recurse)
 | 
				
			||||||
@ -108,10 +119,14 @@ public static class DapperHelper
 | 
				
			|||||||
			switch (metadata)
 | 
								switch (metadata)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				case Include.SingleRelation(var name, var type, var rid):
 | 
									case Include.SingleRelation(var name, var type, var rid):
 | 
				
			||||||
					string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s";
 | 
										string tableName =
 | 
				
			||||||
 | 
											type.GetCustomAttribute<TableAttribute>()?.Name
 | 
				
			||||||
 | 
											?? $"{type.Name.ToSnakeCase()}s";
 | 
				
			||||||
					types.Add(type);
 | 
										types.Add(type);
 | 
				
			||||||
					projection.AppendLine($", r{relation}.* -- {type.Name} as r{relation}");
 | 
										projection.AppendLine($", r{relation}.* -- {type.Name} as r{relation}");
 | 
				
			||||||
					join.Append($"\nleft join {tableName} as r{relation} on r{relation}.id = {Property(rid, config)}");
 | 
										join.Append(
 | 
				
			||||||
 | 
											$"\nleft join {tableName} as r{relation} on r{relation}.id = {Property(rid, config)}"
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case Include.CustomRelation(var name, var type, var sql, var on, var declaring):
 | 
									case Include.CustomRelation(var name, var type, var sql, var on, var declaring):
 | 
				
			||||||
					string owner = config.First(x => x.Value == declaring).Key;
 | 
										string owner = config.First(x => x.Value == declaring).Key;
 | 
				
			||||||
@ -133,7 +148,8 @@ public static class DapperHelper
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		T Map(T item, IEnumerable<object?> relations)
 | 
							T Map(T item, IEnumerable<object?> relations)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			IEnumerable<string> metadatas = include.Metadatas
 | 
								IEnumerable<string> metadatas = include
 | 
				
			||||||
 | 
									.Metadatas
 | 
				
			||||||
				.Where(x => x is not Include.ProjectedRelation)
 | 
									.Where(x => x is not Include.ProjectedRelation)
 | 
				
			||||||
				.Select(x => x.Name);
 | 
									.Select(x => x.Name);
 | 
				
			||||||
			foreach ((string name, object? value) in metadatas.Zip(relations))
 | 
								foreach ((string name, object? value) in metadatas.Zip(relations))
 | 
				
			||||||
@ -150,15 +166,23 @@ public static class DapperHelper
 | 
				
			|||||||
		return (projection.ToString(), join.ToString(), types, Map);
 | 
							return (projection.ToString(), join.ToString(), types, Map);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static FormattableString ProcessFilter<T>(Filter<T> filter, Dictionary<string, Type> config)
 | 
						public static FormattableString ProcessFilter<T>(
 | 
				
			||||||
 | 
							Filter<T> filter,
 | 
				
			||||||
 | 
							Dictionary<string, Type> config
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		FormattableString Format(string key, FormattableString op)
 | 
							FormattableString Format(string key, FormattableString op)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (key == "kind")
 | 
								if (key == "kind")
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				string cases = string.Join('\n', config
 | 
									string cases = string.Join(
 | 
				
			||||||
 | 
										'\n',
 | 
				
			||||||
 | 
										config
 | 
				
			||||||
						.Skip(1)
 | 
											.Skip(1)
 | 
				
			||||||
					.Select(x => $"when {x.Key}.id is not null then '{x.Value.Name.ToLowerInvariant()}'")
 | 
											.Select(
 | 
				
			||||||
 | 
												x =>
 | 
				
			||||||
 | 
													$"when {x.Key}.id is not null then '{x.Value.Name.ToLowerInvariant()}'"
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
				return $"""
 | 
									return $"""
 | 
				
			||||||
					case
 | 
										case
 | 
				
			||||||
@ -172,7 +196,10 @@ public static class DapperHelper
 | 
				
			|||||||
				.Where(x => !x.Key.StartsWith('_'))
 | 
									.Where(x => !x.Key.StartsWith('_'))
 | 
				
			||||||
				// If first char is lower, assume manual sql instead of reflection.
 | 
									// If first char is lower, assume manual sql instead of reflection.
 | 
				
			||||||
				.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
 | 
									.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
 | 
				
			||||||
				.Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}");
 | 
									.Select(
 | 
				
			||||||
 | 
										x =>
 | 
				
			||||||
 | 
											$"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			FormattableString ret = $"{properties.First():raw} {op}";
 | 
								FormattableString ret = $"{properties.First():raw} {op}";
 | 
				
			||||||
			foreach (string property in properties.Skip(1))
 | 
								foreach (string property in properties.Skip(1))
 | 
				
			||||||
@ -194,16 +221,20 @@ public static class DapperHelper
 | 
				
			|||||||
				Filter<T>.And(var first, var second) => $"({Process(first)} and {Process(second)})",
 | 
									Filter<T>.And(var first, var second) => $"({Process(first)} and {Process(second)})",
 | 
				
			||||||
				Filter<T>.Or(var first, var second) => $"({Process(first)} or {Process(second)})",
 | 
									Filter<T>.Or(var first, var second) => $"({Process(first)} or {Process(second)})",
 | 
				
			||||||
				Filter<T>.Not(var inner) => $"(not {Process(inner)})",
 | 
									Filter<T>.Not(var inner) => $"(not {Process(inner)})",
 | 
				
			||||||
				Filter<T>.Eq(var property, var value) when value is null => Format(property, $"is null"),
 | 
									Filter<T>.Eq(var property, var value) when value is null
 | 
				
			||||||
				Filter<T>.Ne(var property, var value) when value is null => Format(property, $"is not null"),
 | 
										=> Format(property, $"is null"),
 | 
				
			||||||
 | 
									Filter<T>.Ne(var property, var value) when value is null
 | 
				
			||||||
 | 
										=> Format(property, $"is not null"),
 | 
				
			||||||
				Filter<T>.Eq(var property, var value) => Format(property, $"= {P(value!)}"),
 | 
									Filter<T>.Eq(var property, var value) => Format(property, $"= {P(value!)}"),
 | 
				
			||||||
				Filter<T>.Ne(var property, var value) => Format(property, $"!= {P(value!)}"),
 | 
									Filter<T>.Ne(var property, var value) => Format(property, $"!= {P(value!)}"),
 | 
				
			||||||
				Filter<T>.Gt(var property, var value) => Format(property, $"> {P(value)}"),
 | 
									Filter<T>.Gt(var property, var value) => Format(property, $"> {P(value)}"),
 | 
				
			||||||
				Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"),
 | 
									Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"),
 | 
				
			||||||
				Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"),
 | 
									Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"),
 | 
				
			||||||
				Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"),
 | 
									Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"),
 | 
				
			||||||
				Filter<T>.Has(var property, var value) => $"{P(value)} = any({Property(property, config):raw})",
 | 
									Filter<T>.Has(var property, var value)
 | 
				
			||||||
				Filter<T>.CmpRandom(var op, var seed, var id) => $"md5({seed} || coalesce({string.Join(", ", config.Select(x => $"{x.Key}.id")):raw})) {op:raw} md5({seed} || {id.ToString()})",
 | 
										=> $"{P(value)} = any({Property(property, config):raw})",
 | 
				
			||||||
 | 
									Filter<T>.CmpRandom(var op, var seed, var id)
 | 
				
			||||||
 | 
										=> $"md5({seed} || coalesce({string.Join(", ", config.Select(x => $"{x.Key}.id")):raw})) {op:raw} md5({seed} || {id.ToString()})",
 | 
				
			||||||
				Filter<T>.Lambda(var lambda) => throw new NotSupportedException(),
 | 
									Filter<T>.Lambda(var lambda) => throw new NotSupportedException(),
 | 
				
			||||||
				_ => throw new NotImplementedException(),
 | 
									_ => throw new NotImplementedException(),
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
@ -213,7 +244,8 @@ public static class DapperHelper
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public static string ExpendProjections(Type type, string? prefix, Include include)
 | 
						public static string ExpendProjections(Type type, string? prefix, Include include)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		IEnumerable<string> projections = include.Metadatas
 | 
							IEnumerable<string> projections = include
 | 
				
			||||||
 | 
								.Metadatas
 | 
				
			||||||
			.Select(x => (x as Include.ProjectedRelation)!)
 | 
								.Select(x => (x as Include.ProjectedRelation)!)
 | 
				
			||||||
			.Where(x => x != null)
 | 
								.Where(x => x != null)
 | 
				
			||||||
			.Where(x => type.GetProperty(x.Name) != null)
 | 
								.Where(x => type.GetProperty(x.Name) != null)
 | 
				
			||||||
@ -231,26 +263,36 @@ public static class DapperHelper
 | 
				
			|||||||
		Include<T>? include,
 | 
							Include<T>? include,
 | 
				
			||||||
		Filter<T>? filter,
 | 
							Filter<T>? filter,
 | 
				
			||||||
		Sort<T>? sort,
 | 
							Sort<T>? sort,
 | 
				
			||||||
		Pagination? limit)
 | 
							Pagination? limit
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
		where T : class, IResource, IQuery
 | 
							where T : class, IResource, IQuery
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		SqlBuilder query = new(db, command);
 | 
							SqlBuilder query = new(db, command);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Include handling
 | 
							// Include handling
 | 
				
			||||||
		include ??= new();
 | 
							include ??= new();
 | 
				
			||||||
		var (includeProjection, includeJoin, includeTypes, mapIncludes) = ProcessInclude(include, config);
 | 
							var (includeProjection, includeJoin, includeTypes, mapIncludes) = ProcessInclude(
 | 
				
			||||||
 | 
								include,
 | 
				
			||||||
 | 
								config
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
		query.Replace("/* includesJoin */", $"{includeJoin:raw}", out bool replaced);
 | 
							query.Replace("/* includesJoin */", $"{includeJoin:raw}", out bool replaced);
 | 
				
			||||||
		if (!replaced)
 | 
							if (!replaced)
 | 
				
			||||||
			query.AppendLiteral(includeJoin);
 | 
								query.AppendLiteral(includeJoin);
 | 
				
			||||||
		query.Replace("/* includes */", $"{includeProjection:raw}", out replaced);
 | 
							query.Replace("/* includes */", $"{includeProjection:raw}", out replaced);
 | 
				
			||||||
		if (!replaced)
 | 
							if (!replaced)
 | 
				
			||||||
			throw new ArgumentException("Missing '/* includes */' placeholder in top level sql select to support includes.");
 | 
								throw new ArgumentException(
 | 
				
			||||||
 | 
									"Missing '/* includes */' placeholder in top level sql select to support includes."
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Handle pagination, orders and filter.
 | 
							// Handle pagination, orders and filter.
 | 
				
			||||||
		if (limit?.AfterID != null)
 | 
							if (limit?.AfterID != null)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			T reference = await get(limit.AfterID.Value);
 | 
								T reference = await get(limit.AfterID.Value);
 | 
				
			||||||
			Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse);
 | 
								Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(
 | 
				
			||||||
 | 
									sort,
 | 
				
			||||||
 | 
									reference,
 | 
				
			||||||
 | 
									!limit.Reverse
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			filter = Filter.And(filter, keysetFilter);
 | 
								filter = Filter.And(filter, keysetFilter);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (filter != null)
 | 
							if (filter != null)
 | 
				
			||||||
@ -273,7 +315,10 @@ public static class DapperHelper
 | 
				
			|||||||
		List<Type> types = config.Select(x => x.Value).Concat(includeTypes).ToList();
 | 
							List<Type> types = config.Select(x => x.Value).Concat(includeTypes).ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Expand projections on every types received.
 | 
							// Expand projections on every types received.
 | 
				
			||||||
		sql = Regex.Replace(sql, @"(,?) -- (\w+)( as (\w+))?", (match) =>
 | 
							sql = Regex.Replace(
 | 
				
			||||||
 | 
								sql,
 | 
				
			||||||
 | 
								@"(,?) -- (\w+)( as (\w+))?",
 | 
				
			||||||
 | 
								(match) =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				string leadingComa = match.Groups[1].Value;
 | 
									string leadingComa = match.Groups[1].Value;
 | 
				
			||||||
				string type = match.Groups[2].Value;
 | 
									string type = match.Groups[2].Value;
 | 
				
			||||||
@ -289,18 +334,26 @@ public static class DapperHelper
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				if (typeV.IsAssignableTo(typeof(IThumbnails)))
 | 
									if (typeV.IsAssignableTo(typeof(IThumbnails)))
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
				string posterProj = string.Join(", ", new[] { "poster", "thumbnail", "logo" }
 | 
										string posterProj = string.Join(
 | 
				
			||||||
					.Select(x => $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash"));
 | 
											", ",
 | 
				
			||||||
 | 
											new[] { "poster", "thumbnail", "logo" }.Select(
 | 
				
			||||||
 | 
												x => $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash"
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					projection = string.IsNullOrEmpty(projection)
 | 
										projection = string.IsNullOrEmpty(projection)
 | 
				
			||||||
						? posterProj
 | 
											? posterProj
 | 
				
			||||||
						: $"{projection}, {posterProj}";
 | 
											: $"{projection}, {posterProj}";
 | 
				
			||||||
				types.InsertRange(types.IndexOf(typeV) + 1, Enumerable.Repeat(typeof(Image), 3));
 | 
										types.InsertRange(
 | 
				
			||||||
 | 
											types.IndexOf(typeV) + 1,
 | 
				
			||||||
 | 
											Enumerable.Repeat(typeof(Image), 3)
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (string.IsNullOrEmpty(projection))
 | 
									if (string.IsNullOrEmpty(projection))
 | 
				
			||||||
					return leadingComa;
 | 
										return leadingComa;
 | 
				
			||||||
				return $", {projection}{leadingComa}";
 | 
									return $", {projection}{leadingComa}";
 | 
				
			||||||
		});
 | 
								}
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		IEnumerable<T> data = await db.QueryAsync<T>(
 | 
							IEnumerable<T> data = await db.QueryAsync<T>(
 | 
				
			||||||
			sql,
 | 
								sql,
 | 
				
			||||||
@ -322,7 +375,10 @@ public static class DapperHelper
 | 
				
			|||||||
				return mapIncludes(mapper(nItems), nItems.Skip(config.Count));
 | 
									return mapIncludes(mapper(nItems), nItems.Skip(config.Count));
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			ParametersDictionary.LoadFrom(cmd),
 | 
								ParametersDictionary.LoadFrom(cmd),
 | 
				
			||||||
			splitOn: string.Join(',', types.Select(x => x.GetCustomAttribute<SqlFirstColumnAttribute>()?.Name ?? "id"))
 | 
								splitOn: string.Join(
 | 
				
			||||||
 | 
									',',
 | 
				
			||||||
 | 
									types.Select(x => x.GetCustomAttribute<SqlFirstColumnAttribute>()?.Name ?? "id")
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
		if (limit?.Reverse == true)
 | 
							if (limit?.Reverse == true)
 | 
				
			||||||
			data = data.Reverse();
 | 
								data = data.Reverse();
 | 
				
			||||||
@ -339,7 +395,8 @@ public static class DapperHelper
 | 
				
			|||||||
		Filter<T>? filter,
 | 
							Filter<T>? filter,
 | 
				
			||||||
		Sort<T>? sort = null,
 | 
							Sort<T>? sort = null,
 | 
				
			||||||
		bool reverse = false,
 | 
							bool reverse = false,
 | 
				
			||||||
		Guid? afterId = default)
 | 
							Guid? afterId = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
		where T : class, IResource, IQuery
 | 
							where T : class, IResource, IQuery
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		ICollection<T> ret = await db.Query<T>(
 | 
							ICollection<T> ret = await db.Query<T>(
 | 
				
			||||||
@ -361,7 +418,8 @@ public static class DapperHelper
 | 
				
			|||||||
		FormattableString command,
 | 
							FormattableString command,
 | 
				
			||||||
		Dictionary<string, Type> config,
 | 
							Dictionary<string, Type> config,
 | 
				
			||||||
		SqlVariableContext context,
 | 
							SqlVariableContext context,
 | 
				
			||||||
		Filter<T>? filter)
 | 
							Filter<T>? filter
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
		where T : class, IResource
 | 
							where T : class, IResource
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		SqlBuilder query = new(db, command);
 | 
							SqlBuilder query = new(db, command);
 | 
				
			||||||
@ -374,10 +432,7 @@ public static class DapperHelper
 | 
				
			|||||||
		// language=postgreSQL
 | 
							// language=postgreSQL
 | 
				
			||||||
		string sql = $"select count(*) from ({cmd.Sql}) as query";
 | 
							string sql = $"select count(*) from ({cmd.Sql}) as query";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return await db.QuerySingleAsync<int>(
 | 
							return await db.QuerySingleAsync<int>(sql, ParametersDictionary.LoadFrom(cmd));
 | 
				
			||||||
			sql,
 | 
					 | 
				
			||||||
			ParametersDictionary.LoadFrom(cmd)
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -43,7 +43,6 @@ public abstract class DapperRepository<T> : IRepository<T>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	protected SqlVariableContext Context { get; init; }
 | 
						protected SqlVariableContext Context { get; init; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
	public DapperRepository(DbConnection database, SqlVariableContext context)
 | 
						public DapperRepository(DbConnection database, SqlVariableContext context)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		Database = database;
 | 
							Database = database;
 | 
				
			||||||
@ -69,11 +68,13 @@ public abstract class DapperRepository<T> : IRepository<T>
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc/>
 | 
						/// <inheritdoc/>
 | 
				
			||||||
	public virtual async Task<T> Get(Filter<T>? filter,
 | 
						public virtual async Task<T> Get(
 | 
				
			||||||
 | 
							Filter<T>? filter,
 | 
				
			||||||
		Include<T>? include = default,
 | 
							Include<T>? include = default,
 | 
				
			||||||
		Sort<T>? sortBy = default,
 | 
							Sort<T>? sortBy = default,
 | 
				
			||||||
		bool reverse = false,
 | 
							bool reverse = false,
 | 
				
			||||||
		Guid? afterId = default)
 | 
							Guid? afterId = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId);
 | 
							T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId);
 | 
				
			||||||
		if (ret == null)
 | 
							if (ret == null)
 | 
				
			||||||
@ -84,7 +85,8 @@ public abstract class DapperRepository<T> : IRepository<T>
 | 
				
			|||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public async Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = null)
 | 
						public async Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = null)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return (await Database.Query<T>(
 | 
							return (
 | 
				
			||||||
 | 
								await Database.Query<T>(
 | 
				
			||||||
				Sql,
 | 
									Sql,
 | 
				
			||||||
				Config,
 | 
									Config,
 | 
				
			||||||
				Mapper,
 | 
									Mapper,
 | 
				
			||||||
@ -94,7 +96,8 @@ public abstract class DapperRepository<T> : IRepository<T>
 | 
				
			|||||||
				Filter.Or(ids.Select(x => new Filter<T>.Eq("id", x)).ToArray()),
 | 
									Filter.Or(ids.Select(x => new Filter<T>.Eq("id", x)).ToArray()),
 | 
				
			||||||
				sort: null,
 | 
									sort: null,
 | 
				
			||||||
				limit: null
 | 
									limit: null
 | 
				
			||||||
			))
 | 
								)
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			.OrderBy(x => ids.IndexOf(x.Id))
 | 
								.OrderBy(x => ids.IndexOf(x.Id))
 | 
				
			||||||
			.ToList();
 | 
								.ToList();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -138,11 +141,13 @@ public abstract class DapperRepository<T> : IRepository<T>
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public virtual Task<T?> GetOrDefault(Filter<T>? filter,
 | 
						public virtual Task<T?> GetOrDefault(
 | 
				
			||||||
 | 
							Filter<T>? filter,
 | 
				
			||||||
		Include<T>? include = default,
 | 
							Include<T>? include = default,
 | 
				
			||||||
		Sort<T>? sortBy = default,
 | 
							Sort<T>? sortBy = default,
 | 
				
			||||||
		bool reverse = false,
 | 
							bool reverse = false,
 | 
				
			||||||
		Guid? afterId = default)
 | 
							Guid? afterId = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return Database.QuerySingle<T>(
 | 
							return Database.QuerySingle<T>(
 | 
				
			||||||
			Sql,
 | 
								Sql,
 | 
				
			||||||
@ -158,10 +163,12 @@ public abstract class DapperRepository<T> : IRepository<T>
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<ICollection<T>> GetAll(Filter<T>? filter = default,
 | 
						public Task<ICollection<T>> GetAll(
 | 
				
			||||||
 | 
							Filter<T>? filter = default,
 | 
				
			||||||
		Sort<T>? sort = default,
 | 
							Sort<T>? sort = default,
 | 
				
			||||||
		Include<T>? include = default,
 | 
							Include<T>? include = default,
 | 
				
			||||||
		Pagination? limit = default)
 | 
							Pagination? limit = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return Database.Query<T>(
 | 
							return Database.Query<T>(
 | 
				
			||||||
			Sql,
 | 
								Sql,
 | 
				
			||||||
@ -179,16 +186,12 @@ public abstract class DapperRepository<T> : IRepository<T>
 | 
				
			|||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<int> GetCount(Filter<T>? filter = null)
 | 
						public Task<int> GetCount(Filter<T>? filter = null)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return Database.Count(
 | 
							return Database.Count(Sql, Config, Context, filter);
 | 
				
			||||||
			Sql,
 | 
					 | 
				
			||||||
			Config,
 | 
					 | 
				
			||||||
			Context,
 | 
					 | 
				
			||||||
			filter
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<ICollection<T>> Search(string query, Include<T>? include = null) => throw new NotImplementedException();
 | 
						public Task<ICollection<T>> Search(string query, Include<T>? include = null) =>
 | 
				
			||||||
 | 
							throw new NotImplementedException();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<T> Create(T obj) => throw new NotImplementedException();
 | 
						public Task<T> Create(T obj) => throw new NotImplementedException();
 | 
				
			||||||
 | 
				
			|||||||
@ -47,8 +47,12 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			IRepository<Show>.OnEdited += async (show) =>
 | 
								IRepository<Show>.OnEdited += async (show) =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
 | 
									await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
 | 
				
			||||||
				DatabaseContext database = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
 | 
									DatabaseContext database = scope
 | 
				
			||||||
				List<Episode> episodes = await database.Episodes.AsTracking()
 | 
										.ServiceProvider
 | 
				
			||||||
 | 
										.GetRequiredService<DatabaseContext>();
 | 
				
			||||||
 | 
									List<Episode> episodes = await database
 | 
				
			||||||
 | 
										.Episodes
 | 
				
			||||||
 | 
										.AsTracking()
 | 
				
			||||||
					.Where(x => x.ShowId == show.Id)
 | 
										.Where(x => x.ShowId == show.Id)
 | 
				
			||||||
					.ToListAsync();
 | 
										.ToListAsync();
 | 
				
			||||||
				foreach (Episode ep in episodes)
 | 
									foreach (Episode ep in episodes)
 | 
				
			||||||
@ -66,9 +70,11 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// <param name="database">The database handle to use.</param>
 | 
							/// <param name="database">The database handle to use.</param>
 | 
				
			||||||
		/// <param name="shows">A show repository</param>
 | 
							/// <param name="shows">A show repository</param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
				
			||||||
		public EpisodeRepository(DatabaseContext database,
 | 
							public EpisodeRepository(
 | 
				
			||||||
 | 
								DatabaseContext database,
 | 
				
			||||||
			IRepository<Show> shows,
 | 
								IRepository<Show> shows,
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
								IThumbnailsManager thumbs
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			: base(database, thumbs)
 | 
								: base(database, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_database = database;
 | 
								_database = database;
 | 
				
			||||||
@ -76,7 +82,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task<ICollection<Episode>> Search(string query, Include<Episode>? include = default)
 | 
							public override async Task<ICollection<Episode>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<Episode>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await AddIncludes(_database.Episodes, include)
 | 
								return await AddIncludes(_database.Episodes, include)
 | 
				
			||||||
				.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
 | 
									.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
 | 
				
			||||||
@ -87,14 +96,26 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		protected override Task<Episode?> GetDuplicated(Episode item)
 | 
							protected override Task<Episode?> GetDuplicated(Episode item)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (item is { SeasonNumber: not null, EpisodeNumber: not null })
 | 
								if (item is { SeasonNumber: not null, EpisodeNumber: not null })
 | 
				
			||||||
				return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber && x.EpisodeNumber == item.EpisodeNumber);
 | 
									return _database
 | 
				
			||||||
			return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber);
 | 
										.Episodes
 | 
				
			||||||
 | 
										.FirstOrDefaultAsync(
 | 
				
			||||||
 | 
											x =>
 | 
				
			||||||
 | 
												x.ShowId == item.ShowId
 | 
				
			||||||
 | 
												&& x.SeasonNumber == item.SeasonNumber
 | 
				
			||||||
 | 
												&& x.EpisodeNumber == item.EpisodeNumber
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
 | 
								return _database
 | 
				
			||||||
 | 
									.Episodes
 | 
				
			||||||
 | 
									.FirstOrDefaultAsync(
 | 
				
			||||||
 | 
										x => x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task<Episode> Create(Episode obj)
 | 
							public override async Task<Episode> Create(Episode obj)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			obj.ShowSlug = obj.Show?.Slug ?? (await _database.Shows.FirstAsync(x => x.Id == obj.ShowId)).Slug;
 | 
								obj.ShowSlug =
 | 
				
			||||||
 | 
									obj.Show?.Slug ?? (await _database.Shows.FirstAsync(x => x.Id == obj.ShowId)).Slug;
 | 
				
			||||||
			await base.Create(obj);
 | 
								await base.Create(obj);
 | 
				
			||||||
			_database.Entry(obj).State = EntityState.Added;
 | 
								_database.Entry(obj).State = EntityState.Added;
 | 
				
			||||||
			await _database.SaveChangesAsync(() => GetDuplicated(obj));
 | 
								await _database.SaveChangesAsync(() => GetDuplicated(obj));
 | 
				
			||||||
@ -110,22 +131,31 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			{
 | 
								{
 | 
				
			||||||
				if (resource.Show == null)
 | 
									if (resource.Show == null)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					throw new ArgumentException($"Can't store an episode not related " +
 | 
										throw new ArgumentException(
 | 
				
			||||||
						$"to any show (showID: {resource.ShowId}).");
 | 
											$"Can't store an episode not related "
 | 
				
			||||||
 | 
												+ $"to any show (showID: {resource.ShowId})."
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				resource.ShowId = resource.Show.Id;
 | 
									resource.ShowId = resource.Show.Id;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (resource.SeasonId == null && resource.SeasonNumber != null)
 | 
								if (resource.SeasonId == null && resource.SeasonNumber != null)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				resource.Season = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == resource.ShowId
 | 
									resource.Season = await _database
 | 
				
			||||||
					&& x.SeasonNumber == resource.SeasonNumber);
 | 
										.Seasons
 | 
				
			||||||
 | 
										.FirstOrDefaultAsync(
 | 
				
			||||||
 | 
											x => x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task Delete(Episode obj)
 | 
							public override async Task Delete(Episode 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;
 | 
								_database.Entry(obj).State = EntityState.Deleted;
 | 
				
			||||||
			await _database.SaveChangesAsync();
 | 
								await _database.SaveChangesAsync();
 | 
				
			||||||
			await base.Delete(obj);
 | 
								await base.Delete(obj);
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
	public class LibraryItemRepository : DapperRepository<ILibraryItem>
 | 
						public class LibraryItemRepository : DapperRepository<ILibraryItem>
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// language=PostgreSQL
 | 
							// language=PostgreSQL
 | 
				
			||||||
		protected override FormattableString Sql => $"""
 | 
							protected override FormattableString Sql =>
 | 
				
			||||||
 | 
								$"""
 | 
				
			||||||
			select
 | 
								select
 | 
				
			||||||
				s.*, -- Show as s
 | 
									s.*, -- Show as s
 | 
				
			||||||
				m.*,
 | 
									m.*,
 | 
				
			||||||
@ -58,7 +59,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
				) as c on false
 | 
									) as c on false
 | 
				
			||||||
			""";
 | 
								""";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protected override Dictionary<string, Type> Config => new()
 | 
							protected override Dictionary<string, Type> Config =>
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				{ "s", typeof(Show) },
 | 
									{ "s", typeof(Show) },
 | 
				
			||||||
				{ "m", typeof(Movie) },
 | 
									{ "m", typeof(Movie) },
 | 
				
			||||||
@ -77,15 +79,15 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public LibraryItemRepository(DbConnection database, SqlVariableContext context)
 | 
							public LibraryItemRepository(DbConnection database, SqlVariableContext context)
 | 
				
			||||||
			: base(database, context)
 | 
								: base(database, context) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
 | 
							public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
 | 
				
			||||||
			Guid collectionId,
 | 
								Guid collectionId,
 | 
				
			||||||
			Filter<ILibraryItem>? filter = default,
 | 
								Filter<ILibraryItem>? filter = default,
 | 
				
			||||||
			Sort<ILibraryItem>? sort = default,
 | 
								Sort<ILibraryItem>? sort = default,
 | 
				
			||||||
			Include<ILibraryItem>? include = default,
 | 
								Include<ILibraryItem>? include = default,
 | 
				
			||||||
			Pagination? limit = default)
 | 
								Pagination? limit = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			// language=PostgreSQL
 | 
								// language=PostgreSQL
 | 
				
			||||||
			FormattableString sql = $"""
 | 
								FormattableString sql = $"""
 | 
				
			||||||
@ -111,11 +113,7 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			return await Database.Query<ILibraryItem>(
 | 
								return await Database.Query<ILibraryItem>(
 | 
				
			||||||
				sql,
 | 
									sql,
 | 
				
			||||||
				new()
 | 
									new() { { "s", typeof(Show) }, { "m", typeof(Movie) }, },
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					{ "s", typeof(Show) },
 | 
					 | 
				
			||||||
					{ "m", typeof(Movie) },
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				Mapper,
 | 
									Mapper,
 | 
				
			||||||
				(id) => Get(id),
 | 
									(id) => Get(id),
 | 
				
			||||||
				Context,
 | 
									Context,
 | 
				
			||||||
 | 
				
			|||||||
@ -75,17 +75,18 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			sortBy ??= new Sort<T>.Default();
 | 
								sortBy ??= new Sort<T>.Default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			IOrderedQueryable<T> _SortBy(IQueryable<T> qr, Expression<Func<T, object>> sort, bool desc, bool then)
 | 
								IOrderedQueryable<T> _SortBy(
 | 
				
			||||||
 | 
									IQueryable<T> qr,
 | 
				
			||||||
 | 
									Expression<Func<T, object>> sort,
 | 
				
			||||||
 | 
									bool desc,
 | 
				
			||||||
 | 
									bool then
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (then && qr is IOrderedQueryable<T> qro)
 | 
									if (then && qr is IOrderedQueryable<T> qro)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					return desc
 | 
										return desc ? qro.ThenByDescending(sort) : qro.ThenBy(sort);
 | 
				
			||||||
						? qro.ThenByDescending(sort)
 | 
					 | 
				
			||||||
						: qro.ThenBy(sort);
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				return desc
 | 
									return desc ? qr.OrderByDescending(sort) : qr.OrderBy(sort);
 | 
				
			||||||
					? qr.OrderByDescending(sort)
 | 
					 | 
				
			||||||
					: qr.OrderBy(sort);
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			IOrderedQueryable<T> _Sort(IQueryable<T> query, Sort<T> sortBy, bool then)
 | 
								IOrderedQueryable<T> _Sort(IQueryable<T> query, Sort<T> sortBy, bool then)
 | 
				
			||||||
@ -98,7 +99,12 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
						return _SortBy(query, x => EF.Property<T>(x, key), desc, then);
 | 
											return _SortBy(query, x => EF.Property<T>(x, key), desc, then);
 | 
				
			||||||
					case Sort<T>.Random(var seed):
 | 
										case Sort<T>.Random(var seed):
 | 
				
			||||||
						// NOTE: To edit this, don't forget to edit the random handiling inside the KeysetPaginate function
 | 
											// NOTE: To edit this, don't forget to edit the random handiling inside the KeysetPaginate function
 | 
				
			||||||
						return _SortBy(query, x => DatabaseContext.MD5(seed + x.Id.ToString()), false, then);
 | 
											return _SortBy(
 | 
				
			||||||
 | 
												query,
 | 
				
			||||||
 | 
												x => DatabaseContext.MD5(seed + x.Id.ToString()),
 | 
				
			||||||
 | 
												false,
 | 
				
			||||||
 | 
												then
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
					case Sort<T>.Conglomerate(var sorts):
 | 
										case Sort<T>.Conglomerate(var sorts):
 | 
				
			||||||
						IOrderedQueryable<T> nQuery = _Sort(query, sorts.First(), false);
 | 
											IOrderedQueryable<T> nQuery = _Sort(query, sorts.First(), false);
 | 
				
			||||||
						foreach (Sort<T> sort in sorts.Skip(1))
 | 
											foreach (Sort<T> sort in sorts.Skip(1))
 | 
				
			||||||
@ -121,11 +127,28 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			Expression CmpRandomHandler(string cmp, string seed, Guid refId)
 | 
								Expression CmpRandomHandler(string cmp, string seed, Guid refId)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				MethodInfo concat = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!;
 | 
									MethodInfo concat = typeof(string).GetMethod(
 | 
				
			||||||
				Expression id = Expression.Call(Expression.Property(x, "ID"), nameof(Guid.ToString), null);
 | 
										nameof(string.Concat),
 | 
				
			||||||
 | 
										new[] { typeof(string), typeof(string) }
 | 
				
			||||||
 | 
									)!;
 | 
				
			||||||
 | 
									Expression id = Expression.Call(
 | 
				
			||||||
 | 
										Expression.Property(x, "ID"),
 | 
				
			||||||
 | 
										nameof(Guid.ToString),
 | 
				
			||||||
 | 
										null
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
				Expression xrng = Expression.Call(concat, Expression.Constant(seed), id);
 | 
									Expression xrng = Expression.Call(concat, Expression.Constant(seed), id);
 | 
				
			||||||
				Expression left = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, xrng);
 | 
									Expression left = Expression.Call(
 | 
				
			||||||
				Expression right = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, Expression.Constant($"{seed}{refId}"));
 | 
										typeof(DatabaseContext),
 | 
				
			||||||
 | 
										nameof(DatabaseContext.MD5),
 | 
				
			||||||
 | 
										null,
 | 
				
			||||||
 | 
										xrng
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
									Expression right = Expression.Call(
 | 
				
			||||||
 | 
										typeof(DatabaseContext),
 | 
				
			||||||
 | 
										nameof(DatabaseContext.MD5),
 | 
				
			||||||
 | 
										null,
 | 
				
			||||||
 | 
										Expression.Constant($"{seed}{refId}")
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
				return cmp switch
 | 
									return cmp switch
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					"=" => Expression.Equal(left, right),
 | 
										"=" => Expression.Equal(left, right),
 | 
				
			||||||
@ -138,17 +161,28 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			BinaryExpression StringCompatibleExpression(
 | 
								BinaryExpression StringCompatibleExpression(
 | 
				
			||||||
				Func<Expression, Expression, BinaryExpression> operand,
 | 
									Func<Expression, Expression, BinaryExpression> operand,
 | 
				
			||||||
				string property,
 | 
									string property,
 | 
				
			||||||
				object value)
 | 
									object value
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				var left = Expression.Property(x, property);
 | 
									var left = Expression.Property(x, property);
 | 
				
			||||||
				var right = Expression.Constant(value, ((PropertyInfo)left.Member).PropertyType);
 | 
									var right = Expression.Constant(value, ((PropertyInfo)left.Member).PropertyType);
 | 
				
			||||||
				if (left.Type != typeof(string))
 | 
									if (left.Type != typeof(string))
 | 
				
			||||||
					return operand(left, right);
 | 
										return operand(left, right);
 | 
				
			||||||
				MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right);
 | 
									MethodCallExpression call = Expression.Call(
 | 
				
			||||||
 | 
										typeof(string),
 | 
				
			||||||
 | 
										"Compare",
 | 
				
			||||||
 | 
										null,
 | 
				
			||||||
 | 
										left,
 | 
				
			||||||
 | 
										right
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
				return operand(call, Expression.Constant(0));
 | 
									return operand(call, Expression.Constant(0));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			Expression Exp(Func<Expression, Expression, BinaryExpression> operand, string property, object? value)
 | 
								Expression Exp(
 | 
				
			||||||
 | 
									Func<Expression, Expression, BinaryExpression> operand,
 | 
				
			||||||
 | 
									string property,
 | 
				
			||||||
 | 
									object? value
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				var prop = Expression.Property(x, property);
 | 
									var prop = Expression.Property(x, property);
 | 
				
			||||||
				var val = Expression.Constant(value, ((PropertyInfo)prop.Member).PropertyType);
 | 
									var val = Expression.Constant(value, ((PropertyInfo)prop.Member).PropertyType);
 | 
				
			||||||
@ -159,18 +193,42 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			{
 | 
								{
 | 
				
			||||||
				return f switch
 | 
									return f switch
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					Filter<T>.And(var first, var second) => Expression.AndAlso(Parse(first), Parse(second)),
 | 
										Filter<T>.And(var first, var second)
 | 
				
			||||||
					Filter<T>.Or(var first, var second) => Expression.OrElse(Parse(first), Parse(second)),
 | 
											=> Expression.AndAlso(Parse(first), Parse(second)),
 | 
				
			||||||
 | 
										Filter<T>.Or(var first, var second)
 | 
				
			||||||
 | 
											=> Expression.OrElse(Parse(first), Parse(second)),
 | 
				
			||||||
					Filter<T>.Not(var inner) => Expression.Not(Parse(inner)),
 | 
										Filter<T>.Not(var inner) => Expression.Not(Parse(inner)),
 | 
				
			||||||
					Filter<T>.Eq(var property, var value) => Exp(Expression.Equal, property, value),
 | 
										Filter<T>.Eq(var property, var value) => Exp(Expression.Equal, property, value),
 | 
				
			||||||
					Filter<T>.Ne(var property, var value) => Exp(Expression.NotEqual, property, value),
 | 
										Filter<T>.Ne(var property, var value)
 | 
				
			||||||
					Filter<T>.Gt(var property, var value) => StringCompatibleExpression(Expression.GreaterThan, property, value),
 | 
											=> Exp(Expression.NotEqual, property, value),
 | 
				
			||||||
					Filter<T>.Ge(var property, var value) => StringCompatibleExpression(Expression.GreaterThanOrEqual, property, value),
 | 
										Filter<T>.Gt(var property, var value)
 | 
				
			||||||
					Filter<T>.Lt(var property, var value) => StringCompatibleExpression(Expression.LessThan, property, value),
 | 
											=> StringCompatibleExpression(Expression.GreaterThan, property, value),
 | 
				
			||||||
					Filter<T>.Le(var property, var value) => StringCompatibleExpression(Expression.LessThanOrEqual, property, value),
 | 
										Filter<T>.Ge(var property, var value)
 | 
				
			||||||
					Filter<T>.Has(var property, var value) => Expression.Call(typeof(Enumerable), "Contains", new[] { value.GetType() }, Expression.Property(x, property), Expression.Constant(value)),
 | 
											=> StringCompatibleExpression(
 | 
				
			||||||
					Filter<T>.CmpRandom(var op, var seed, var refId) => CmpRandomHandler(op, seed, refId),
 | 
												Expression.GreaterThanOrEqual,
 | 
				
			||||||
					Filter<T>.Lambda(var lambda) => ExpressionArgumentReplacer.ReplaceParams(lambda.Body, lambda.Parameters, x),
 | 
												property,
 | 
				
			||||||
 | 
												value
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
										Filter<T>.Lt(var property, var value)
 | 
				
			||||||
 | 
											=> StringCompatibleExpression(Expression.LessThan, property, value),
 | 
				
			||||||
 | 
										Filter<T>.Le(var property, var value)
 | 
				
			||||||
 | 
											=> StringCompatibleExpression(Expression.LessThanOrEqual, property, value),
 | 
				
			||||||
 | 
										Filter<T>.Has(var property, var value)
 | 
				
			||||||
 | 
											=> Expression.Call(
 | 
				
			||||||
 | 
												typeof(Enumerable),
 | 
				
			||||||
 | 
												"Contains",
 | 
				
			||||||
 | 
												new[] { value.GetType() },
 | 
				
			||||||
 | 
												Expression.Property(x, property),
 | 
				
			||||||
 | 
												Expression.Constant(value)
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
										Filter<T>.CmpRandom(var op, var seed, var refId)
 | 
				
			||||||
 | 
											=> CmpRandomHandler(op, seed, refId),
 | 
				
			||||||
 | 
										Filter<T>.Lambda(var lambda)
 | 
				
			||||||
 | 
											=> ExpressionArgumentReplacer.ReplaceParams(
 | 
				
			||||||
 | 
												lambda.Body,
 | 
				
			||||||
 | 
												lambda.Parameters,
 | 
				
			||||||
 | 
												x
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
					_ => throw new NotImplementedException(),
 | 
										_ => throw new NotImplementedException(),
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -231,7 +289,9 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId);
 | 
								T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId);
 | 
				
			||||||
			if (ret == null)
 | 
								if (ret == null)
 | 
				
			||||||
				throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
 | 
									throw new ItemNotFoundException(
 | 
				
			||||||
 | 
										$"No {typeof(T).Name} found with the given predicate."
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			return ret;
 | 
								return ret;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -243,8 +303,7 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public virtual Task<T?> GetOrDefault(Guid id, Include<T>? include = default)
 | 
							public virtual Task<T?> GetOrDefault(Guid id, Include<T>? include = default)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return AddIncludes(Database.Set<T>(), include)
 | 
								return AddIncludes(Database.Set<T>(), include).FirstOrDefaultAsync(x => x.Id == id);
 | 
				
			||||||
				.FirstOrDefaultAsync(x => x.Id == id);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
@ -256,16 +315,17 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
					.OrderBy(x => EF.Functions.Random())
 | 
										.OrderBy(x => EF.Functions.Random())
 | 
				
			||||||
					.FirstOrDefaultAsync();
 | 
										.FirstOrDefaultAsync();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return AddIncludes(Database.Set<T>(), include)
 | 
								return AddIncludes(Database.Set<T>(), include).FirstOrDefaultAsync(x => x.Slug == slug);
 | 
				
			||||||
				.FirstOrDefaultAsync(x => x.Slug == slug);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public virtual async Task<T?> GetOrDefault(Filter<T>? filter,
 | 
							public virtual async Task<T?> GetOrDefault(
 | 
				
			||||||
 | 
								Filter<T>? filter,
 | 
				
			||||||
			Include<T>? include = default,
 | 
								Include<T>? include = default,
 | 
				
			||||||
			Sort<T>? sortBy = default,
 | 
								Sort<T>? sortBy = default,
 | 
				
			||||||
			bool reverse = false,
 | 
								bool reverse = false,
 | 
				
			||||||
			Guid? afterId = default)
 | 
								Guid? afterId = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			IQueryable<T> query = await ApplyFilters(
 | 
								IQueryable<T> query = await ApplyFilters(
 | 
				
			||||||
				Database.Set<T>(),
 | 
									Database.Set<T>(),
 | 
				
			||||||
@ -278,7 +338,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc/>
 | 
							/// <inheritdoc/>
 | 
				
			||||||
		public virtual async Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = default)
 | 
							public virtual async Task<ICollection<T>> FromIds(
 | 
				
			||||||
 | 
								IList<Guid> ids,
 | 
				
			||||||
 | 
								Include<T>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return (
 | 
								return (
 | 
				
			||||||
				await AddIncludes(Database.Set<T>(), include)
 | 
									await AddIncludes(Database.Set<T>(), include)
 | 
				
			||||||
@ -293,12 +356,20 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		public abstract Task<ICollection<T>> Search(string query, Include<T>? include = default);
 | 
							public abstract Task<ICollection<T>> Search(string query, Include<T>? include = default);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc/>
 | 
							/// <inheritdoc/>
 | 
				
			||||||
		public virtual async Task<ICollection<T>> GetAll(Filter<T>? filter = null,
 | 
							public virtual async Task<ICollection<T>> GetAll(
 | 
				
			||||||
 | 
								Filter<T>? filter = null,
 | 
				
			||||||
			Sort<T>? sort = default,
 | 
								Sort<T>? sort = default,
 | 
				
			||||||
			Include<T>? include = default,
 | 
								Include<T>? include = default,
 | 
				
			||||||
			Pagination? limit = default)
 | 
								Pagination? limit = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			IQueryable<T> query = await ApplyFilters(Database.Set<T>(), filter, sort, limit, include);
 | 
								IQueryable<T> query = await ApplyFilters(
 | 
				
			||||||
 | 
									Database.Set<T>(),
 | 
				
			||||||
 | 
									filter,
 | 
				
			||||||
 | 
									sort,
 | 
				
			||||||
 | 
									limit,
 | 
				
			||||||
 | 
									include
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			return await query.ToListAsync();
 | 
								return await query.ToListAsync();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -311,11 +382,13 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// <param name="limit">Pagination information (where to start and how many to get)</param>
 | 
							/// <param name="limit">Pagination information (where to start and how many to get)</param>
 | 
				
			||||||
		/// <param name="include">Related fields to also load with this query.</param>
 | 
							/// <param name="include">Related fields to also load with this query.</param>
 | 
				
			||||||
		/// <returns>The filtered query</returns>
 | 
							/// <returns>The filtered query</returns>
 | 
				
			||||||
		protected async Task<IQueryable<T>> ApplyFilters(IQueryable<T> query,
 | 
							protected async Task<IQueryable<T>> ApplyFilters(
 | 
				
			||||||
 | 
								IQueryable<T> query,
 | 
				
			||||||
			Filter<T>? filter = null,
 | 
								Filter<T>? filter = null,
 | 
				
			||||||
			Sort<T>? sort = default,
 | 
								Sort<T>? sort = default,
 | 
				
			||||||
			Pagination? limit = default,
 | 
								Pagination? limit = default,
 | 
				
			||||||
			Include<T>? include = default)
 | 
								Include<T>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			query = AddIncludes(query, include);
 | 
								query = AddIncludes(query, include);
 | 
				
			||||||
			query = Sort(query, sort);
 | 
								query = Sort(query, sort);
 | 
				
			||||||
@ -324,7 +397,11 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			if (limit.AfterID != null)
 | 
								if (limit.AfterID != null)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				T reference = await Get(limit.AfterID.Value);
 | 
									T reference = await Get(limit.AfterID.Value);
 | 
				
			||||||
				Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse);
 | 
									Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(
 | 
				
			||||||
 | 
										sort,
 | 
				
			||||||
 | 
										reference,
 | 
				
			||||||
 | 
										!limit.Reverse
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
				filter = Filter.And(filter, keysetFilter);
 | 
									filter = Filter.And(filter, keysetFilter);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (filter != null)
 | 
								if (filter != null)
 | 
				
			||||||
@ -364,11 +441,14 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
					throw new DuplicatedItemException(await GetDuplicated(obj));
 | 
										throw new DuplicatedItemException(await GetDuplicated(obj));
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				if (thumbs.Poster != null)
 | 
									if (thumbs.Poster != null)
 | 
				
			||||||
					Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry!.State = EntityState.Added;
 | 
										Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry!.State =
 | 
				
			||||||
 | 
											EntityState.Added;
 | 
				
			||||||
				if (thumbs.Thumbnail != null)
 | 
									if (thumbs.Thumbnail != null)
 | 
				
			||||||
					Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry!.State = EntityState.Added;
 | 
										Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry!.State =
 | 
				
			||||||
 | 
											EntityState.Added;
 | 
				
			||||||
				if (thumbs.Logo != null)
 | 
									if (thumbs.Logo != null)
 | 
				
			||||||
					Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State = EntityState.Added;
 | 
										Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State =
 | 
				
			||||||
 | 
											EntityState.Added;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return obj;
 | 
								return obj;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -399,7 +479,11 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			{
 | 
								{
 | 
				
			||||||
				T old = await GetWithTracking(edited.Id);
 | 
									T old = await GetWithTracking(edited.Id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				Merger.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null);
 | 
									Merger.Complete(
 | 
				
			||||||
 | 
										old,
 | 
				
			||||||
 | 
										edited,
 | 
				
			||||||
 | 
										x => x.GetCustomAttribute<LoadableRelationAttribute>() == null
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
				await EditRelations(old, edited);
 | 
									await EditRelations(old, edited);
 | 
				
			||||||
				await Database.SaveChangesAsync();
 | 
									await Database.SaveChangesAsync();
 | 
				
			||||||
				await IRepository<T>.OnResourceEdited(old);
 | 
									await IRepository<T>.OnResourceEdited(old);
 | 
				
			||||||
@ -450,8 +534,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			if (resource is IThumbnails thumbs && changed is IThumbnails chng)
 | 
								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.Poster).IsModified =
 | 
				
			||||||
				Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = thumbs.Thumbnail != chng.Thumbnail;
 | 
										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;
 | 
									Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != chng.Logo;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return Validate(resource);
 | 
								return Validate(resource);
 | 
				
			||||||
@ -468,7 +554,11 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
							/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 | 
				
			||||||
		protected virtual Task Validate(T resource)
 | 
							protected virtual Task Validate(T resource)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute<ComputedAttribute>() != null)
 | 
								if (
 | 
				
			||||||
 | 
									typeof(T)
 | 
				
			||||||
 | 
										.GetProperty(nameof(resource.Slug))!
 | 
				
			||||||
 | 
										.GetCustomAttribute<ComputedAttribute>() != null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return Task.CompletedTask;
 | 
									return Task.CompletedTask;
 | 
				
			||||||
			if (string.IsNullOrEmpty(resource.Slug))
 | 
								if (string.IsNullOrEmpty(resource.Slug))
 | 
				
			||||||
				throw new ArgumentException("Resource can't have null as a slug.");
 | 
									throw new ArgumentException("Resource can't have null as a slug.");
 | 
				
			||||||
@ -476,15 +566,21 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			{
 | 
								{
 | 
				
			||||||
				try
 | 
									try
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					MethodInfo? setter = typeof(T).GetProperty(nameof(resource.Slug))!.GetSetMethod();
 | 
										MethodInfo? setter = typeof(T)
 | 
				
			||||||
 | 
											.GetProperty(nameof(resource.Slug))!
 | 
				
			||||||
 | 
											.GetSetMethod();
 | 
				
			||||||
					if (setter != null)
 | 
										if (setter != null)
 | 
				
			||||||
						setter.Invoke(resource, new object[] { resource.Slug + '!' });
 | 
											setter.Invoke(resource, new object[] { resource.Slug + '!' });
 | 
				
			||||||
					else
 | 
										else
 | 
				
			||||||
						throw new ArgumentException("Resources slug can't be number only or the literal \"random\".");
 | 
											throw new ArgumentException(
 | 
				
			||||||
 | 
												"Resources slug can't be number only or the literal \"random\"."
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				catch
 | 
									catch
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					throw new ArgumentException("Resources slug can't be number only or the literal \"random\".");
 | 
										throw new ArgumentException(
 | 
				
			||||||
 | 
											"Resources slug can't be number only or the literal \"random\"."
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return Task.CompletedTask;
 | 
								return Task.CompletedTask;
 | 
				
			||||||
 | 
				
			|||||||
@ -54,10 +54,12 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// <param name="studios">A studio repository</param>
 | 
							/// <param name="studios">A studio repository</param>
 | 
				
			||||||
		/// <param name="people">A people repository</param>
 | 
							/// <param name="people">A people repository</param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
				
			||||||
		public MovieRepository(DatabaseContext database,
 | 
							public MovieRepository(
 | 
				
			||||||
 | 
								DatabaseContext database,
 | 
				
			||||||
			IRepository<Studio> studios,
 | 
								IRepository<Studio> studios,
 | 
				
			||||||
			IRepository<People> people,
 | 
								IRepository<People> people,
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
								IThumbnailsManager thumbs
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			: base(database, thumbs)
 | 
								: base(database, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_database = database;
 | 
								_database = database;
 | 
				
			||||||
@ -66,7 +68,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task<ICollection<Movie>> Search(string query, Include<Movie>? include = default)
 | 
							public override async Task<ICollection<Movie>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<Movie>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await AddIncludes(_database.Movies, include)
 | 
								return await AddIncludes(_database.Movies, include)
 | 
				
			||||||
				.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
 | 
									.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
	public class NewsRepository : DapperRepository<INews>
 | 
						public class NewsRepository : DapperRepository<INews>
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// language=PostgreSQL
 | 
							// language=PostgreSQL
 | 
				
			||||||
		protected override FormattableString Sql => $"""
 | 
							protected override FormattableString Sql =>
 | 
				
			||||||
 | 
								$"""
 | 
				
			||||||
			select
 | 
								select
 | 
				
			||||||
				e.*, -- Episode as e
 | 
									e.*, -- Episode as e
 | 
				
			||||||
				m.*
 | 
									m.*
 | 
				
			||||||
@ -45,11 +46,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			) as m on false
 | 
								) as m on false
 | 
				
			||||||
			""";
 | 
								""";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protected override Dictionary<string, Type> Config => new()
 | 
							protected override Dictionary<string, Type> Config =>
 | 
				
			||||||
		{
 | 
								new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, };
 | 
				
			||||||
			{ "e", typeof(Episode) },
 | 
					 | 
				
			||||||
			{ "m", typeof(Movie) },
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protected override INews Mapper(List<object?> items)
 | 
							protected override INews Mapper(List<object?> items)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -61,7 +59,6 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public NewsRepository(DbConnection database, SqlVariableContext context)
 | 
							public NewsRepository(DbConnection database, SqlVariableContext context)
 | 
				
			||||||
			: base(database, context)
 | 
								: base(database, context) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -50,9 +50,11 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// <param name="database">The database handle</param>
 | 
							/// <param name="database">The database handle</param>
 | 
				
			||||||
		/// <param name="shows">A lazy loaded show repository</param>
 | 
							/// <param name="shows">A lazy loaded show repository</param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
				
			||||||
		public PeopleRepository(DatabaseContext database,
 | 
							public PeopleRepository(
 | 
				
			||||||
 | 
								DatabaseContext database,
 | 
				
			||||||
			Lazy<IRepository<Show>> shows,
 | 
								Lazy<IRepository<Show>> shows,
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
								IThumbnailsManager thumbs
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			: base(database, thumbs)
 | 
								: base(database, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_database = database;
 | 
								_database = database;
 | 
				
			||||||
@ -60,7 +62,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override Task<ICollection<People>> Search(string query, Include<People>? include = default)
 | 
							public override Task<ICollection<People>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<People>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			throw new NotImplementedException();
 | 
								throw new NotImplementedException();
 | 
				
			||||||
			// return await AddIncludes(_database.People, include)
 | 
								// return await AddIncludes(_database.People, include)
 | 
				
			||||||
@ -88,7 +93,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			{
 | 
								{
 | 
				
			||||||
				foreach (PeopleRole role in resource.Roles)
 | 
									foreach (PeopleRole role in resource.Roles)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					role.Show = _database.LocalEntity<Show>(role.Show!.Slug)
 | 
										role.Show =
 | 
				
			||||||
 | 
											_database.LocalEntity<Show>(role.Show!.Slug)
 | 
				
			||||||
						?? await _shows.Value.CreateIfNotExists(role.Show);
 | 
											?? await _shows.Value.CreateIfNotExists(role.Show);
 | 
				
			||||||
					role.ShowID = role.Show.Id;
 | 
										role.ShowID = role.Show.Id;
 | 
				
			||||||
					_database.Entry(role).State = EntityState.Added;
 | 
										_database.Entry(role).State = EntityState.Added;
 | 
				
			||||||
 | 
				
			|||||||
@ -59,9 +59,11 @@ public class RepositoryHelper
 | 
				
			|||||||
			return sort switch
 | 
								return sort switch
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				Sort<T>.Default(var value) => GetSortsBy(value),
 | 
									Sort<T>.Default(var value) => GetSortsBy(value),
 | 
				
			||||||
				Sort<T>.By @sortBy => new[] { new SortIndicator(sortBy.Key, sortBy.Desendant, null) },
 | 
									Sort<T>.By @sortBy
 | 
				
			||||||
 | 
										=> new[] { new SortIndicator(sortBy.Key, sortBy.Desendant, null) },
 | 
				
			||||||
				Sort<T>.Conglomerate(var list) => list.SelectMany(GetSortsBy),
 | 
									Sort<T>.Conglomerate(var list) => list.SelectMany(GetSortsBy),
 | 
				
			||||||
				Sort<T>.Random(var seed) => new[] { new SortIndicator("random", false, seed.ToString()) },
 | 
									Sort<T>.Random(var seed)
 | 
				
			||||||
 | 
										=> new[] { new SortIndicator("random", false, seed.ToString()) },
 | 
				
			||||||
				_ => Array.Empty<SortIndicator>(),
 | 
									_ => Array.Empty<SortIndicator>(),
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -88,8 +90,12 @@ public class RepositoryHelper
 | 
				
			|||||||
			Filter<T>? equals = null;
 | 
								Filter<T>? equals = null;
 | 
				
			||||||
			foreach ((string pKey, bool pDesc, string? pSeed) in previousSteps)
 | 
								foreach ((string pKey, bool pDesc, string? pSeed) in previousSteps)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				Filter<T> pEquals = pSeed == null
 | 
									Filter<T> pEquals =
 | 
				
			||||||
					? new Filter<T>.Eq(pKey, reference.GetType().GetProperty(pKey)?.GetValue(reference))
 | 
										pSeed == null
 | 
				
			||||||
 | 
											? new Filter<T>.Eq(
 | 
				
			||||||
 | 
												pKey,
 | 
				
			||||||
 | 
												reference.GetType().GetProperty(pKey)?.GetValue(reference)
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
						: new Filter<T>.CmpRandom("=", pSeed, reference.Id);
 | 
											: new Filter<T>.CmpRandom("=", pSeed, reference.Id);
 | 
				
			||||||
				equals = Filter.And(equals, pEquals);
 | 
									equals = Filter.And(equals, pEquals);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -98,14 +104,18 @@ public class RepositoryHelper
 | 
				
			|||||||
			Func<string, object, Filter<T>> comparer = greaterThan
 | 
								Func<string, object, Filter<T>> comparer = greaterThan
 | 
				
			||||||
				? (prop, val) => new Filter<T>.Gt(prop, val)
 | 
									? (prop, val) => new Filter<T>.Gt(prop, val)
 | 
				
			||||||
				: (prop, val) => new Filter<T>.Lt(prop, val);
 | 
									: (prop, val) => new Filter<T>.Lt(prop, val);
 | 
				
			||||||
			Filter<T> last = seed == null
 | 
								Filter<T> last =
 | 
				
			||||||
 | 
									seed == null
 | 
				
			||||||
					? comparer(key, value!)
 | 
										? comparer(key, value!)
 | 
				
			||||||
					: new Filter<T>.CmpRandom(greaterThan ? ">" : "<", seed, reference.Id);
 | 
										: new Filter<T>.CmpRandom(greaterThan ? ">" : "<", seed, reference.Id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (key != "random")
 | 
								if (key != "random")
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
									Type[] types =
 | 
				
			||||||
				PropertyInfo property = types.Select(x => x.GetProperty(key)!).First(x => x != null);
 | 
										typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
 | 
				
			||||||
 | 
									PropertyInfo property = types
 | 
				
			||||||
 | 
										.Select(x => x.GetProperty(key)!)
 | 
				
			||||||
 | 
										.First(x => x != null);
 | 
				
			||||||
				if (Nullable.GetUnderlyingType(property.PropertyType) != null)
 | 
									if (Nullable.GetUnderlyingType(property.PropertyType) != null)
 | 
				
			||||||
					last = new Filter<T>.Or(last, new Filter<T>.Eq(key, null));
 | 
										last = new Filter<T>.Or(last, new Filter<T>.Eq(key, null));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
				
			|||||||
@ -47,8 +47,12 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			IRepository<Show>.OnEdited += async (show) =>
 | 
								IRepository<Show>.OnEdited += async (show) =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
 | 
									await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
 | 
				
			||||||
				DatabaseContext database = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
 | 
									DatabaseContext database = scope
 | 
				
			||||||
				List<Season> seasons = await database.Seasons.AsTracking()
 | 
										.ServiceProvider
 | 
				
			||||||
 | 
										.GetRequiredService<DatabaseContext>();
 | 
				
			||||||
 | 
									List<Season> seasons = await database
 | 
				
			||||||
 | 
										.Seasons
 | 
				
			||||||
 | 
										.AsTracking()
 | 
				
			||||||
					.Where(x => x.ShowId == show.Id)
 | 
										.Where(x => x.ShowId == show.Id)
 | 
				
			||||||
					.ToListAsync();
 | 
										.ToListAsync();
 | 
				
			||||||
				foreach (Season season in seasons)
 | 
									foreach (Season season in seasons)
 | 
				
			||||||
@ -65,8 +69,7 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="database">The database handle that will be used</param>
 | 
							/// <param name="database">The database handle that will be used</param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
				
			||||||
		public SeasonRepository(DatabaseContext database,
 | 
							public SeasonRepository(DatabaseContext database, IThumbnailsManager thumbs)
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
					 | 
				
			||||||
			: base(database, thumbs)
 | 
								: base(database, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_database = database;
 | 
								_database = database;
 | 
				
			||||||
@ -74,11 +77,18 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		protected override Task<Season?> GetDuplicated(Season item)
 | 
							protected override Task<Season?> GetDuplicated(Season item)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber);
 | 
								return _database
 | 
				
			||||||
 | 
									.Seasons
 | 
				
			||||||
 | 
									.FirstOrDefaultAsync(
 | 
				
			||||||
 | 
										x => x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc/>
 | 
							/// <inheritdoc/>
 | 
				
			||||||
		public override async Task<ICollection<Season>> Search(string query, Include<Season>? include = default)
 | 
							public override async Task<ICollection<Season>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<Season>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await AddIncludes(_database.Seasons, include)
 | 
								return await AddIncludes(_database.Seasons, include)
 | 
				
			||||||
				.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
 | 
									.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
 | 
				
			||||||
@ -90,7 +100,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		public override async Task<Season> Create(Season obj)
 | 
							public override async Task<Season> Create(Season obj)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			await base.Create(obj);
 | 
								await base.Create(obj);
 | 
				
			||||||
			obj.ShowSlug = (await _database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug
 | 
								obj.ShowSlug =
 | 
				
			||||||
 | 
									(await _database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug
 | 
				
			||||||
				?? throw new ItemNotFoundException($"No show found with ID {obj.ShowId}");
 | 
									?? throw new ItemNotFoundException($"No show found with ID {obj.ShowId}");
 | 
				
			||||||
			_database.Entry(obj).State = EntityState.Added;
 | 
								_database.Entry(obj).State = EntityState.Added;
 | 
				
			||||||
			await _database.SaveChangesAsync(() => GetDuplicated(obj));
 | 
								await _database.SaveChangesAsync(() => GetDuplicated(obj));
 | 
				
			||||||
@ -106,8 +117,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
			{
 | 
								{
 | 
				
			||||||
				if (resource.Show == null)
 | 
									if (resource.Show == null)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					throw new ValidationException($"Can't store a season not related to any show " +
 | 
										throw new ValidationException(
 | 
				
			||||||
						$"(showID: {resource.ShowId}).");
 | 
											$"Can't store a season not related to any show "
 | 
				
			||||||
 | 
												+ $"(showID: {resource.ShowId})."
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				resource.ShowId = resource.Show.Id;
 | 
									resource.ShowId = resource.Show.Id;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
				
			|||||||
@ -55,10 +55,12 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// <param name="studios">A studio repository</param>
 | 
							/// <param name="studios">A studio repository</param>
 | 
				
			||||||
		/// <param name="people">A people repository</param>
 | 
							/// <param name="people">A people repository</param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to store images.</param>
 | 
				
			||||||
		public ShowRepository(DatabaseContext database,
 | 
							public ShowRepository(
 | 
				
			||||||
 | 
								DatabaseContext database,
 | 
				
			||||||
			IRepository<Studio> studios,
 | 
								IRepository<Studio> studios,
 | 
				
			||||||
			IRepository<People> people,
 | 
								IRepository<People> people,
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
								IThumbnailsManager thumbs
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			: base(database, thumbs)
 | 
								: base(database, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_database = database;
 | 
								_database = database;
 | 
				
			||||||
@ -67,7 +69,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task<ICollection<Show>> Search(string query, Include<Show>? include = default)
 | 
							public override async Task<ICollection<Show>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<Show>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await AddIncludes(_database.Shows, include)
 | 
								return await AddIncludes(_database.Shows, include)
 | 
				
			||||||
				.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
 | 
									.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
 | 
				
			||||||
 | 
				
			|||||||
@ -50,7 +50,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task<ICollection<Studio>> Search(string query, Include<Studio>? include = default)
 | 
							public override async Task<ICollection<Studio>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<Studio>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await AddIncludes(_database.Studios, include)
 | 
								return await AddIncludes(_database.Studios, include)
 | 
				
			||||||
				.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
 | 
									.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
 | 
				
			||||||
 | 
				
			|||||||
@ -49,7 +49,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override async Task<ICollection<User>> Search(string query, Include<User>? include = default)
 | 
							public override async Task<ICollection<User>> Search(
 | 
				
			||||||
 | 
								string query,
 | 
				
			||||||
 | 
								Include<User>? include = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await AddIncludes(_database.Users, include)
 | 
								return await AddIncludes(_database.Users, include)
 | 
				
			||||||
				.Where(x => EF.Functions.ILike(x.Username, $"%{query}%"))
 | 
									.Where(x => EF.Functions.ILike(x.Username, $"%{query}%"))
 | 
				
			||||||
 | 
				
			|||||||
@ -66,7 +66,9 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
 | 
								await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
 | 
				
			||||||
			DatabaseContext db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
 | 
								DatabaseContext db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
 | 
				
			||||||
			WatchStatusRepository repo = scope.ServiceProvider.GetRequiredService<WatchStatusRepository>();
 | 
								WatchStatusRepository repo = scope
 | 
				
			||||||
 | 
									.ServiceProvider
 | 
				
			||||||
 | 
									.GetRequiredService<WatchStatusRepository>();
 | 
				
			||||||
			List<Guid> users = await db.ShowWatchStatus
 | 
								List<Guid> users = await db.ShowWatchStatus
 | 
				
			||||||
				.IgnoreQueryFilters()
 | 
									.IgnoreQueryFilters()
 | 
				
			||||||
				.Where(x => x.ShowId == ep.ShowId && x.Status == WatchStatus.Completed)
 | 
									.Where(x => x.ShowId == ep.ShowId && x.Status == WatchStatus.Completed)
 | 
				
			||||||
@ -77,10 +79,12 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public WatchStatusRepository(DatabaseContext database,
 | 
						public WatchStatusRepository(
 | 
				
			||||||
 | 
							DatabaseContext database,
 | 
				
			||||||
		IRepository<Movie> movies,
 | 
							IRepository<Movie> movies,
 | 
				
			||||||
		DbConnection db,
 | 
							DbConnection db,
 | 
				
			||||||
		SqlVariableContext context)
 | 
							SqlVariableContext context
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		_database = database;
 | 
							_database = database;
 | 
				
			||||||
		_movies = movies;
 | 
							_movies = movies;
 | 
				
			||||||
@ -89,7 +93,8 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// language=PostgreSQL
 | 
						// language=PostgreSQL
 | 
				
			||||||
	protected FormattableString Sql => $"""
 | 
						protected FormattableString Sql =>
 | 
				
			||||||
 | 
							$"""
 | 
				
			||||||
		select
 | 
							select
 | 
				
			||||||
			s.*,
 | 
								s.*,
 | 
				
			||||||
			swe.*, -- Episode as swe
 | 
								swe.*, -- Episode as swe
 | 
				
			||||||
@ -126,7 +131,8 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
			coalesce(s.id, m.id) asc
 | 
								coalesce(s.id, m.id) asc
 | 
				
			||||||
		""";
 | 
							""";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	protected Dictionary<string, Type> Config => new()
 | 
						protected Dictionary<string, Type> Config =>
 | 
				
			||||||
 | 
							new()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			{ "s", typeof(Show) },
 | 
								{ "s", typeof(Show) },
 | 
				
			||||||
			{ "_sw", typeof(ShowWatchStatus) },
 | 
								{ "_sw", typeof(ShowWatchStatus) },
 | 
				
			||||||
@ -178,10 +184,14 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
	public async Task<ICollection<IWatchlist>> GetAll(
 | 
						public async Task<ICollection<IWatchlist>> GetAll(
 | 
				
			||||||
		Filter<IWatchlist>? filter = default,
 | 
							Filter<IWatchlist>? filter = default,
 | 
				
			||||||
		Include<IWatchlist>? include = default,
 | 
							Include<IWatchlist>? include = default,
 | 
				
			||||||
		Pagination? limit = default)
 | 
							Pagination? limit = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		if (include != null)
 | 
							if (include != null)
 | 
				
			||||||
			include.Metadatas = include.Metadatas.Where(x => x.Name != nameof(Show.WatchStatus)).ToList();
 | 
								include.Metadatas = include
 | 
				
			||||||
 | 
									.Metadatas
 | 
				
			||||||
 | 
									.Where(x => x.Name != nameof(Show.WatchStatus))
 | 
				
			||||||
 | 
									.ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// We can't use the generic after id hanler since the sort depends on a relation.
 | 
							// We can't use the generic after id hanler since the sort depends on a relation.
 | 
				
			||||||
		if (limit?.AfterID != null)
 | 
							if (limit?.AfterID != null)
 | 
				
			||||||
@ -216,7 +226,9 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<MovieWatchStatus?> GetMovieStatus(Guid movieId, Guid userId)
 | 
						public Task<MovieWatchStatus?> GetMovieStatus(Guid movieId, Guid userId)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _database.MovieWatchStatus.FirstOrDefaultAsync(x => x.MovieId == movieId && x.UserId == userId);
 | 
							return _database
 | 
				
			||||||
 | 
								.MovieWatchStatus
 | 
				
			||||||
 | 
								.FirstOrDefaultAsync(x => x.MovieId == movieId && x.UserId == userId);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
@ -224,10 +236,12 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
		Guid movieId,
 | 
							Guid movieId,
 | 
				
			||||||
		Guid userId,
 | 
							Guid userId,
 | 
				
			||||||
		WatchStatus status,
 | 
							WatchStatus status,
 | 
				
			||||||
		int? watchedTime)
 | 
							int? watchedTime
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		Movie movie = await _movies.Get(movieId);
 | 
							Movie movie = await _movies.Get(movieId);
 | 
				
			||||||
		int? percent = watchedTime != null && movie.Runtime > 0
 | 
							int? percent =
 | 
				
			||||||
 | 
								watchedTime != null && movie.Runtime > 0
 | 
				
			||||||
				? (int)Math.Round(watchedTime.Value / (movie.Runtime * 60f) * 100f)
 | 
									? (int)Math.Round(watchedTime.Value / (movie.Runtime * 60f) * 100f)
 | 
				
			||||||
				: null;
 | 
									: null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -241,9 +255,12 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (watchedTime.HasValue && status != WatchStatus.Watching)
 | 
							if (watchedTime.HasValue && status != WatchStatus.Watching)
 | 
				
			||||||
			throw new ValidationException("Can't have a watched time if the status is not watching.");
 | 
								throw new ValidationException(
 | 
				
			||||||
 | 
									"Can't have a watched time if the status is not watching."
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		MovieWatchStatus ret = new()
 | 
							MovieWatchStatus ret =
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				UserId = userId,
 | 
									UserId = userId,
 | 
				
			||||||
				MovieId = movieId,
 | 
									MovieId = movieId,
 | 
				
			||||||
@ -253,18 +270,19 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
				AddedDate = DateTime.UtcNow,
 | 
									AddedDate = DateTime.UtcNow,
 | 
				
			||||||
				PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
 | 
									PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		await _database.MovieWatchStatus.Upsert(ret)
 | 
							await _database
 | 
				
			||||||
 | 
								.MovieWatchStatus
 | 
				
			||||||
 | 
								.Upsert(ret)
 | 
				
			||||||
			.UpdateIf(x => status != Watching || x.Status != Completed)
 | 
								.UpdateIf(x => status != Watching || x.Status != Completed)
 | 
				
			||||||
			.RunAsync();
 | 
								.RunAsync();
 | 
				
			||||||
		return ret;
 | 
							return ret;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public async Task DeleteMovieStatus(
 | 
						public async Task DeleteMovieStatus(Guid movieId, Guid userId)
 | 
				
			||||||
		Guid movieId,
 | 
					 | 
				
			||||||
		Guid userId)
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		await _database.MovieWatchStatus
 | 
							await _database
 | 
				
			||||||
 | 
								.MovieWatchStatus
 | 
				
			||||||
			.Where(x => x.MovieId == movieId && x.UserId == userId)
 | 
								.Where(x => x.MovieId == movieId && x.UserId == userId)
 | 
				
			||||||
			.ExecuteDeleteAsync();
 | 
								.ExecuteDeleteAsync();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -272,26 +290,32 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<ShowWatchStatus?> GetShowStatus(Guid showId, Guid userId)
 | 
						public Task<ShowWatchStatus?> GetShowStatus(Guid showId, Guid userId)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _database.ShowWatchStatus.FirstOrDefaultAsync(x => x.ShowId == showId && x.UserId == userId);
 | 
							return _database
 | 
				
			||||||
 | 
								.ShowWatchStatus
 | 
				
			||||||
 | 
								.FirstOrDefaultAsync(x => x.ShowId == showId && x.UserId == userId);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<ShowWatchStatus?> SetShowStatus(
 | 
						public Task<ShowWatchStatus?> SetShowStatus(Guid showId, Guid userId, WatchStatus status) =>
 | 
				
			||||||
		Guid showId,
 | 
							_SetShowStatus(showId, userId, status);
 | 
				
			||||||
		Guid userId,
 | 
					 | 
				
			||||||
		WatchStatus status
 | 
					 | 
				
			||||||
	) => _SetShowStatus(showId, userId, status);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async Task<ShowWatchStatus?> _SetShowStatus(
 | 
						private async Task<ShowWatchStatus?> _SetShowStatus(
 | 
				
			||||||
		Guid showId,
 | 
							Guid showId,
 | 
				
			||||||
		Guid userId,
 | 
							Guid userId,
 | 
				
			||||||
		WatchStatus status,
 | 
							WatchStatus status,
 | 
				
			||||||
		bool newEpisode = false)
 | 
							bool newEpisode = false
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		int unseenEpisodeCount = status != WatchStatus.Completed
 | 
							int unseenEpisodeCount =
 | 
				
			||||||
			? await _database.Episodes
 | 
								status != WatchStatus.Completed
 | 
				
			||||||
 | 
									? await _database
 | 
				
			||||||
 | 
										.Episodes
 | 
				
			||||||
					.Where(x => x.ShowId == showId)
 | 
										.Where(x => x.ShowId == showId)
 | 
				
			||||||
				.Where(x => x.Watched!.First(x => x.UserId == userId)!.Status != WatchStatus.Completed)
 | 
										.Where(
 | 
				
			||||||
 | 
											x =>
 | 
				
			||||||
 | 
												x.Watched!.First(x => x.UserId == userId)!.Status
 | 
				
			||||||
 | 
												!= WatchStatus.Completed
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
					.CountAsync()
 | 
										.CountAsync()
 | 
				
			||||||
				: 0;
 | 
									: 0;
 | 
				
			||||||
		if (unseenEpisodeCount == 0)
 | 
							if (unseenEpisodeCount == 0)
 | 
				
			||||||
@ -301,79 +325,105 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
		Guid? nextEpisodeId = null;
 | 
							Guid? nextEpisodeId = null;
 | 
				
			||||||
		if (status == WatchStatus.Watching)
 | 
							if (status == WatchStatus.Watching)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			var cursor = await _database.Episodes
 | 
								var cursor = await _database
 | 
				
			||||||
 | 
									.Episodes
 | 
				
			||||||
				.IgnoreQueryFilters()
 | 
									.IgnoreQueryFilters()
 | 
				
			||||||
				.Where(x => x.ShowId == showId)
 | 
									.Where(x => x.ShowId == showId)
 | 
				
			||||||
				.OrderByDescending(x => x.AbsoluteNumber)
 | 
									.OrderByDescending(x => x.AbsoluteNumber)
 | 
				
			||||||
				.OrderByDescending(x => x.SeasonNumber)
 | 
									.OrderByDescending(x => x.SeasonNumber)
 | 
				
			||||||
				.OrderByDescending(x => x.EpisodeNumber)
 | 
									.OrderByDescending(x => x.EpisodeNumber)
 | 
				
			||||||
				.Select(x => new { x.Id, Status = x.Watched!.First(x => x.UserId == userId) })
 | 
									.Select(x => new { x.Id, Status = x.Watched!.First(x => x.UserId == userId) })
 | 
				
			||||||
				.FirstOrDefaultAsync(x => x.Status.Status == WatchStatus.Completed || x.Status.Status == WatchStatus.Watching);
 | 
									.FirstOrDefaultAsync(
 | 
				
			||||||
 | 
										x =>
 | 
				
			||||||
 | 
											x.Status.Status == WatchStatus.Completed
 | 
				
			||||||
 | 
											|| x.Status.Status == WatchStatus.Watching
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			cursorWatchStatus = cursor?.Status;
 | 
								cursorWatchStatus = cursor?.Status;
 | 
				
			||||||
			nextEpisodeId = cursor?.Status.Status == WatchStatus.Watching
 | 
								nextEpisodeId =
 | 
				
			||||||
 | 
									cursor?.Status.Status == WatchStatus.Watching
 | 
				
			||||||
					? cursor.Id
 | 
										? cursor.Id
 | 
				
			||||||
				: await _database.Episodes
 | 
										: await _database
 | 
				
			||||||
 | 
											.Episodes
 | 
				
			||||||
						.IgnoreQueryFilters()
 | 
											.IgnoreQueryFilters()
 | 
				
			||||||
						.Where(x => x.ShowId == showId)
 | 
											.Where(x => x.ShowId == showId)
 | 
				
			||||||
						.OrderByDescending(x => x.AbsoluteNumber)
 | 
											.OrderByDescending(x => x.AbsoluteNumber)
 | 
				
			||||||
						.OrderByDescending(x => x.SeasonNumber)
 | 
											.OrderByDescending(x => x.SeasonNumber)
 | 
				
			||||||
						.OrderByDescending(x => x.EpisodeNumber)
 | 
											.OrderByDescending(x => x.EpisodeNumber)
 | 
				
			||||||
					.Select(x => new { x.Id, Status = x.Watched!.FirstOrDefault(x => x.UserId == userId) })
 | 
											.Select(
 | 
				
			||||||
 | 
												x =>
 | 
				
			||||||
 | 
													new
 | 
				
			||||||
 | 
													{
 | 
				
			||||||
 | 
														x.Id,
 | 
				
			||||||
 | 
														Status = x.Watched!.FirstOrDefault(x => x.UserId == userId)
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
						.Where(x => x.Status == null || x.Status.Status != WatchStatus.Completed)
 | 
											.Where(x => x.Status == null || x.Status.Status != WatchStatus.Completed)
 | 
				
			||||||
						.Select(x => x.Id)
 | 
											.Select(x => x.Id)
 | 
				
			||||||
						.FirstOrDefaultAsync();
 | 
											.FirstOrDefaultAsync();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else if (status == WatchStatus.Completed)
 | 
							else if (status == WatchStatus.Completed)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			List<Guid> episodes = await _database.Episodes
 | 
								List<Guid> episodes = await _database
 | 
				
			||||||
 | 
									.Episodes
 | 
				
			||||||
				.Where(x => x.ShowId == showId)
 | 
									.Where(x => x.ShowId == showId)
 | 
				
			||||||
				.Select(x => x.Id)
 | 
									.Select(x => x.Id)
 | 
				
			||||||
				.ToListAsync();
 | 
									.ToListAsync();
 | 
				
			||||||
			await _database.EpisodeWatchStatus
 | 
								await _database
 | 
				
			||||||
				.UpsertRange(episodes.Select(episodeId => new EpisodeWatchStatus
 | 
									.EpisodeWatchStatus
 | 
				
			||||||
 | 
									.UpsertRange(
 | 
				
			||||||
 | 
										episodes.Select(
 | 
				
			||||||
 | 
											episodeId =>
 | 
				
			||||||
 | 
												new EpisodeWatchStatus
 | 
				
			||||||
							{
 | 
												{
 | 
				
			||||||
								UserId = userId,
 | 
													UserId = userId,
 | 
				
			||||||
								EpisodeId = episodeId,
 | 
													EpisodeId = episodeId,
 | 
				
			||||||
								Status = WatchStatus.Completed,
 | 
													Status = WatchStatus.Completed,
 | 
				
			||||||
								AddedDate = DateTime.UtcNow,
 | 
													AddedDate = DateTime.UtcNow,
 | 
				
			||||||
								PlayedDate = DateTime.UtcNow
 | 
													PlayedDate = DateTime.UtcNow
 | 
				
			||||||
				}))
 | 
												}
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
				.UpdateIf(x => x.Status == Watching || x.Status == Planned)
 | 
									.UpdateIf(x => x.Status == Watching || x.Status == Planned)
 | 
				
			||||||
				.RunAsync();
 | 
									.RunAsync();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ShowWatchStatus ret = new()
 | 
							ShowWatchStatus ret =
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				UserId = userId,
 | 
									UserId = userId,
 | 
				
			||||||
				ShowId = showId,
 | 
									ShowId = showId,
 | 
				
			||||||
				Status = status,
 | 
									Status = status,
 | 
				
			||||||
				AddedDate = DateTime.UtcNow,
 | 
									AddedDate = DateTime.UtcNow,
 | 
				
			||||||
				NextEpisodeId = nextEpisodeId,
 | 
									NextEpisodeId = nextEpisodeId,
 | 
				
			||||||
			WatchedTime = cursorWatchStatus?.Status == WatchStatus.Watching
 | 
									WatchedTime =
 | 
				
			||||||
 | 
										cursorWatchStatus?.Status == WatchStatus.Watching
 | 
				
			||||||
						? cursorWatchStatus.WatchedTime
 | 
											? cursorWatchStatus.WatchedTime
 | 
				
			||||||
						: null,
 | 
											: null,
 | 
				
			||||||
			WatchedPercent = cursorWatchStatus?.Status == WatchStatus.Watching
 | 
									WatchedPercent =
 | 
				
			||||||
 | 
										cursorWatchStatus?.Status == WatchStatus.Watching
 | 
				
			||||||
						? cursorWatchStatus.WatchedPercent
 | 
											? cursorWatchStatus.WatchedPercent
 | 
				
			||||||
						: null,
 | 
											: null,
 | 
				
			||||||
				UnseenEpisodesCount = unseenEpisodeCount,
 | 
									UnseenEpisodesCount = unseenEpisodeCount,
 | 
				
			||||||
				PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
 | 
									PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		await _database.ShowWatchStatus.Upsert(ret)
 | 
							await _database
 | 
				
			||||||
 | 
								.ShowWatchStatus
 | 
				
			||||||
 | 
								.Upsert(ret)
 | 
				
			||||||
			.UpdateIf(x => status != Watching || x.Status != Completed || newEpisode)
 | 
								.UpdateIf(x => status != Watching || x.Status != Completed || newEpisode)
 | 
				
			||||||
			.RunAsync();
 | 
								.RunAsync();
 | 
				
			||||||
		return ret;
 | 
							return ret;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public async Task DeleteShowStatus(
 | 
						public async Task DeleteShowStatus(Guid showId, Guid userId)
 | 
				
			||||||
		Guid showId,
 | 
					 | 
				
			||||||
		Guid userId)
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		await _database.ShowWatchStatus
 | 
							await _database
 | 
				
			||||||
 | 
								.ShowWatchStatus
 | 
				
			||||||
			.IgnoreAutoIncludes()
 | 
								.IgnoreAutoIncludes()
 | 
				
			||||||
			.Where(x => x.ShowId == showId && x.UserId == userId)
 | 
								.Where(x => x.ShowId == showId && x.UserId == userId)
 | 
				
			||||||
			.ExecuteDeleteAsync();
 | 
								.ExecuteDeleteAsync();
 | 
				
			||||||
		await _database.EpisodeWatchStatus
 | 
							await _database
 | 
				
			||||||
 | 
								.EpisodeWatchStatus
 | 
				
			||||||
			.Where(x => x.Episode.ShowId == showId && x.UserId == userId)
 | 
								.Where(x => x.Episode.ShowId == showId && x.UserId == userId)
 | 
				
			||||||
			.ExecuteDeleteAsync();
 | 
								.ExecuteDeleteAsync();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -381,7 +431,9 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public Task<EpisodeWatchStatus?> GetEpisodeStatus(Guid episodeId, Guid userId)
 | 
						public Task<EpisodeWatchStatus?> GetEpisodeStatus(Guid episodeId, Guid userId)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _database.EpisodeWatchStatus.FirstOrDefaultAsync(x => x.EpisodeId == episodeId && x.UserId == userId);
 | 
							return _database
 | 
				
			||||||
 | 
								.EpisodeWatchStatus
 | 
				
			||||||
 | 
								.FirstOrDefaultAsync(x => x.EpisodeId == episodeId && x.UserId == userId);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
@ -389,10 +441,12 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
		Guid episodeId,
 | 
							Guid episodeId,
 | 
				
			||||||
		Guid userId,
 | 
							Guid userId,
 | 
				
			||||||
		WatchStatus status,
 | 
							WatchStatus status,
 | 
				
			||||||
		int? watchedTime)
 | 
							int? watchedTime
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		Episode episode = await _database.Episodes.FirstAsync(x => x.Id == episodeId);
 | 
							Episode episode = await _database.Episodes.FirstAsync(x => x.Id == episodeId);
 | 
				
			||||||
		int? percent = watchedTime != null && episode.Runtime > 0
 | 
							int? percent =
 | 
				
			||||||
 | 
								watchedTime != null && episode.Runtime > 0
 | 
				
			||||||
				? (int)Math.Round(watchedTime.Value / (episode.Runtime * 60f) * 100f)
 | 
									? (int)Math.Round(watchedTime.Value / (episode.Runtime * 60f) * 100f)
 | 
				
			||||||
				: null;
 | 
									: null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -406,9 +460,12 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (watchedTime.HasValue && status != WatchStatus.Watching)
 | 
							if (watchedTime.HasValue && status != WatchStatus.Watching)
 | 
				
			||||||
			throw new ValidationException("Can't have a watched time if the status is not watching.");
 | 
								throw new ValidationException(
 | 
				
			||||||
 | 
									"Can't have a watched time if the status is not watching."
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		EpisodeWatchStatus ret = new()
 | 
							EpisodeWatchStatus ret =
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				UserId = userId,
 | 
									UserId = userId,
 | 
				
			||||||
				EpisodeId = episodeId,
 | 
									EpisodeId = episodeId,
 | 
				
			||||||
@ -418,7 +475,9 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
				AddedDate = DateTime.UtcNow,
 | 
									AddedDate = DateTime.UtcNow,
 | 
				
			||||||
				PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
 | 
									PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		await _database.EpisodeWatchStatus.Upsert(ret)
 | 
							await _database
 | 
				
			||||||
 | 
								.EpisodeWatchStatus
 | 
				
			||||||
 | 
								.Upsert(ret)
 | 
				
			||||||
			.UpdateIf(x => status != Watching || x.Status != Completed)
 | 
								.UpdateIf(x => status != Watching || x.Status != Completed)
 | 
				
			||||||
			.RunAsync();
 | 
								.RunAsync();
 | 
				
			||||||
		await SetShowStatus(episode.ShowId, userId, WatchStatus.Watching);
 | 
							await SetShowStatus(episode.ShowId, userId, WatchStatus.Watching);
 | 
				
			||||||
@ -426,11 +485,10 @@ public class WatchStatusRepository : IWatchStatusRepository
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc />
 | 
						/// <inheritdoc />
 | 
				
			||||||
	public async Task DeleteEpisodeStatus(
 | 
						public async Task DeleteEpisodeStatus(Guid episodeId, Guid userId)
 | 
				
			||||||
		Guid episodeId,
 | 
					 | 
				
			||||||
		Guid userId)
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		await _database.EpisodeWatchStatus
 | 
							await _database
 | 
				
			||||||
 | 
								.EpisodeWatchStatus
 | 
				
			||||||
			.Where(x => x.EpisodeId == episodeId && x.UserId == userId)
 | 
								.Where(x => x.EpisodeId == episodeId && x.UserId == userId)
 | 
				
			||||||
			.ExecuteDeleteAsync();
 | 
								.ExecuteDeleteAsync();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -36,7 +36,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
	/// </summary>
 | 
						/// </summary>
 | 
				
			||||||
	public class ThumbnailsManager : IThumbnailsManager
 | 
						public class ThumbnailsManager : IThumbnailsManager
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		private static readonly Dictionary<string, TaskCompletionSource<object>> _downloading = new();
 | 
							private static readonly Dictionary<string, TaskCompletionSource<object>> _downloading =
 | 
				
			||||||
 | 
								new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private readonly ILogger<ThumbnailsManager> _logger;
 | 
							private readonly ILogger<ThumbnailsManager> _logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -47,8 +48,10 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="clientFactory">Client factory</param>
 | 
							/// <param name="clientFactory">Client factory</param>
 | 
				
			||||||
		/// <param name="logger">A logger to report errors</param>
 | 
							/// <param name="logger">A logger to report errors</param>
 | 
				
			||||||
		public ThumbnailsManager(IHttpClientFactory clientFactory,
 | 
							public ThumbnailsManager(
 | 
				
			||||||
			ILogger<ThumbnailsManager> logger)
 | 
								IHttpClientFactory clientFactory,
 | 
				
			||||||
 | 
								ILogger<ThumbnailsManager> logger
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_clientFactory = clientFactory;
 | 
								_clientFactory = clientFactory;
 | 
				
			||||||
			_logger = logger;
 | 
								_logger = logger;
 | 
				
			||||||
@ -85,14 +88,35 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
				info.ColorType = SKColorType.Rgba8888;
 | 
									info.ColorType = SKColorType.Rgba8888;
 | 
				
			||||||
				using SKBitmap original = SKBitmap.Decode(codec, info);
 | 
									using SKBitmap original = SKBitmap.Decode(codec, info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				using SKBitmap high = original.Resize(new SKSizeI(original.Width, original.Height), SKFilterQuality.High);
 | 
									using SKBitmap high = original.Resize(
 | 
				
			||||||
				await _WriteTo(original, $"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp", 90);
 | 
										new SKSizeI(original.Width, original.Height),
 | 
				
			||||||
 | 
										SKFilterQuality.High
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
									await _WriteTo(
 | 
				
			||||||
 | 
										original,
 | 
				
			||||||
 | 
										$"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp",
 | 
				
			||||||
 | 
										90
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				using SKBitmap medium = high.Resize(new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)), SKFilterQuality.Medium);
 | 
									using SKBitmap medium = high.Resize(
 | 
				
			||||||
				await _WriteTo(medium, $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp", 75);
 | 
										new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)),
 | 
				
			||||||
 | 
										SKFilterQuality.Medium
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
									await _WriteTo(
 | 
				
			||||||
 | 
										medium,
 | 
				
			||||||
 | 
										$"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp",
 | 
				
			||||||
 | 
										75
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				using SKBitmap low = medium.Resize(new SKSizeI(original.Width / 2, original.Height / 2), SKFilterQuality.Low);
 | 
									using SKBitmap low = medium.Resize(
 | 
				
			||||||
				await _WriteTo(low, $"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp", 50);
 | 
										new SKSizeI(original.Width / 2, original.Height / 2),
 | 
				
			||||||
 | 
										SKFilterQuality.Low
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
									await _WriteTo(
 | 
				
			||||||
 | 
										low,
 | 
				
			||||||
 | 
										$"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp",
 | 
				
			||||||
 | 
										50
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				image.Blurhash = Blurhasher.Encode(low, 4, 3);
 | 
									image.Blurhash = Blurhasher.Encode(low, 4, 3);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -108,7 +132,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			string name = item is IResource res ? res.Slug : "???";
 | 
								string name = item is IResource res ? res.Slug : "???";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			string posterPath = $"{_GetBaseImagePath(item, "poster")}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp";
 | 
								string posterPath =
 | 
				
			||||||
 | 
									$"{_GetBaseImagePath(item, "poster")}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp";
 | 
				
			||||||
			bool duplicated = false;
 | 
								bool duplicated = false;
 | 
				
			||||||
			TaskCompletionSource<object>? sync = null;
 | 
								TaskCompletionSource<object>? sync = null;
 | 
				
			||||||
			try
 | 
								try
 | 
				
			||||||
@ -128,15 +153,25 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
				if (duplicated)
 | 
									if (duplicated)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					object? dup = sync != null
 | 
										object? dup = sync != null ? await sync.Task : null;
 | 
				
			||||||
						? await sync.Task
 | 
					 | 
				
			||||||
						: null;
 | 
					 | 
				
			||||||
					throw new DuplicatedItemException(dup);
 | 
										throw new DuplicatedItemException(dup);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				await _DownloadImage(item.Poster, _GetBaseImagePath(item, "poster"), $"The poster of {name}");
 | 
									await _DownloadImage(
 | 
				
			||||||
				await _DownloadImage(item.Thumbnail, _GetBaseImagePath(item, "thumbnail"), $"The poster of {name}");
 | 
										item.Poster,
 | 
				
			||||||
				await _DownloadImage(item.Logo, _GetBaseImagePath(item, "logo"), $"The poster of {name}");
 | 
										_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}"
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			finally
 | 
								finally
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -155,7 +190,8 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			string directory = item switch
 | 
								string directory = item switch
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				IResource res => Path.Combine("./metadata", item.GetType().Name.ToLowerInvariant(), res.Slug),
 | 
									IResource res
 | 
				
			||||||
 | 
										=> Path.Combine("./metadata", item.GetType().Name.ToLowerInvariant(), res.Slug),
 | 
				
			||||||
				_ => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant())
 | 
									_ => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant())
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			Directory.CreateDirectory(directory);
 | 
								Directory.CreateDirectory(directory);
 | 
				
			||||||
@ -175,7 +211,9 @@ namespace Kyoo.Core.Controllers
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			IEnumerable<string> images = new[] { "poster", "thumbnail", "logo" }
 | 
								IEnumerable<string> images = new[] { "poster", "thumbnail", "logo" }
 | 
				
			||||||
				.SelectMany(x => _GetBaseImagePath(item, x))
 | 
									.SelectMany(x => _GetBaseImagePath(item, x))
 | 
				
			||||||
				.SelectMany(x => new[]
 | 
									.SelectMany(
 | 
				
			||||||
 | 
										x =>
 | 
				
			||||||
 | 
											new[]
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							ImageQuality.High.ToString().ToLowerInvariant(),
 | 
												ImageQuality.High.ToString().ToLowerInvariant(),
 | 
				
			||||||
							ImageQuality.Medium.ToString().ToLowerInvariant(),
 | 
												ImageQuality.Medium.ToString().ToLowerInvariant(),
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,10 @@ namespace Kyoo.Core
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void Configure(ContainerBuilder builder)
 | 
							public void Configure(ContainerBuilder builder)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
 | 
								builder
 | 
				
			||||||
 | 
									.RegisterType<ThumbnailsManager>()
 | 
				
			||||||
 | 
									.As<IThumbnailsManager>()
 | 
				
			||||||
 | 
									.InstancePerLifetimeScope();
 | 
				
			||||||
			builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
 | 
								builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			builder.RegisterRepository<LibraryItemRepository>();
 | 
								builder.RegisterRepository<LibraryItemRepository>();
 | 
				
			||||||
@ -67,7 +70,11 @@ namespace Kyoo.Core
 | 
				
			|||||||
			builder.RegisterRepository<StudioRepository>();
 | 
								builder.RegisterRepository<StudioRepository>();
 | 
				
			||||||
			builder.RegisterRepository<UserRepository>();
 | 
								builder.RegisterRepository<UserRepository>();
 | 
				
			||||||
			builder.RegisterRepository<NewsRepository>();
 | 
								builder.RegisterRepository<NewsRepository>();
 | 
				
			||||||
			builder.RegisterType<WatchStatusRepository>().As<IWatchStatusRepository>().AsSelf().InstancePerLifetimeScope();
 | 
								builder
 | 
				
			||||||
 | 
									.RegisterType<WatchStatusRepository>()
 | 
				
			||||||
 | 
									.As<IWatchStatusRepository>()
 | 
				
			||||||
 | 
									.AsSelf()
 | 
				
			||||||
 | 
									.InstancePerLifetimeScope();
 | 
				
			||||||
			builder.RegisterType<SqlVariableContext>().InstancePerLifetimeScope();
 | 
								builder.RegisterType<SqlVariableContext>().InstancePerLifetimeScope();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -77,7 +84,8 @@ namespace Kyoo.Core
 | 
				
			|||||||
			services.AddHttpContextAccessor();
 | 
								services.AddHttpContextAccessor();
 | 
				
			||||||
			services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
 | 
								services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			services.AddMvcCore(options =>
 | 
								services
 | 
				
			||||||
 | 
									.AddMvcCore(options =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					options.Filters.Add<ExceptionFilter>();
 | 
										options.Filters.Add<ExceptionFilter>();
 | 
				
			||||||
					options.ModelBinderProviders.Insert(0, new SortBinder.Provider());
 | 
										options.ModelBinderProviders.Insert(0, new SortBinder.Provider());
 | 
				
			||||||
@ -120,12 +128,16 @@ namespace Kyoo.Core
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
 | 
							public IEnumerable<IStartupAction> ConfigureSteps =>
 | 
				
			||||||
 | 
								new IStartupAction[]
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				SA.New<IApplicationBuilder>(app => app.UseHsts(), SA.Before),
 | 
									SA.New<IApplicationBuilder>(app => app.UseHsts(), SA.Before),
 | 
				
			||||||
				SA.New<IApplicationBuilder>(app => app.UseResponseCompression(), SA.Routing + 1),
 | 
									SA.New<IApplicationBuilder>(app => app.UseResponseCompression(), SA.Routing + 1),
 | 
				
			||||||
				SA.New<IApplicationBuilder>(app => app.UseRouting(), SA.Routing),
 | 
									SA.New<IApplicationBuilder>(app => app.UseRouting(), SA.Routing),
 | 
				
			||||||
			SA.New<IApplicationBuilder>(app => app.UseEndpoints(x => x.MapControllers()), SA.Endpoint)
 | 
									SA.New<IApplicationBuilder>(
 | 
				
			||||||
 | 
										app => app.UseEndpoints(x => x.MapControllers()),
 | 
				
			||||||
 | 
										SA.Endpoint
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -66,7 +66,9 @@ namespace Kyoo.Core
 | 
				
			|||||||
					break;
 | 
										break;
 | 
				
			||||||
				case Exception ex:
 | 
									case Exception ex:
 | 
				
			||||||
					_logger.LogError(ex, "Unhandled error");
 | 
										_logger.LogError(ex, "Unhandled error");
 | 
				
			||||||
					context.Result = new ServerErrorObjectResult(new RequestError("Internal Server Error"));
 | 
										context.Result = new ServerErrorObjectResult(
 | 
				
			||||||
 | 
											new RequestError("Internal Server Error")
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,9 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit)
 | 
							protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit)
 | 
				
			||||||
			where TResult : IResource
 | 
								where TResult : IResource
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Dictionary<string, string> query = Request.Query.ToDictionary(
 | 
								Dictionary<string, string> query = Request
 | 
				
			||||||
 | 
									.Query
 | 
				
			||||||
 | 
									.ToDictionary(
 | 
				
			||||||
					x => x.Key,
 | 
										x => x.Key,
 | 
				
			||||||
					x => x.Value.ToString(),
 | 
										x => x.Value.ToString(),
 | 
				
			||||||
					StringComparer.InvariantCultureIgnoreCase
 | 
										StringComparer.InvariantCultureIgnoreCase
 | 
				
			||||||
@ -58,18 +60,15 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				query["sortBy"] = Regex.Replace(query["sortBy"], "random(?!:)", $"random:{seed}");
 | 
									query["sortBy"] = Regex.Replace(query["sortBy"], "random(?!:)", $"random:{seed}");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return new Page<TResult>(
 | 
								return new Page<TResult>(resources, Request.Path, query, limit);
 | 
				
			||||||
				resources,
 | 
					 | 
				
			||||||
				Request.Path,
 | 
					 | 
				
			||||||
				query,
 | 
					 | 
				
			||||||
				limit
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protected SearchPage<TResult> SearchPage<TResult>(SearchPage<TResult>.SearchResult result)
 | 
							protected SearchPage<TResult> SearchPage<TResult>(SearchPage<TResult>.SearchResult result)
 | 
				
			||||||
			where TResult : IResource
 | 
								where TResult : IResource
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Dictionary<string, string> query = Request.Query.ToDictionary(
 | 
								Dictionary<string, string> query = Request
 | 
				
			||||||
 | 
									.Query
 | 
				
			||||||
 | 
									.ToDictionary(
 | 
				
			||||||
					x => x.Key,
 | 
										x => x.Key,
 | 
				
			||||||
					x => x.Value.ToString(),
 | 
										x => x.Value.ToString(),
 | 
				
			||||||
					StringComparer.InvariantCultureIgnoreCase
 | 
										StringComparer.InvariantCultureIgnoreCase
 | 
				
			||||||
@ -79,7 +78,9 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			string? previous = null;
 | 
								string? previous = null;
 | 
				
			||||||
			string? next = null;
 | 
								string? next = null;
 | 
				
			||||||
			string first;
 | 
								string first;
 | 
				
			||||||
			int limit = query.TryGetValue("limit", out string? limitStr) ? int.Parse(limitStr) : new SearchPagination().Limit;
 | 
								int limit = query.TryGetValue("limit", out string? limitStr)
 | 
				
			||||||
 | 
									? int.Parse(limitStr)
 | 
				
			||||||
 | 
									: new SearchPagination().Limit;
 | 
				
			||||||
			int? skip = query.TryGetValue("skip", out string? skipStr) ? int.Parse(skipStr) : null;
 | 
								int? skip = query.TryGetValue("skip", out string? skipStr) ? int.Parse(skipStr) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (skip != null)
 | 
								if (skip != null)
 | 
				
			||||||
@ -97,13 +98,7 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			query.Remove("skip");
 | 
								query.Remove("skip");
 | 
				
			||||||
			first = Request.Path + query.ToQueryString();
 | 
								first = Request.Path + query.ToQueryString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return new SearchPage<TResult>(
 | 
								return new SearchPage<TResult>(result, self, previous, next, first);
 | 
				
			||||||
				result,
 | 
					 | 
				
			||||||
				self,
 | 
					 | 
				
			||||||
				previous,
 | 
					 | 
				
			||||||
				next,
 | 
					 | 
				
			||||||
				first
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -67,7 +67,10 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[PartialPermission(Kind.Read)]
 | 
							[PartialPermission(Kind.Read)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<T>> Get(Identifier identifier, [FromQuery] Include<T>? fields)
 | 
							public async Task<ActionResult<T>> Get(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] Include<T>? fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			T? ret = await identifier.Match(
 | 
								T? ret = await identifier.Match(
 | 
				
			||||||
				id => Repository.GetOrDefault(id, fields),
 | 
									id => Repository.GetOrDefault(id, fields),
 | 
				
			||||||
@ -116,14 +119,10 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			[FromQuery] Sort<T> sortBy,
 | 
								[FromQuery] Sort<T> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<T>? filter,
 | 
								[FromQuery] Filter<T>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<T>? fields)
 | 
								[FromQuery] Include<T>? fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<T> resources = await Repository.GetAll(
 | 
								ICollection<T> resources = await Repository.GetAll(filter, sortBy, fields, pagination);
 | 
				
			||||||
				filter,
 | 
					 | 
				
			||||||
				sortBy,
 | 
					 | 
				
			||||||
				fields,
 | 
					 | 
				
			||||||
				pagination
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -195,7 +194,9 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			if (resource.Id.HasValue)
 | 
								if (resource.Id.HasValue)
 | 
				
			||||||
				return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync);
 | 
									return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync);
 | 
				
			||||||
			if (resource.Slug == null)
 | 
								if (resource.Slug == null)
 | 
				
			||||||
				throw new ArgumentException("Either the Id or the slug of the resource has to be defined to edit it.");
 | 
									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);
 | 
								T old = await Repository.Get(resource.Slug);
 | 
				
			||||||
			return await Repository.Patch(old.Id, TryUpdateModelAsync);
 | 
								return await Repository.Patch(old.Id, TryUpdateModelAsync);
 | 
				
			||||||
@ -216,10 +217,7 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<IActionResult> Delete(Identifier identifier)
 | 
							public async Task<IActionResult> Delete(Identifier identifier)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			await identifier.Match(
 | 
								await identifier.Match(id => Repository.Delete(id), slug => Repository.Delete(slug));
 | 
				
			||||||
				id => Repository.Delete(id),
 | 
					 | 
				
			||||||
				slug => Repository.Delete(slug)
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			return NoContent();
 | 
								return NoContent();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -239,7 +237,9 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		public async Task<IActionResult> Delete([FromQuery] Filter<T> filter)
 | 
							public async Task<IActionResult> Delete([FromQuery] Filter<T> filter)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (filter == null)
 | 
								if (filter == null)
 | 
				
			||||||
				return BadRequest(new RequestError("Incule a filter to delete items, all items won't be deleted."));
 | 
									return BadRequest(
 | 
				
			||||||
 | 
										new RequestError("Incule a filter to delete items, all items won't be deleted.")
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await Repository.DeleteAll(filter);
 | 
								await Repository.DeleteAll(filter);
 | 
				
			||||||
			return NoContent();
 | 
								return NoContent();
 | 
				
			||||||
 | 
				
			|||||||
@ -49,14 +49,17 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// The repository to use as a baking store for the type <typeparamref name="T"/>.
 | 
							/// The repository to use as a baking store for the type <typeparamref name="T"/>.
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
				
			||||||
		public CrudThumbsApi(IRepository<T> repository,
 | 
							public CrudThumbsApi(IRepository<T> repository, IThumbnailsManager thumbs)
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
					 | 
				
			||||||
			: base(repository)
 | 
								: base(repository)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_thumbs = thumbs;
 | 
								_thumbs = thumbs;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private async Task<IActionResult> _GetImage(Identifier identifier, string image, ImageQuality? quality)
 | 
							private async Task<IActionResult> _GetImage(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								string image,
 | 
				
			||||||
 | 
								ImageQuality? quality
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			T? resource = await identifier.Match(
 | 
								T? resource = await identifier.Match(
 | 
				
			||||||
				id => Repository.GetOrDefault(id),
 | 
									id => Repository.GetOrDefault(id),
 | 
				
			||||||
@ -94,7 +97,10 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[PartialPermission(Kind.Read)]
 | 
							[PartialPermission(Kind.Read)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public Task<IActionResult> GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
 | 
							public Task<IActionResult> GetPoster(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] ImageQuality? quality
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return _GetImage(identifier, "poster", quality);
 | 
								return _GetImage(identifier, "poster", quality);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -134,7 +140,10 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// </response>
 | 
							/// </response>
 | 
				
			||||||
		[HttpGet("{identifier:id}/thumbnail")]
 | 
							[HttpGet("{identifier:id}/thumbnail")]
 | 
				
			||||||
		[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
 | 
							[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
 | 
				
			||||||
		public Task<IActionResult> GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
 | 
							public Task<IActionResult> GetBackdrop(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] ImageQuality? quality
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return _GetImage(identifier, "thumbnail", quality);
 | 
								return _GetImage(identifier, "thumbnail", quality);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -28,10 +28,14 @@ public class FilterBinder : IModelBinder
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
	public Task BindModelAsync(ModelBindingContext bindingContext)
 | 
						public Task BindModelAsync(ModelBindingContext bindingContext)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		ValueProviderResult fields = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
 | 
							ValueProviderResult fields = bindingContext
 | 
				
			||||||
 | 
								.ValueProvider
 | 
				
			||||||
 | 
								.GetValue(bindingContext.FieldName);
 | 
				
			||||||
		try
 | 
							try
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			object? filter = bindingContext.ModelType.GetMethod(nameof(Filter<object>.From))!
 | 
								object? filter = bindingContext
 | 
				
			||||||
 | 
									.ModelType
 | 
				
			||||||
 | 
									.GetMethod(nameof(Filter<object>.From))!
 | 
				
			||||||
				.Invoke(null, new object?[] { fields.FirstValue });
 | 
									.Invoke(null, new object?[] { fields.FirstValue });
 | 
				
			||||||
			bindingContext.Result = ModelBindingResult.Success(filter);
 | 
								bindingContext.Result = ModelBindingResult.Success(filter);
 | 
				
			||||||
			return Task.CompletedTask;
 | 
								return Task.CompletedTask;
 | 
				
			||||||
 | 
				
			|||||||
@ -31,10 +31,14 @@ public class IncludeBinder : IModelBinder
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public Task BindModelAsync(ModelBindingContext bindingContext)
 | 
						public Task BindModelAsync(ModelBindingContext bindingContext)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		ValueProviderResult fields = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
 | 
							ValueProviderResult fields = bindingContext
 | 
				
			||||||
 | 
								.ValueProvider
 | 
				
			||||||
 | 
								.GetValue(bindingContext.FieldName);
 | 
				
			||||||
		try
 | 
							try
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			object include = bindingContext.ModelType.GetMethod(nameof(Include<object>.From))!
 | 
								object include = bindingContext
 | 
				
			||||||
 | 
									.ModelType
 | 
				
			||||||
 | 
									.GetMethod(nameof(Include<object>.From))!
 | 
				
			||||||
				.Invoke(null, new object?[] { fields.FirstValue })!;
 | 
									.Invoke(null, new object?[] { fields.FirstValue })!;
 | 
				
			||||||
			bindingContext.Result = ModelBindingResult.Success(include);
 | 
								bindingContext.Result = ModelBindingResult.Success(include);
 | 
				
			||||||
			bindingContext.HttpContext.Items["fields"] = ((dynamic)include).Fields;
 | 
								bindingContext.HttpContext.Items["fields"] = ((dynamic)include).Fields;
 | 
				
			||||||
 | 
				
			|||||||
@ -47,7 +47,9 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void Configure(MvcNewtonsoftJsonOptions options)
 | 
							public void Configure(MvcNewtonsoftJsonOptions options)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			options.SerializerSettings.ContractResolver = new JsonSerializerContract(_httpContextAccessor);
 | 
								options.SerializerSettings.ContractResolver = new JsonSerializerContract(
 | 
				
			||||||
 | 
									_httpContextAccessor
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			options.SerializerSettings.Converters.Add(new PeopleRoleConverter());
 | 
								options.SerializerSettings.Converters.Add(new PeopleRoleConverter());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -51,16 +51,23 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
 | 
							protected override JsonProperty CreateProperty(
 | 
				
			||||||
 | 
								MemberInfo member,
 | 
				
			||||||
 | 
								MemberSerialization memberSerialization
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			JsonProperty property = base.CreateProperty(member, memberSerialization);
 | 
								JsonProperty property = base.CreateProperty(member, memberSerialization);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			LoadableRelationAttribute? relation = member.GetCustomAttribute<LoadableRelationAttribute>();
 | 
								LoadableRelationAttribute? relation =
 | 
				
			||||||
 | 
									member.GetCustomAttribute<LoadableRelationAttribute>();
 | 
				
			||||||
			if (relation != null)
 | 
								if (relation != null)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				property.ShouldSerialize = _ =>
 | 
									property.ShouldSerialize = _ =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					if (_httpContextAccessor.HttpContext!.Items["fields"] is not ICollection<string> fields)
 | 
										if (
 | 
				
			||||||
 | 
											_httpContextAccessor.HttpContext!.Items["fields"]
 | 
				
			||||||
 | 
											is not ICollection<string> fields
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
						return false;
 | 
											return false;
 | 
				
			||||||
					return fields.Contains(member.Name);
 | 
										return fields.Contains(member.Name);
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
@ -73,13 +80,20 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			return property;
 | 
								return property;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
 | 
							protected override IList<JsonProperty> CreateProperties(
 | 
				
			||||||
 | 
								Type type,
 | 
				
			||||||
 | 
								MemberSerialization memberSerialization
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
 | 
								IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (properties.All(x => x.PropertyName != "kind") && type.IsAssignableTo(typeof(IResource)))
 | 
								if (
 | 
				
			||||||
 | 
									properties.All(x => x.PropertyName != "kind")
 | 
				
			||||||
 | 
									&& type.IsAssignableTo(typeof(IResource))
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				properties.Add(new JsonProperty()
 | 
									properties.Add(
 | 
				
			||||||
 | 
										new JsonProperty()
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						DeclaringType = type,
 | 
											DeclaringType = type,
 | 
				
			||||||
						PropertyName = "kind",
 | 
											PropertyName = "kind",
 | 
				
			||||||
@ -89,7 +103,8 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
						Readable = true,
 | 
											Readable = true,
 | 
				
			||||||
						Writable = false,
 | 
											Writable = false,
 | 
				
			||||||
						TypeNameHandling = TypeNameHandling.None,
 | 
											TypeNameHandling = TypeNameHandling.None,
 | 
				
			||||||
				});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return properties;
 | 
								return properties;
 | 
				
			||||||
@ -104,11 +119,10 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
				_value = value;
 | 
									_value = value;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			public object GetValue(object target)
 | 
								public object GetValue(object target) => _value;
 | 
				
			||||||
				=> _value;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			public void SetValue(object target, object? value)
 | 
								public void SetValue(object target, object? value) =>
 | 
				
			||||||
				=> throw new NotImplementedException();
 | 
									throw new NotImplementedException();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,11 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
	public class PeopleRoleConverter : JsonConverter<PeopleRole>
 | 
						public class PeopleRoleConverter : JsonConverter<PeopleRole>
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override void WriteJson(JsonWriter writer, PeopleRole? value, JsonSerializer serializer)
 | 
							public override void WriteJson(
 | 
				
			||||||
 | 
								JsonWriter writer,
 | 
				
			||||||
 | 
								PeopleRole? value,
 | 
				
			||||||
 | 
								JsonSerializer serializer
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			// if (value == null)
 | 
								// if (value == null)
 | 
				
			||||||
			// {
 | 
								// {
 | 
				
			||||||
@ -58,11 +62,13 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public override PeopleRole ReadJson(JsonReader reader,
 | 
							public override PeopleRole ReadJson(
 | 
				
			||||||
 | 
								JsonReader reader,
 | 
				
			||||||
			Type objectType,
 | 
								Type objectType,
 | 
				
			||||||
			PeopleRole? existingValue,
 | 
								PeopleRole? existingValue,
 | 
				
			||||||
			bool hasExistingValue,
 | 
								bool hasExistingValue,
 | 
				
			||||||
			JsonSerializer serializer)
 | 
								JsonSerializer serializer
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			throw new NotImplementedException();
 | 
								throw new NotImplementedException();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -32,14 +32,18 @@ public class SortBinder : IModelBinder
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public Task BindModelAsync(ModelBindingContext bindingContext)
 | 
						public Task BindModelAsync(ModelBindingContext bindingContext)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		ValueProviderResult sortBy = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
 | 
							ValueProviderResult sortBy = bindingContext
 | 
				
			||||||
 | 
								.ValueProvider
 | 
				
			||||||
 | 
								.GetValue(bindingContext.FieldName);
 | 
				
			||||||
		uint seed = BitConverter.ToUInt32(
 | 
							uint seed = BitConverter.ToUInt32(
 | 
				
			||||||
			BitConverter.GetBytes(_rng.Next(int.MinValue, int.MaxValue)),
 | 
								BitConverter.GetBytes(_rng.Next(int.MinValue, int.MaxValue)),
 | 
				
			||||||
			0
 | 
								0
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
		try
 | 
							try
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			object sort = bindingContext.ModelType.GetMethod(nameof(Sort<Movie>.From))!
 | 
								object sort = bindingContext
 | 
				
			||||||
 | 
									.ModelType
 | 
				
			||||||
 | 
									.GetMethod(nameof(Sort<Movie>.From))!
 | 
				
			||||||
				.Invoke(null, new object?[] { sortBy.FirstValue, seed })!;
 | 
									.Invoke(null, new object?[] { sortBy.FirstValue, seed })!;
 | 
				
			||||||
			bindingContext.Result = ModelBindingResult.Success(sort);
 | 
								bindingContext.Result = ModelBindingResult.Success(sort);
 | 
				
			||||||
			bindingContext.HttpContext.Items["seed"] = seed;
 | 
								bindingContext.HttpContext.Items["seed"] = seed;
 | 
				
			||||||
 | 
				
			|||||||
@ -47,8 +47,7 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// The library manager used to modify or retrieve information about the data store.
 | 
							/// The library manager used to modify or retrieve information about the data store.
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
				
			||||||
		public StaffApi(ILibraryManager libraryManager,
 | 
							public StaffApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
					 | 
				
			||||||
			: base(libraryManager.People, thumbs)
 | 
								: base(libraryManager.People, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_libraryManager = libraryManager;
 | 
								_libraryManager = libraryManager;
 | 
				
			||||||
 | 
				
			|||||||
@ -77,20 +77,30 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Show>>> GetShows(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Show> sortBy,
 | 
								[FromQuery] Sort<Show> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Show>? filter,
 | 
								[FromQuery] Filter<Show>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Show> fields)
 | 
								[FromQuery] Include<Show> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Show> resources = await _libraryManager.Shows.GetAll(
 | 
								ICollection<Show> resources = await _libraryManager
 | 
				
			||||||
				Filter.And(filter, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug)),
 | 
									.Shows
 | 
				
			||||||
 | 
									.GetAll(
 | 
				
			||||||
 | 
										Filter.And(
 | 
				
			||||||
 | 
											filter,
 | 
				
			||||||
 | 
											identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
					sortBy,
 | 
										sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Studios.GetOrDefault(identifier.IsSame<Studio>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Studios.GetOrDefault(identifier.IsSame<Studio>()) == null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -46,10 +46,12 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		private readonly CollectionRepository _collections;
 | 
							private readonly CollectionRepository _collections;
 | 
				
			||||||
		private readonly LibraryItemRepository _items;
 | 
							private readonly LibraryItemRepository _items;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public CollectionApi(ILibraryManager libraryManager,
 | 
							public CollectionApi(
 | 
				
			||||||
 | 
								ILibraryManager libraryManager,
 | 
				
			||||||
			CollectionRepository collections,
 | 
								CollectionRepository collections,
 | 
				
			||||||
			LibraryItemRepository items,
 | 
								LibraryItemRepository items,
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
								IThumbnailsManager thumbs
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			: base(libraryManager.Collections, thumbs)
 | 
								: base(libraryManager.Collections, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_libraryManager = libraryManager;
 | 
								_libraryManager = libraryManager;
 | 
				
			||||||
@ -139,11 +141,13 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<ILibraryItem>>> GetItems(Identifier identifier,
 | 
							public async Task<ActionResult<Page<ILibraryItem>>> GetItems(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<ILibraryItem> sortBy,
 | 
								[FromQuery] Sort<ILibraryItem> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<ILibraryItem>? filter,
 | 
								[FromQuery] Filter<ILibraryItem>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<ILibraryItem>? fields)
 | 
								[FromQuery] Include<ILibraryItem>? fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Guid collectionId = await identifier.Match(
 | 
								Guid collectionId = await identifier.Match(
 | 
				
			||||||
				id => Task.FromResult(id),
 | 
									id => Task.FromResult(id),
 | 
				
			||||||
@ -152,12 +156,18 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			ICollection<ILibraryItem> resources = await _items.GetAllOfCollection(
 | 
								ICollection<ILibraryItem> resources = await _items.GetAllOfCollection(
 | 
				
			||||||
				collectionId,
 | 
									collectionId,
 | 
				
			||||||
				filter,
 | 
									filter,
 | 
				
			||||||
				sortBy == new Sort<ILibraryItem>.Default() ? new Sort<ILibraryItem>.By(nameof(Movie.AirDate)) : sortBy,
 | 
									sortBy == new Sort<ILibraryItem>.Default()
 | 
				
			||||||
 | 
										? new Sort<ILibraryItem>.By(nameof(Movie.AirDate))
 | 
				
			||||||
 | 
										: sortBy,
 | 
				
			||||||
				fields,
 | 
									fields,
 | 
				
			||||||
				pagination
 | 
									pagination
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
 | 
				
			||||||
 | 
										== null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -182,20 +192,31 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Show>>> GetShows(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Show> sortBy,
 | 
								[FromQuery] Sort<Show> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Show>? filter,
 | 
								[FromQuery] Filter<Show>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Show>? fields)
 | 
								[FromQuery] Include<Show>? fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Show> resources = await _libraryManager.Shows.GetAll(
 | 
								ICollection<Show> resources = await _libraryManager
 | 
				
			||||||
				Filter.And(filter, identifier.IsContainedIn<Show, Collection>(x => x.Collections)),
 | 
									.Shows
 | 
				
			||||||
 | 
									.GetAll(
 | 
				
			||||||
 | 
										Filter.And(
 | 
				
			||||||
 | 
											filter,
 | 
				
			||||||
 | 
											identifier.IsContainedIn<Show, Collection>(x => x.Collections)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
					sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
 | 
										sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
 | 
				
			||||||
 | 
										== null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -220,20 +241,33 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Movie>>> GetMovies(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Movie>>> GetMovies(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Movie> sortBy,
 | 
								[FromQuery] Sort<Movie> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Movie>? filter,
 | 
								[FromQuery] Filter<Movie>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Movie>? fields)
 | 
								[FromQuery] Include<Movie>? fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Movie> resources = await _libraryManager.Movies.GetAll(
 | 
								ICollection<Movie> resources = await _libraryManager
 | 
				
			||||||
				Filter.And(filter, identifier.IsContainedIn<Movie, Collection>(x => x.Collections)),
 | 
									.Movies
 | 
				
			||||||
				sortBy == new Sort<Movie>.Default() ? new Sort<Movie>.By(x => x.AirDate) : sortBy,
 | 
									.GetAll(
 | 
				
			||||||
 | 
										Filter.And(
 | 
				
			||||||
 | 
											filter,
 | 
				
			||||||
 | 
											identifier.IsContainedIn<Movie, Collection>(x => x.Collections)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
										sortBy == new Sort<Movie>.Default()
 | 
				
			||||||
 | 
											? new Sort<Movie>.By(x => x.AirDate)
 | 
				
			||||||
 | 
											: sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
 | 
				
			||||||
 | 
										== null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -52,8 +52,7 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// The library manager used to modify or retrieve information in the data store.
 | 
							/// The library manager used to modify or retrieve information in the data store.
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
 | 
							/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
 | 
				
			||||||
		public EpisodeApi(ILibraryManager libraryManager,
 | 
							public EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails)
 | 
				
			||||||
			IThumbnailsManager thumbnails)
 | 
					 | 
				
			||||||
			: base(libraryManager.Episodes, thumbnails)
 | 
								: base(libraryManager.Episodes, thumbnails)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_libraryManager = libraryManager;
 | 
								_libraryManager = libraryManager;
 | 
				
			||||||
@ -73,9 +72,14 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[PartialPermission(Kind.Read)]
 | 
							[PartialPermission(Kind.Read)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Show>> GetShow(Identifier identifier, [FromQuery] Include<Show> fields)
 | 
							public async Task<ActionResult<Show>> GetShow(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] Include<Show> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await _libraryManager.Shows.Get(identifier.IsContainedIn<Show, Episode>(x => x.Episodes!), fields);
 | 
								return await _libraryManager
 | 
				
			||||||
 | 
									.Shows
 | 
				
			||||||
 | 
									.Get(identifier.IsContainedIn<Show, Episode>(x => x.Episodes!), fields);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -94,21 +98,21 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
							[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Season>> GetSeason(Identifier identifier, [FromQuery] Include<Season> fields)
 | 
							public async Task<ActionResult<Season>> GetSeason(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] Include<Season> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Season? ret = await _libraryManager.Seasons.GetOrDefault(
 | 
								Season? ret = await _libraryManager
 | 
				
			||||||
				identifier.IsContainedIn<Season, Episode>(x => x.Episodes!),
 | 
									.Seasons
 | 
				
			||||||
				fields
 | 
									.GetOrDefault(identifier.IsContainedIn<Season, Episode>(x => x.Episodes!), fields);
 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			if (ret != null)
 | 
								if (ret != null)
 | 
				
			||||||
				return ret;
 | 
									return ret;
 | 
				
			||||||
			Episode? episode = await identifier.Match(
 | 
								Episode? episode = await identifier.Match(
 | 
				
			||||||
				id => _libraryManager.Episodes.GetOrDefault(id),
 | 
									id => _libraryManager.Episodes.GetOrDefault(id),
 | 
				
			||||||
				slug => _libraryManager.Episodes.GetOrDefault(slug)
 | 
									slug => _libraryManager.Episodes.GetOrDefault(slug)
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			return episode == null
 | 
								return episode == null ? NotFound() : NoContent();
 | 
				
			||||||
				? NotFound()
 | 
					 | 
				
			||||||
				: NoContent();
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -154,18 +158,19 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
							[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<EpisodeWatchStatus?> SetWatchStatus(Identifier identifier, WatchStatus status, int? watchedTime)
 | 
							public async Task<EpisodeWatchStatus?> SetWatchStatus(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								WatchStatus status,
 | 
				
			||||||
 | 
								int? watchedTime
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Guid id = await identifier.Match(
 | 
								Guid id = await identifier.Match(
 | 
				
			||||||
				id => Task.FromResult(id),
 | 
									id => Task.FromResult(id),
 | 
				
			||||||
				async slug => (await _libraryManager.Episodes.Get(slug)).Id
 | 
									async slug => (await _libraryManager.Episodes.Get(slug)).Id
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			return await _libraryManager.WatchStatus.SetEpisodeStatus(
 | 
								return await _libraryManager
 | 
				
			||||||
				id,
 | 
									.WatchStatus
 | 
				
			||||||
				User.GetIdOrThrow(),
 | 
									.SetEpisodeStatus(id, User.GetIdOrThrow(), status, watchedTime);
 | 
				
			||||||
				status,
 | 
					 | 
				
			||||||
				watchedTime
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
 | 
				
			|||||||
@ -54,8 +54,7 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// The library manager used to modify or retrieve information about the data store.
 | 
							/// The library manager used to modify or retrieve information about the data store.
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
				
			||||||
		public MovieApi(ILibraryManager libraryManager,
 | 
							public MovieApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
					 | 
				
			||||||
			: base(libraryManager.Movies, thumbs)
 | 
								: base(libraryManager.Movies, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_libraryManager = libraryManager;
 | 
								_libraryManager = libraryManager;
 | 
				
			||||||
@ -109,9 +108,14 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[PartialPermission(Kind.Read)]
 | 
							[PartialPermission(Kind.Read)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Studio>> GetStudio(Identifier identifier, [FromQuery] Include<Studio> fields)
 | 
							public async Task<ActionResult<Studio>> GetStudio(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] Include<Studio> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await _libraryManager.Studios.Get(identifier.IsContainedIn<Studio, Movie>(x => x.Movies!), fields);
 | 
								return await _libraryManager
 | 
				
			||||||
 | 
									.Studios
 | 
				
			||||||
 | 
									.Get(identifier.IsContainedIn<Studio, Movie>(x => x.Movies!), fields);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -134,20 +138,27 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Collection>>> GetCollections(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Collection> sortBy,
 | 
								[FromQuery] Sort<Collection> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Collection>? filter,
 | 
								[FromQuery] Filter<Collection>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Collection> fields)
 | 
								[FromQuery] Include<Collection> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
 | 
								ICollection<Collection> resources = await _libraryManager
 | 
				
			||||||
 | 
									.Collections
 | 
				
			||||||
 | 
									.GetAll(
 | 
				
			||||||
					Filter.And(filter, identifier.IsContainedIn<Collection, Movie>(x => x.Movies)),
 | 
										Filter.And(filter, identifier.IsContainedIn<Collection, Movie>(x => x.Movies)),
 | 
				
			||||||
					sortBy,
 | 
										sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Movies.GetOrDefault(identifier.IsSame<Movie>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Movies.GetOrDefault(identifier.IsSame<Movie>()) == null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -196,18 +207,19 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
							[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<MovieWatchStatus?> SetWatchStatus(Identifier identifier, WatchStatus status, int? watchedTime)
 | 
							public async Task<MovieWatchStatus?> SetWatchStatus(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								WatchStatus status,
 | 
				
			||||||
 | 
								int? watchedTime
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Guid id = await identifier.Match(
 | 
								Guid id = await identifier.Match(
 | 
				
			||||||
				id => Task.FromResult(id),
 | 
									id => Task.FromResult(id),
 | 
				
			||||||
				async slug => (await _libraryManager.Movies.Get(slug)).Id
 | 
									async slug => (await _libraryManager.Movies.Get(slug)).Id
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			return await _libraryManager.WatchStatus.SetMovieStatus(
 | 
								return await _libraryManager
 | 
				
			||||||
				id,
 | 
									.WatchStatus
 | 
				
			||||||
				User.GetIdOrThrow(),
 | 
									.SetMovieStatus(id, User.GetIdOrThrow(), status, watchedTime);
 | 
				
			||||||
				status,
 | 
					 | 
				
			||||||
				watchedTime
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
 | 
				
			|||||||
@ -36,7 +36,6 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
	public class NewsApi : CrudThumbsApi<INews>
 | 
						public class NewsApi : CrudThumbsApi<INews>
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		public NewsApi(IRepository<INews> news, IThumbnailsManager thumbs)
 | 
							public NewsApi(IRepository<INews> news, IThumbnailsManager thumbs)
 | 
				
			||||||
			: base(news, thumbs)
 | 
								: base(news, thumbs) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -66,9 +66,12 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			[FromQuery] string? q,
 | 
								[FromQuery] string? q,
 | 
				
			||||||
			[FromQuery] Sort<Collection> sortBy,
 | 
								[FromQuery] Sort<Collection> sortBy,
 | 
				
			||||||
			[FromQuery] SearchPagination pagination,
 | 
								[FromQuery] SearchPagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Collection> fields)
 | 
								[FromQuery] Include<Collection> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return SearchPage(await _searchManager.SearchCollections(q, sortBy, pagination, fields));
 | 
								return SearchPage(
 | 
				
			||||||
 | 
									await _searchManager.SearchCollections(q, sortBy, pagination, fields)
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -91,7 +94,8 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			[FromQuery] string? q,
 | 
								[FromQuery] string? q,
 | 
				
			||||||
			[FromQuery] Sort<Show> sortBy,
 | 
								[FromQuery] Sort<Show> sortBy,
 | 
				
			||||||
			[FromQuery] SearchPagination pagination,
 | 
								[FromQuery] SearchPagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Show> fields)
 | 
								[FromQuery] Include<Show> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return SearchPage(await _searchManager.SearchShows(q, sortBy, pagination, fields));
 | 
								return SearchPage(await _searchManager.SearchShows(q, sortBy, pagination, fields));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -116,7 +120,8 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			[FromQuery] string? q,
 | 
								[FromQuery] string? q,
 | 
				
			||||||
			[FromQuery] Sort<Movie> sortBy,
 | 
								[FromQuery] Sort<Movie> sortBy,
 | 
				
			||||||
			[FromQuery] SearchPagination pagination,
 | 
								[FromQuery] SearchPagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Movie> fields)
 | 
								[FromQuery] Include<Movie> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return SearchPage(await _searchManager.SearchMovies(q, sortBy, pagination, fields));
 | 
								return SearchPage(await _searchManager.SearchMovies(q, sortBy, pagination, fields));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -141,7 +146,8 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			[FromQuery] string? q,
 | 
								[FromQuery] string? q,
 | 
				
			||||||
			[FromQuery] Sort<ILibraryItem> sortBy,
 | 
								[FromQuery] Sort<ILibraryItem> sortBy,
 | 
				
			||||||
			[FromQuery] SearchPagination pagination,
 | 
								[FromQuery] SearchPagination pagination,
 | 
				
			||||||
			[FromQuery] Include<ILibraryItem> fields)
 | 
								[FromQuery] Include<ILibraryItem> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return SearchPage(await _searchManager.SearchItems(q, sortBy, pagination, fields));
 | 
								return SearchPage(await _searchManager.SearchItems(q, sortBy, pagination, fields));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -166,7 +172,8 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			[FromQuery] string? q,
 | 
								[FromQuery] string? q,
 | 
				
			||||||
			[FromQuery] Sort<Episode> sortBy,
 | 
								[FromQuery] Sort<Episode> sortBy,
 | 
				
			||||||
			[FromQuery] SearchPagination pagination,
 | 
								[FromQuery] SearchPagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Episode> fields)
 | 
								[FromQuery] Include<Episode> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return SearchPage(await _searchManager.SearchEpisodes(q, sortBy, pagination, fields));
 | 
								return SearchPage(await _searchManager.SearchEpisodes(q, sortBy, pagination, fields));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -191,7 +198,8 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
			[FromQuery] string? q,
 | 
								[FromQuery] string? q,
 | 
				
			||||||
			[FromQuery] Sort<Studio> sortBy,
 | 
								[FromQuery] Sort<Studio> sortBy,
 | 
				
			||||||
			[FromQuery] SearchPagination pagination,
 | 
								[FromQuery] SearchPagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Studio> fields)
 | 
								[FromQuery] Include<Studio> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return SearchPage(await _searchManager.SearchStudios(q, sortBy, pagination, fields));
 | 
								return SearchPage(await _searchManager.SearchStudios(q, sortBy, pagination, fields));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -52,8 +52,7 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// The library manager used to modify or retrieve information in the data store.
 | 
							/// The library manager used to modify or retrieve information in the data store.
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
				
			||||||
		public SeasonApi(ILibraryManager libraryManager,
 | 
							public SeasonApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
					 | 
				
			||||||
			: base(libraryManager.Seasons, thumbs)
 | 
								: base(libraryManager.Seasons, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_libraryManager = libraryManager;
 | 
								_libraryManager = libraryManager;
 | 
				
			||||||
@ -79,20 +78,30 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Episode>>> GetEpisode(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Episode>>> GetEpisode(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Episode> sortBy,
 | 
								[FromQuery] Sort<Episode> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Episode>? filter,
 | 
								[FromQuery] Filter<Episode>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Episode> fields)
 | 
								[FromQuery] Include<Episode> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
 | 
								ICollection<Episode> resources = await _libraryManager
 | 
				
			||||||
				Filter.And(filter, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)),
 | 
									.Episodes
 | 
				
			||||||
 | 
									.GetAll(
 | 
				
			||||||
 | 
										Filter.And(
 | 
				
			||||||
 | 
											filter,
 | 
				
			||||||
 | 
											identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
					sortBy,
 | 
										sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -111,12 +120,14 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[PartialPermission(Kind.Read)]
 | 
							[PartialPermission(Kind.Read)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Show>> GetShow(Identifier identifier, [FromQuery] Include<Show> fields)
 | 
							public async Task<ActionResult<Show>> GetShow(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] Include<Show> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Show? ret = await _libraryManager.Shows.GetOrDefault(
 | 
								Show? ret = await _libraryManager
 | 
				
			||||||
				identifier.IsContainedIn<Show, Season>(x => x.Seasons!),
 | 
									.Shows
 | 
				
			||||||
				fields
 | 
									.GetOrDefault(identifier.IsContainedIn<Show, Season>(x => x.Seasons!), fields);
 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			if (ret == null)
 | 
								if (ret == null)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return ret;
 | 
								return ret;
 | 
				
			||||||
 | 
				
			|||||||
@ -54,8 +54,7 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		/// The library manager used to modify or retrieve information about the data store.
 | 
							/// The library manager used to modify or retrieve information about the data store.
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
							/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
 | 
				
			||||||
		public ShowApi(ILibraryManager libraryManager,
 | 
							public ShowApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
 | 
				
			||||||
			IThumbnailsManager thumbs)
 | 
					 | 
				
			||||||
			: base(libraryManager.Shows, thumbs)
 | 
								: base(libraryManager.Shows, thumbs)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_libraryManager = libraryManager;
 | 
								_libraryManager = libraryManager;
 | 
				
			||||||
@ -81,20 +80,30 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Season>>> GetSeasons(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Season>>> GetSeasons(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Season> sortBy,
 | 
								[FromQuery] Sort<Season> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Season>? filter,
 | 
								[FromQuery] Filter<Season>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Season> fields)
 | 
								[FromQuery] Include<Season> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Season> resources = await _libraryManager.Seasons.GetAll(
 | 
								ICollection<Season> resources = await _libraryManager
 | 
				
			||||||
				Filter.And(filter, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)),
 | 
									.Seasons
 | 
				
			||||||
 | 
									.GetAll(
 | 
				
			||||||
 | 
										Filter.And(
 | 
				
			||||||
 | 
											filter,
 | 
				
			||||||
 | 
											identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
					sortBy,
 | 
										sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -119,20 +128,30 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Episode>>> GetEpisodes(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Episode>>> GetEpisodes(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Episode> sortBy,
 | 
								[FromQuery] Sort<Episode> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Episode>? filter,
 | 
								[FromQuery] Filter<Episode>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Episode> fields)
 | 
								[FromQuery] Include<Episode> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
 | 
								ICollection<Episode> resources = await _libraryManager
 | 
				
			||||||
				Filter.And(filter, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)),
 | 
									.Episodes
 | 
				
			||||||
 | 
									.GetAll(
 | 
				
			||||||
 | 
										Filter.And(
 | 
				
			||||||
 | 
											filter,
 | 
				
			||||||
 | 
											identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
					sortBy,
 | 
										sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -186,9 +205,14 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[PartialPermission(Kind.Read)]
 | 
							[PartialPermission(Kind.Read)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Studio>> GetStudio(Identifier identifier, [FromQuery] Include<Studio> fields)
 | 
							public async Task<ActionResult<Studio>> GetStudio(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								[FromQuery] Include<Studio> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return await _libraryManager.Studios.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows!), fields);
 | 
								return await _libraryManager
 | 
				
			||||||
 | 
									.Studios
 | 
				
			||||||
 | 
									.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows!), fields);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -211,20 +235,27 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status200OK)]
 | 
							[ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
 | 
							public async Task<ActionResult<Page<Collection>>> GetCollections(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
			[FromQuery] Sort<Collection> sortBy,
 | 
								[FromQuery] Sort<Collection> sortBy,
 | 
				
			||||||
			[FromQuery] Filter<Collection>? filter,
 | 
								[FromQuery] Filter<Collection>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<Collection> fields)
 | 
								[FromQuery] Include<Collection> fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
 | 
								ICollection<Collection> resources = await _libraryManager
 | 
				
			||||||
 | 
									.Collections
 | 
				
			||||||
 | 
									.GetAll(
 | 
				
			||||||
					Filter.And(filter, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
 | 
										Filter.And(filter, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
 | 
				
			||||||
					sortBy,
 | 
										sortBy,
 | 
				
			||||||
					fields,
 | 
										fields,
 | 
				
			||||||
					pagination
 | 
										pagination
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
 | 
								if (
 | 
				
			||||||
 | 
									!resources.Any()
 | 
				
			||||||
 | 
									&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				return NotFound();
 | 
									return NotFound();
 | 
				
			||||||
			return Page(resources, pagination.Limit);
 | 
								return Page(resources, pagination.Limit);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -271,17 +302,16 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
							[ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
							[ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
				
			||||||
		[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
							[ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
		public async Task<ShowWatchStatus?> SetWatchStatus(Identifier identifier, WatchStatus status)
 | 
							public async Task<ShowWatchStatus?> SetWatchStatus(
 | 
				
			||||||
 | 
								Identifier identifier,
 | 
				
			||||||
 | 
								WatchStatus status
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Guid id = await identifier.Match(
 | 
								Guid id = await identifier.Match(
 | 
				
			||||||
				id => Task.FromResult(id),
 | 
									id => Task.FromResult(id),
 | 
				
			||||||
				async slug => (await _libraryManager.Shows.Get(slug)).Id
 | 
									async slug => (await _libraryManager.Shows.Get(slug)).Id
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			return await _libraryManager.WatchStatus.SetShowStatus(
 | 
								return await _libraryManager.WatchStatus.SetShowStatus(id, User.GetIdOrThrow(), status);
 | 
				
			||||||
				id,
 | 
					 | 
				
			||||||
				User.GetIdOrThrow(),
 | 
					 | 
				
			||||||
				status
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,8 @@ namespace Kyoo.Core.Api
 | 
				
			|||||||
		public async Task<ActionResult<Page<IWatchlist>>> GetAll(
 | 
							public async Task<ActionResult<Page<IWatchlist>>> GetAll(
 | 
				
			||||||
			[FromQuery] Filter<IWatchlist>? filter,
 | 
								[FromQuery] Filter<IWatchlist>? filter,
 | 
				
			||||||
			[FromQuery] Pagination pagination,
 | 
								[FromQuery] Pagination pagination,
 | 
				
			||||||
			[FromQuery] Include<IWatchlist>? fields)
 | 
								[FromQuery] Include<IWatchlist>? fields
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ICollection<IWatchlist> resources = await _repository.GetAll(
 | 
								ICollection<IWatchlist> resources = await _repository.GetAll(
 | 
				
			||||||
				filter,
 | 
									filter,
 | 
				
			||||||
 | 
				
			|||||||
@ -96,12 +96,10 @@ namespace Kyoo.Host
 | 
				
			|||||||
			_logger = Log.Logger.ForContext<Application>();
 | 
								_logger = Log.Logger.ForContext<Application>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			AppDomain.CurrentDomain.ProcessExit += (_, _) => Log.CloseAndFlush();
 | 
								AppDomain.CurrentDomain.ProcessExit += (_, _) => Log.CloseAndFlush();
 | 
				
			||||||
			AppDomain.CurrentDomain.UnhandledException += (_, ex)
 | 
								AppDomain.CurrentDomain.UnhandledException += (_, ex) =>
 | 
				
			||||||
				=> Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception");
 | 
									Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			IHost host = _CreateWebHostBuilder(args)
 | 
								IHost host = _CreateWebHostBuilder(args).ConfigureContainer(configure).Build();
 | 
				
			||||||
				.ConfigureContainer(configure)
 | 
					 | 
				
			||||||
				.Build();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await using (AsyncServiceScope scope = host.Services.CreateAsyncScope())
 | 
								await using (AsyncServiceScope scope = host.Services.CreateAsyncScope())
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -122,8 +120,14 @@ namespace Kyoo.Host
 | 
				
			|||||||
			try
 | 
								try
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				CoreModule.Services = host.Services;
 | 
									CoreModule.Services = host.Services;
 | 
				
			||||||
				_logger.Information("Version: {Version}", Assembly.GetExecutingAssembly().GetName().Version.ToString(3));
 | 
									_logger.Information(
 | 
				
			||||||
				_logger.Information("Data directory: {DataDirectory}", Environment.CurrentDirectory);
 | 
										"Version: {Version}",
 | 
				
			||||||
 | 
										Assembly.GetExecutingAssembly().GetName().Version.ToString(3)
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
									_logger.Information(
 | 
				
			||||||
 | 
										"Data directory: {DataDirectory}",
 | 
				
			||||||
 | 
										Environment.CurrentDirectory
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
				await host.RunAsync(cancellationToken);
 | 
									await host.RunAsync(cancellationToken);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			catch (Exception ex)
 | 
								catch (Exception ex)
 | 
				
			||||||
@ -146,12 +150,25 @@ namespace Kyoo.Host
 | 
				
			|||||||
				.ConfigureAppConfiguration(x => _SetupConfig(x, args))
 | 
									.ConfigureAppConfiguration(x => _SetupConfig(x, args))
 | 
				
			||||||
				.UseSerilog((host, services, builder) => _ConfigureLogging(builder))
 | 
									.UseSerilog((host, services, builder) => _ConfigureLogging(builder))
 | 
				
			||||||
				.ConfigureServices(x => x.AddRouting())
 | 
									.ConfigureServices(x => x.AddRouting())
 | 
				
			||||||
				.ConfigureWebHost(x => x
 | 
									.ConfigureWebHost(
 | 
				
			||||||
					.UseKestrel(options => { options.AddServerHeader = false; })
 | 
										x =>
 | 
				
			||||||
 | 
											x.UseKestrel(options =>
 | 
				
			||||||
 | 
												{
 | 
				
			||||||
 | 
													options.AddServerHeader = false;
 | 
				
			||||||
 | 
												})
 | 
				
			||||||
							.UseIIS()
 | 
												.UseIIS()
 | 
				
			||||||
							.UseIISIntegration()
 | 
												.UseIISIntegration()
 | 
				
			||||||
					.UseUrls(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000")
 | 
												.UseUrls(
 | 
				
			||||||
					.UseStartup(host => PluginsStartup.FromWebHost(host, new LoggerFactory().AddSerilog()))
 | 
													Environment.GetEnvironmentVariable("KYOO_BIND_URL")
 | 
				
			||||||
 | 
														?? "http://*:5000"
 | 
				
			||||||
 | 
												)
 | 
				
			||||||
 | 
												.UseStartup(
 | 
				
			||||||
 | 
													host =>
 | 
				
			||||||
 | 
														PluginsStartup.FromWebHost(
 | 
				
			||||||
 | 
															host,
 | 
				
			||||||
 | 
															new LoggerFactory().AddSerilog()
 | 
				
			||||||
 | 
														)
 | 
				
			||||||
 | 
												)
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -179,13 +196,20 @@ namespace Kyoo.Host
 | 
				
			|||||||
				"[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 25} "
 | 
									"[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 25} "
 | 
				
			||||||
				+ "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}";
 | 
									+ "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}";
 | 
				
			||||||
			builder
 | 
								builder
 | 
				
			||||||
				.MinimumLevel.Warning()
 | 
									.MinimumLevel
 | 
				
			||||||
				.MinimumLevel.Override("Kyoo", LogEventLevel.Verbose)
 | 
									.Warning()
 | 
				
			||||||
				.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Verbose)
 | 
									.MinimumLevel
 | 
				
			||||||
				.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Fatal)
 | 
									.Override("Kyoo", LogEventLevel.Verbose)
 | 
				
			||||||
				.WriteTo.Console(new ExpressionTemplate(template, theme: TemplateTheme.Code))
 | 
									.MinimumLevel
 | 
				
			||||||
				.Enrich.WithThreadId()
 | 
									.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Verbose)
 | 
				
			||||||
				.Enrich.FromLogContext();
 | 
									.MinimumLevel
 | 
				
			||||||
 | 
									.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Fatal)
 | 
				
			||||||
 | 
									.WriteTo
 | 
				
			||||||
 | 
									.Console(new ExpressionTemplate(template, theme: TemplateTheme.Code))
 | 
				
			||||||
 | 
									.Enrich
 | 
				
			||||||
 | 
									.WithThreadId()
 | 
				
			||||||
 | 
									.Enrich
 | 
				
			||||||
 | 
									.FromLogContext();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -51,8 +51,7 @@ namespace Kyoo.Host.Controllers
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="provider">A service container to allow initialization of plugins</param>
 | 
							/// <param name="provider">A service container to allow initialization of plugins</param>
 | 
				
			||||||
		/// <param name="logger">The logger used by this class.</param>
 | 
							/// <param name="logger">The logger used by this class.</param>
 | 
				
			||||||
		public PluginManager(IServiceProvider provider,
 | 
							public PluginManager(IServiceProvider provider, ILogger<PluginManager> logger)
 | 
				
			||||||
			ILogger<PluginManager> logger)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			_provider = provider;
 | 
								_provider = provider;
 | 
				
			||||||
			_logger = logger;
 | 
								_logger = logger;
 | 
				
			||||||
@ -86,7 +85,8 @@ namespace Kyoo.Host.Controllers
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void LoadPlugins(params Type[] plugins)
 | 
							public void LoadPlugins(params Type[] plugins)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			LoadPlugins(plugins
 | 
								LoadPlugins(
 | 
				
			||||||
 | 
									plugins
 | 
				
			||||||
					.Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x))
 | 
										.Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x))
 | 
				
			||||||
					.ToArray()
 | 
										.ToArray()
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
 | 
				
			|||||||
@ -55,9 +55,7 @@ namespace Kyoo.Host
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public IEnumerable<IStartupAction> ConfigureSteps => new[]
 | 
							public IEnumerable<IStartupAction> ConfigureSteps =>
 | 
				
			||||||
		{
 | 
								new[] { SA.New<IApplicationBuilder>(app => app.UseSerilogRequestLogging(), SA.Before) };
 | 
				
			||||||
			SA.New<IApplicationBuilder>(app => app.UseSerilogRequestLogging(), SA.Before)
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -79,14 +79,11 @@ namespace Kyoo.Host
 | 
				
			|||||||
		/// The logger factory used to log while the application is setting itself up.
 | 
							/// The logger factory used to log while the application is setting itself up.
 | 
				
			||||||
		/// </param>
 | 
							/// </param>
 | 
				
			||||||
		/// <returns>A new <see cref="PluginsStartup"/>.</returns>
 | 
							/// <returns>A new <see cref="PluginsStartup"/>.</returns>
 | 
				
			||||||
		public static PluginsStartup FromWebHost(WebHostBuilderContext host,
 | 
							public static PluginsStartup FromWebHost(WebHostBuilderContext host, ILoggerFactory logger)
 | 
				
			||||||
			ILoggerFactory logger)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			HostServiceProvider hostProvider = new(host.HostingEnvironment, host.Configuration, logger);
 | 
								HostServiceProvider hostProvider =
 | 
				
			||||||
			PluginManager plugins = new(
 | 
									new(host.HostingEnvironment, host.Configuration, logger);
 | 
				
			||||||
				hostProvider,
 | 
								PluginManager plugins = new(hostProvider, logger.CreateLogger<PluginManager>());
 | 
				
			||||||
				logger.CreateLogger<PluginManager>()
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			return new PluginsStartup(plugins);
 | 
								return new PluginsStartup(plugins);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -96,7 +93,9 @@ namespace Kyoo.Host
 | 
				
			|||||||
		/// <param name="services">The service collection to fill.</param>
 | 
							/// <param name="services">The service collection to fill.</param>
 | 
				
			||||||
		public void ConfigureServices(IServiceCollection services)
 | 
							public void ConfigureServices(IServiceCollection services)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			foreach (Assembly assembly in _plugins.GetAllPlugins().Select(x => x.GetType().Assembly))
 | 
								foreach (
 | 
				
			||||||
 | 
									Assembly assembly in _plugins.GetAllPlugins().Select(x => x.GetType().Assembly)
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
				services.AddMvcCore().AddApplicationPart(assembly);
 | 
									services.AddMvcCore().AddApplicationPart(assembly);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			_hostModule.Configure(services);
 | 
								_hostModule.Configure(services);
 | 
				
			||||||
@ -122,13 +121,14 @@ namespace Kyoo.Host
 | 
				
			|||||||
		/// <param name="container">An autofac container used to create a new scope to configure asp-net.</param>
 | 
							/// <param name="container">An autofac container used to create a new scope to configure asp-net.</param>
 | 
				
			||||||
		public void Configure(IApplicationBuilder app, ILifetimeScope container)
 | 
							public void Configure(IApplicationBuilder app, ILifetimeScope container)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			IEnumerable<IStartupAction> steps = _plugins.GetAllPlugins()
 | 
								IEnumerable<IStartupAction> steps = _plugins
 | 
				
			||||||
 | 
									.GetAllPlugins()
 | 
				
			||||||
				.Append(_hostModule)
 | 
									.Append(_hostModule)
 | 
				
			||||||
				.SelectMany(x => x.ConfigureSteps)
 | 
									.SelectMany(x => x.ConfigureSteps)
 | 
				
			||||||
				.OrderByDescending(x => x.Priority);
 | 
									.OrderByDescending(x => x.Priority);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			using ILifetimeScope scope = container.BeginLifetimeScope(x =>
 | 
								using ILifetimeScope scope = container.BeginLifetimeScope(
 | 
				
			||||||
				x.RegisterInstance(app).SingleInstance().ExternallyOwned()
 | 
									x => x.RegisterInstance(app).SingleInstance().ExternallyOwned()
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			IServiceProvider provider = scope.Resolve<IServiceProvider>();
 | 
								IServiceProvider provider = scope.Resolve<IServiceProvider>();
 | 
				
			||||||
			foreach (IStartupAction step in steps)
 | 
								foreach (IStartupAction step in steps)
 | 
				
			||||||
@ -164,9 +164,11 @@ namespace Kyoo.Host
 | 
				
			|||||||
			/// </param>
 | 
								/// </param>
 | 
				
			||||||
			/// <param name="configuration">The configuration context</param>
 | 
								/// <param name="configuration">The configuration context</param>
 | 
				
			||||||
			/// <param name="loggerFactory">A logger factory used to create a logger for the plugin manager.</param>
 | 
								/// <param name="loggerFactory">A logger factory used to create a logger for the plugin manager.</param>
 | 
				
			||||||
			public HostServiceProvider(IWebHostEnvironment hostEnvironment,
 | 
								public HostServiceProvider(
 | 
				
			||||||
 | 
									IWebHostEnvironment hostEnvironment,
 | 
				
			||||||
				IConfiguration configuration,
 | 
									IConfiguration configuration,
 | 
				
			||||||
				ILoggerFactory loggerFactory)
 | 
									ILoggerFactory loggerFactory
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				_hostEnvironment = hostEnvironment;
 | 
									_hostEnvironment = hostEnvironment;
 | 
				
			||||||
				_configuration = configuration;
 | 
									_configuration = configuration;
 | 
				
			||||||
@ -176,7 +178,10 @@ namespace Kyoo.Host
 | 
				
			|||||||
			/// <inheritdoc />
 | 
								/// <inheritdoc />
 | 
				
			||||||
			public object GetService(Type serviceType)
 | 
								public object GetService(Type serviceType)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (serviceType == typeof(IWebHostEnvironment) || serviceType == typeof(IHostEnvironment))
 | 
									if (
 | 
				
			||||||
 | 
										serviceType == typeof(IWebHostEnvironment)
 | 
				
			||||||
 | 
										|| serviceType == typeof(IHostEnvironment)
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
					return _hostEnvironment;
 | 
										return _hostEnvironment;
 | 
				
			||||||
				if (serviceType == typeof(IConfiguration))
 | 
									if (serviceType == typeof(IConfiguration))
 | 
				
			||||||
					return _configuration;
 | 
										return _configuration;
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,8 @@ namespace Kyoo.Meiliseach
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		private readonly IConfiguration _configuration;
 | 
							private readonly IConfiguration _configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public static Dictionary<string, Settings> IndexSettings => new()
 | 
							public static Dictionary<string, Settings> IndexSettings =>
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					"items",
 | 
										"items",
 | 
				
			||||||
@ -104,10 +105,7 @@ namespace Kyoo.Meiliseach
 | 
				
			|||||||
							CamelCase.ConvertName(nameof(Episode.EpisodeNumber)),
 | 
												CamelCase.ConvertName(nameof(Episode.EpisodeNumber)),
 | 
				
			||||||
							CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)),
 | 
												CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)),
 | 
				
			||||||
						},
 | 
											},
 | 
				
			||||||
					DisplayedAttributes = new[]
 | 
											DisplayedAttributes = new[] { CamelCase.ConvertName(nameof(Episode.Id)), },
 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						CamelCase.ConvertName(nameof(Episode.Id)),
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
						// TODO: Add stopwords
 | 
											// TODO: Add stopwords
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
@ -122,10 +120,7 @@ namespace Kyoo.Meiliseach
 | 
				
			|||||||
						},
 | 
											},
 | 
				
			||||||
						FilterableAttributes = Array.Empty<string>(),
 | 
											FilterableAttributes = Array.Empty<string>(),
 | 
				
			||||||
						SortableAttributes = Array.Empty<string>(),
 | 
											SortableAttributes = Array.Empty<string>(),
 | 
				
			||||||
					DisplayedAttributes = new[]
 | 
											DisplayedAttributes = new[] { CamelCase.ConvertName(nameof(Studio.Id)), },
 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						CamelCase.ConvertName(nameof(Studio.Id)),
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
						// TODO: Add stopwords
 | 
											// TODO: Add stopwords
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
@ -173,7 +168,10 @@ namespace Kyoo.Meiliseach
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind)
 | 
							private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			TaskInfo task = await client.CreateIndexAsync(index, hasKind ? "ref" : CamelCase.ConvertName(nameof(IResource.Id)));
 | 
								TaskInfo task = await client.CreateIndexAsync(
 | 
				
			||||||
 | 
									index,
 | 
				
			||||||
 | 
									hasKind ? "ref" : CamelCase.ConvertName(nameof(IResource.Id))
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			await client.WaitForTaskAsync(task.TaskUid);
 | 
								await client.WaitForTaskAsync(task.TaskUid);
 | 
				
			||||||
			await client.Index(index).UpdateSettingsAsync(IndexSettings[index]);
 | 
								await client.Index(index).UpdateSettingsAsync(IndexSettings[index]);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -181,12 +179,15 @@ namespace Kyoo.Meiliseach
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void Configure(ContainerBuilder builder)
 | 
							public void Configure(ContainerBuilder builder)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			builder.RegisterInstance(new MeilisearchClient(
 | 
								builder
 | 
				
			||||||
 | 
									.RegisterInstance(
 | 
				
			||||||
 | 
										new MeilisearchClient(
 | 
				
			||||||
						_configuration.GetValue("MEILI_HOST", "http://meilisearch:7700"),
 | 
											_configuration.GetValue("MEILI_HOST", "http://meilisearch:7700"),
 | 
				
			||||||
						_configuration.GetValue<string?>("MEILI_MASTER_KEY")
 | 
											_configuration.GetValue<string?>("MEILI_MASTER_KEY")
 | 
				
			||||||
			)).SingleInstance();
 | 
										)
 | 
				
			||||||
			builder.RegisterType<MeiliSync>().AsSelf().SingleInstance()
 | 
									)
 | 
				
			||||||
				.AutoActivate();
 | 
									.SingleInstance();
 | 
				
			||||||
 | 
								builder.RegisterType<MeiliSync>().AsSelf().SingleInstance().AutoActivate();
 | 
				
			||||||
			builder.RegisterType<SearchManager>().As<ISearchManager>().InstancePerLifetimeScope();
 | 
								builder.RegisterType<SearchManager>().As<ISearchManager>().InstancePerLifetimeScope();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -36,11 +36,21 @@ public class SearchManager : ISearchManager
 | 
				
			|||||||
		return sort switch
 | 
							return sort switch
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Sort<T>.Default => Array.Empty<string>(),
 | 
								Sort<T>.Default => Array.Empty<string>(),
 | 
				
			||||||
			Sort<T>.By @sortBy => MeilisearchModule.IndexSettings[index].SortableAttributes.Contains(sortBy.Key, StringComparer.InvariantCultureIgnoreCase)
 | 
								Sort<T>.By @sortBy
 | 
				
			||||||
				? new[] { $"{CamelCase.ConvertName(sortBy.Key)}:{(sortBy.Desendant ? "desc" : "asc")}" }
 | 
									=> MeilisearchModule
 | 
				
			||||||
 | 
										.IndexSettings[index]
 | 
				
			||||||
 | 
										.SortableAttributes
 | 
				
			||||||
 | 
										.Contains(sortBy.Key, StringComparer.InvariantCultureIgnoreCase)
 | 
				
			||||||
 | 
										? new[]
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											$"{CamelCase.ConvertName(sortBy.Key)}:{(sortBy.Desendant ? "desc" : "asc")}"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
					: throw new ValidationException($"Invalid sorting mode: {sortBy.Key}"),
 | 
										: throw new ValidationException($"Invalid sorting mode: {sortBy.Key}"),
 | 
				
			||||||
			Sort<T>.Conglomerate(var list) => list.SelectMany(x => _GetSortsBy(index, x)),
 | 
								Sort<T>.Conglomerate(var list) => list.SelectMany(x => _GetSortsBy(index, x)),
 | 
				
			||||||
			Sort<T>.Random => throw new ValidationException("Random sorting is not supported while searching."),
 | 
								Sort<T>.Random
 | 
				
			||||||
 | 
									=> throw new ValidationException(
 | 
				
			||||||
 | 
										"Random sorting is not supported while searching."
 | 
				
			||||||
 | 
									),
 | 
				
			||||||
			_ => Array.Empty<string>(),
 | 
								_ => Array.Empty<string>(),
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -51,79 +61,100 @@ public class SearchManager : ISearchManager
 | 
				
			|||||||
		_libraryManager = libraryManager;
 | 
							_libraryManager = libraryManager;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async Task<SearchPage<T>.SearchResult> _Search<T>(string index, string? query,
 | 
						private async Task<SearchPage<T>.SearchResult> _Search<T>(
 | 
				
			||||||
 | 
							string index,
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		string? where = null,
 | 
							string? where = null,
 | 
				
			||||||
		Sort<T>? sortBy = default,
 | 
							Sort<T>? sortBy = default,
 | 
				
			||||||
		SearchPagination? pagination = default,
 | 
							SearchPagination? pagination = default,
 | 
				
			||||||
		Include<T>? include = default)
 | 
							Include<T>? include = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
		where T : class, IResource, IQuery
 | 
							where T : class, IResource, IQuery
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// TODO: add filters and facets
 | 
							// TODO: add filters and facets
 | 
				
			||||||
		ISearchable<IdResource> res = await _client.Index(index).SearchAsync<IdResource>(query, new SearchQuery()
 | 
							ISearchable<IdResource> res = await _client
 | 
				
			||||||
 | 
								.Index(index)
 | 
				
			||||||
 | 
								.SearchAsync<IdResource>(
 | 
				
			||||||
 | 
									query,
 | 
				
			||||||
 | 
									new SearchQuery()
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					Filter = where,
 | 
										Filter = where,
 | 
				
			||||||
					Sort = _GetSortsBy(index, sortBy),
 | 
										Sort = _GetSortsBy(index, sortBy),
 | 
				
			||||||
					Limit = pagination?.Limit ?? 50,
 | 
										Limit = pagination?.Limit ?? 50,
 | 
				
			||||||
					Offset = pagination?.Skip ?? 0,
 | 
										Offset = pagination?.Skip ?? 0,
 | 
				
			||||||
		});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		return new SearchPage<T>.SearchResult
 | 
							return new SearchPage<T>.SearchResult
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Query = query,
 | 
								Query = query,
 | 
				
			||||||
			Items = await _libraryManager.Repository<T>()
 | 
								Items = await _libraryManager
 | 
				
			||||||
 | 
									.Repository<T>()
 | 
				
			||||||
				.FromIds(res.Hits.Select(x => x.Id).ToList(), include),
 | 
									.FromIds(res.Hits.Select(x => x.Id).ToList(), include),
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc/>
 | 
						/// <inheritdoc/>
 | 
				
			||||||
	public Task<SearchPage<ILibraryItem>.SearchResult> SearchItems(string? query,
 | 
						public Task<SearchPage<ILibraryItem>.SearchResult> SearchItems(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<ILibraryItem> sortBy,
 | 
							Sort<ILibraryItem> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<ILibraryItem>? include = default)
 | 
							Include<ILibraryItem>? include = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _Search("items", query, null, sortBy, pagination, include);
 | 
							return _Search("items", query, null, sortBy, pagination, include);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc/>
 | 
						/// <inheritdoc/>
 | 
				
			||||||
	public Task<SearchPage<Movie>.SearchResult> SearchMovies(string? query,
 | 
						public Task<SearchPage<Movie>.SearchResult> SearchMovies(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Movie> sortBy,
 | 
							Sort<Movie> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Movie>? include = default)
 | 
							Include<Movie>? include = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _Search("items", query, $"kind = {nameof(Movie)}", sortBy, pagination, include);
 | 
							return _Search("items", query, $"kind = {nameof(Movie)}", sortBy, pagination, include);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc/>
 | 
						/// <inheritdoc/>
 | 
				
			||||||
	public Task<SearchPage<Show>.SearchResult> SearchShows(string? query,
 | 
						public Task<SearchPage<Show>.SearchResult> SearchShows(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Show> sortBy,
 | 
							Sort<Show> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Show>? include = default)
 | 
							Include<Show>? include = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _Search("items", query, $"kind = {nameof(Show)}", sortBy, pagination, include);
 | 
							return _Search("items", query, $"kind = {nameof(Show)}", sortBy, pagination, include);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc/>
 | 
						/// <inheritdoc/>
 | 
				
			||||||
	public Task<SearchPage<Collection>.SearchResult> SearchCollections(string? query,
 | 
						public Task<SearchPage<Collection>.SearchResult> SearchCollections(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Collection> sortBy,
 | 
							Sort<Collection> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Collection>? include = default)
 | 
							Include<Collection>? include = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _Search("items", query, $"kind = {nameof(Collection)}", sortBy, pagination, include);
 | 
							return _Search("items", query, $"kind = {nameof(Collection)}", sortBy, pagination, include);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc/>
 | 
						/// <inheritdoc/>
 | 
				
			||||||
	public Task<SearchPage<Episode>.SearchResult> SearchEpisodes(string? query,
 | 
						public Task<SearchPage<Episode>.SearchResult> SearchEpisodes(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Episode> sortBy,
 | 
							Sort<Episode> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Episode>? include = default)
 | 
							Include<Episode>? include = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _Search(nameof(Episode), query, null, sortBy, pagination, include);
 | 
							return _Search(nameof(Episode), query, null, sortBy, pagination, include);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/// <inheritdoc/>
 | 
						/// <inheritdoc/>
 | 
				
			||||||
	public Task<SearchPage<Studio>.SearchResult> SearchStudios(string? query,
 | 
						public Task<SearchPage<Studio>.SearchResult> SearchStudios(
 | 
				
			||||||
 | 
							string? query,
 | 
				
			||||||
		Sort<Studio> sortBy,
 | 
							Sort<Studio> sortBy,
 | 
				
			||||||
		SearchPagination pagination,
 | 
							SearchPagination pagination,
 | 
				
			||||||
		Include<Studio>? include = default)
 | 
							Include<Studio>? include = default
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return _Search(nameof(Studio), query, null, sortBy, pagination, include);
 | 
							return _Search(nameof(Studio), query, null, sortBy, pagination, include);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -117,11 +117,13 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
			where T2 : class, IResource
 | 
								where T2 : class, IResource
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Set<Dictionary<string, object>>(LinkName<T1, T2>())
 | 
								Set<Dictionary<string, object>>(LinkName<T1, T2>())
 | 
				
			||||||
				.Add(new Dictionary<string, object>
 | 
									.Add(
 | 
				
			||||||
 | 
										new Dictionary<string, object>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						[LinkNameFk<T1>()] = first,
 | 
											[LinkNameFk<T1>()] = first,
 | 
				
			||||||
						[LinkNameFk<T2>()] = second
 | 
											[LinkNameFk<T2>()] = second
 | 
				
			||||||
				});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protected DatabaseContext(IHttpContextAccessor accessor)
 | 
							protected DatabaseContext(IHttpContextAccessor accessor)
 | 
				
			||||||
@ -177,11 +179,16 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
			// 	{
 | 
								// 	{
 | 
				
			||||||
			// 		x.ToJson();
 | 
								// 		x.ToJson();
 | 
				
			||||||
			// 	});
 | 
								// 	});
 | 
				
			||||||
			modelBuilder.Entity<T>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<T>()
 | 
				
			||||||
				.Property(x => x.ExternalId)
 | 
									.Property(x => x.ExternalId)
 | 
				
			||||||
				.HasConversion(
 | 
									.HasConversion(
 | 
				
			||||||
					v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
 | 
										v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
 | 
				
			||||||
					v => JsonSerializer.Deserialize<Dictionary<string, MetadataId>>(v, (JsonSerializerOptions?)null)!
 | 
										v =>
 | 
				
			||||||
 | 
											JsonSerializer.Deserialize<Dictionary<string, MetadataId>>(
 | 
				
			||||||
 | 
												v,
 | 
				
			||||||
 | 
												(JsonSerializerOptions?)null
 | 
				
			||||||
 | 
											)!
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
				.HasColumnType("json");
 | 
									.HasColumnType("json");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -189,18 +196,16 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		private static void _HasImages<T>(ModelBuilder modelBuilder)
 | 
							private static void _HasImages<T>(ModelBuilder modelBuilder)
 | 
				
			||||||
			where T : class, IThumbnails
 | 
								where T : class, IThumbnails
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			modelBuilder.Entity<T>()
 | 
								modelBuilder.Entity<T>().OwnsOne(x => x.Poster);
 | 
				
			||||||
				.OwnsOne(x => x.Poster);
 | 
								modelBuilder.Entity<T>().OwnsOne(x => x.Thumbnail);
 | 
				
			||||||
			modelBuilder.Entity<T>()
 | 
								modelBuilder.Entity<T>().OwnsOne(x => x.Logo);
 | 
				
			||||||
				.OwnsOne(x => x.Thumbnail);
 | 
					 | 
				
			||||||
			modelBuilder.Entity<T>()
 | 
					 | 
				
			||||||
				.OwnsOne(x => x.Logo);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private static void _HasAddedDate<T>(ModelBuilder modelBuilder)
 | 
							private static void _HasAddedDate<T>(ModelBuilder modelBuilder)
 | 
				
			||||||
			where T : class, IAddedDate
 | 
								where T : class, IAddedDate
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			modelBuilder.Entity<T>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<T>()
 | 
				
			||||||
				.Property(x => x.AddedDate)
 | 
									.Property(x => x.AddedDate)
 | 
				
			||||||
				.HasDefaultValueSql("now() at time zone 'utc'")
 | 
									.HasDefaultValueSql("now() at time zone 'utc'")
 | 
				
			||||||
				.ValueGeneratedOnAdd();
 | 
									.ValueGeneratedOnAdd();
 | 
				
			||||||
@ -215,24 +220,27 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// <param name="secondNavigation">The second navigation expression from T2 to T</param>
 | 
							/// <param name="secondNavigation">The second navigation expression from T2 to T</param>
 | 
				
			||||||
		/// <typeparam name="T">The owning type of the relationship</typeparam>
 | 
							/// <typeparam name="T">The owning type of the relationship</typeparam>
 | 
				
			||||||
		/// <typeparam name="T2">The owned type of the relationship</typeparam>
 | 
							/// <typeparam name="T2">The owned type of the relationship</typeparam>
 | 
				
			||||||
		private void _HasManyToMany<T, T2>(ModelBuilder modelBuilder,
 | 
							private void _HasManyToMany<T, T2>(
 | 
				
			||||||
 | 
								ModelBuilder modelBuilder,
 | 
				
			||||||
			Expression<Func<T, IEnumerable<T2>?>> firstNavigation,
 | 
								Expression<Func<T, IEnumerable<T2>?>> firstNavigation,
 | 
				
			||||||
			Expression<Func<T2, IEnumerable<T>?>> secondNavigation)
 | 
								Expression<Func<T2, IEnumerable<T>?>> secondNavigation
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
			where T : class, IResource
 | 
								where T : class, IResource
 | 
				
			||||||
			where T2 : class, IResource
 | 
								where T2 : class, IResource
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			modelBuilder.Entity<T2>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<T2>()
 | 
				
			||||||
				.HasMany(secondNavigation)
 | 
									.HasMany(secondNavigation)
 | 
				
			||||||
				.WithMany(firstNavigation)
 | 
									.WithMany(firstNavigation)
 | 
				
			||||||
				.UsingEntity<Dictionary<string, object>>(
 | 
									.UsingEntity<Dictionary<string, object>>(
 | 
				
			||||||
					LinkName<T, T2>(),
 | 
										LinkName<T, T2>(),
 | 
				
			||||||
					x => x
 | 
										x =>
 | 
				
			||||||
						.HasOne<T>()
 | 
											x.HasOne<T>()
 | 
				
			||||||
							.WithMany()
 | 
												.WithMany()
 | 
				
			||||||
							.HasForeignKey(LinkNameFk<T>())
 | 
												.HasForeignKey(LinkNameFk<T>())
 | 
				
			||||||
							.OnDelete(DeleteBehavior.Cascade),
 | 
												.OnDelete(DeleteBehavior.Cascade),
 | 
				
			||||||
					x => x
 | 
										x =>
 | 
				
			||||||
						.HasOne<T2>()
 | 
											x.HasOne<T2>()
 | 
				
			||||||
							.WithMany()
 | 
												.WithMany()
 | 
				
			||||||
							.HasForeignKey(LinkNameFk<T2>())
 | 
												.HasForeignKey(LinkNameFk<T2>())
 | 
				
			||||||
							.OnDelete(DeleteBehavior.Cascade)
 | 
												.OnDelete(DeleteBehavior.Cascade)
 | 
				
			||||||
@ -247,33 +255,37 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			base.OnModelCreating(modelBuilder);
 | 
								base.OnModelCreating(modelBuilder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<Show>()
 | 
								modelBuilder.Entity<Show>().Ignore(x => x.FirstEpisode).Ignore(x => x.AirDate);
 | 
				
			||||||
				.Ignore(x => x.FirstEpisode)
 | 
								modelBuilder
 | 
				
			||||||
				.Ignore(x => x.AirDate);
 | 
									.Entity<Episode>()
 | 
				
			||||||
			modelBuilder.Entity<Episode>()
 | 
					 | 
				
			||||||
				.Ignore(x => x.PreviousEpisode)
 | 
									.Ignore(x => x.PreviousEpisode)
 | 
				
			||||||
				.Ignore(x => x.NextEpisode);
 | 
									.Ignore(x => x.NextEpisode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// modelBuilder.Entity<PeopleRole>()
 | 
								// modelBuilder.Entity<PeopleRole>()
 | 
				
			||||||
			// 	.Ignore(x => x.ForPeople);
 | 
								// 	.Ignore(x => x.ForPeople);
 | 
				
			||||||
			modelBuilder.Entity<Show>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<Show>()
 | 
				
			||||||
				.HasMany(x => x.Seasons)
 | 
									.HasMany(x => x.Seasons)
 | 
				
			||||||
				.WithOne(x => x.Show)
 | 
									.WithOne(x => x.Show)
 | 
				
			||||||
				.OnDelete(DeleteBehavior.Cascade);
 | 
									.OnDelete(DeleteBehavior.Cascade);
 | 
				
			||||||
			modelBuilder.Entity<Show>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<Show>()
 | 
				
			||||||
				.HasMany(x => x.Episodes)
 | 
									.HasMany(x => x.Episodes)
 | 
				
			||||||
				.WithOne(x => x.Show)
 | 
									.WithOne(x => x.Show)
 | 
				
			||||||
				.OnDelete(DeleteBehavior.Cascade);
 | 
									.OnDelete(DeleteBehavior.Cascade);
 | 
				
			||||||
			modelBuilder.Entity<Season>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<Season>()
 | 
				
			||||||
				.HasMany(x => x.Episodes)
 | 
									.HasMany(x => x.Episodes)
 | 
				
			||||||
				.WithOne(x => x.Season)
 | 
									.WithOne(x => x.Season)
 | 
				
			||||||
				.OnDelete(DeleteBehavior.Cascade);
 | 
									.OnDelete(DeleteBehavior.Cascade);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<Movie>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<Movie>()
 | 
				
			||||||
				.HasOne(x => x.Studio)
 | 
									.HasOne(x => x.Studio)
 | 
				
			||||||
				.WithMany(x => x.Movies)
 | 
									.WithMany(x => x.Movies)
 | 
				
			||||||
				.OnDelete(DeleteBehavior.SetNull);
 | 
									.OnDelete(DeleteBehavior.SetNull);
 | 
				
			||||||
			modelBuilder.Entity<Show>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<Show>()
 | 
				
			||||||
				.HasOne(x => x.Studio)
 | 
									.HasOne(x => x.Studio)
 | 
				
			||||||
				.WithMany(x => x.Shows)
 | 
									.WithMany(x => x.Shows)
 | 
				
			||||||
				.OnDelete(DeleteBehavior.SetNull);
 | 
									.OnDelete(DeleteBehavior.SetNull);
 | 
				
			||||||
@ -305,16 +317,21 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<User>().OwnsOne(x => x.Logo);
 | 
								modelBuilder.Entity<User>().OwnsOne(x => x.Logo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<MovieWatchStatus>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<MovieWatchStatus>()
 | 
				
			||||||
				.HasKey(x => new { User = x.UserId, Movie = x.MovieId });
 | 
									.HasKey(x => new { User = x.UserId, Movie = x.MovieId });
 | 
				
			||||||
			modelBuilder.Entity<ShowWatchStatus>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<ShowWatchStatus>()
 | 
				
			||||||
				.HasKey(x => new { User = x.UserId, Show = x.ShowId });
 | 
									.HasKey(x => new { User = x.UserId, Show = x.ShowId });
 | 
				
			||||||
			modelBuilder.Entity<EpisodeWatchStatus>()
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<EpisodeWatchStatus>()
 | 
				
			||||||
				.HasKey(x => new { User = x.UserId, Episode = x.EpisodeId });
 | 
									.HasKey(x => new { User = x.UserId, Episode = x.EpisodeId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<MovieWatchStatus>().HasQueryFilter(x => x.UserId == CurrentUserId);
 | 
								modelBuilder.Entity<MovieWatchStatus>().HasQueryFilter(x => x.UserId == CurrentUserId);
 | 
				
			||||||
			modelBuilder.Entity<ShowWatchStatus>().HasQueryFilter(x => x.UserId == CurrentUserId);
 | 
								modelBuilder.Entity<ShowWatchStatus>().HasQueryFilter(x => x.UserId == CurrentUserId);
 | 
				
			||||||
			modelBuilder.Entity<EpisodeWatchStatus>().HasQueryFilter(x => x.UserId == CurrentUserId);
 | 
								modelBuilder
 | 
				
			||||||
 | 
									.Entity<EpisodeWatchStatus>()
 | 
				
			||||||
 | 
									.HasQueryFilter(x => x.UserId == CurrentUserId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<ShowWatchStatus>().Navigation(x => x.NextEpisode).AutoInclude();
 | 
								modelBuilder.Entity<ShowWatchStatus>().Navigation(x => x.NextEpisode).AutoInclude();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -326,39 +343,35 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
			modelBuilder.Entity<Show>().Ignore(x => x.WatchStatus);
 | 
								modelBuilder.Entity<Show>().Ignore(x => x.WatchStatus);
 | 
				
			||||||
			modelBuilder.Entity<Episode>().Ignore(x => x.WatchStatus);
 | 
								modelBuilder.Entity<Episode>().Ignore(x => x.WatchStatus);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<Collection>()
 | 
								modelBuilder.Entity<Collection>().HasIndex(x => x.Slug).IsUnique();
 | 
				
			||||||
				.HasIndex(x => x.Slug)
 | 
					 | 
				
			||||||
				.IsUnique();
 | 
					 | 
				
			||||||
			// modelBuilder.Entity<People>()
 | 
								// modelBuilder.Entity<People>()
 | 
				
			||||||
			// 	.HasIndex(x => x.Slug)
 | 
								// 	.HasIndex(x => x.Slug)
 | 
				
			||||||
			// 	.IsUnique();
 | 
								// 	.IsUnique();
 | 
				
			||||||
			modelBuilder.Entity<Movie>()
 | 
								modelBuilder.Entity<Movie>().HasIndex(x => x.Slug).IsUnique();
 | 
				
			||||||
				.HasIndex(x => x.Slug)
 | 
								modelBuilder.Entity<Show>().HasIndex(x => x.Slug).IsUnique();
 | 
				
			||||||
				.IsUnique();
 | 
								modelBuilder.Entity<Studio>().HasIndex(x => x.Slug).IsUnique();
 | 
				
			||||||
			modelBuilder.Entity<Show>()
 | 
								modelBuilder
 | 
				
			||||||
				.HasIndex(x => x.Slug)
 | 
									.Entity<Season>()
 | 
				
			||||||
				.IsUnique();
 | 
					 | 
				
			||||||
			modelBuilder.Entity<Studio>()
 | 
					 | 
				
			||||||
				.HasIndex(x => x.Slug)
 | 
					 | 
				
			||||||
				.IsUnique();
 | 
					 | 
				
			||||||
			modelBuilder.Entity<Season>()
 | 
					 | 
				
			||||||
				.HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber })
 | 
									.HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber })
 | 
				
			||||||
				.IsUnique();
 | 
									.IsUnique();
 | 
				
			||||||
			modelBuilder.Entity<Season>()
 | 
								modelBuilder.Entity<Season>().HasIndex(x => x.Slug).IsUnique();
 | 
				
			||||||
				.HasIndex(x => x.Slug)
 | 
								modelBuilder
 | 
				
			||||||
				.IsUnique();
 | 
									.Entity<Episode>()
 | 
				
			||||||
			modelBuilder.Entity<Episode>()
 | 
									.HasIndex(
 | 
				
			||||||
				.HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber })
 | 
										x =>
 | 
				
			||||||
				.IsUnique();
 | 
											new
 | 
				
			||||||
			modelBuilder.Entity<Episode>()
 | 
											{
 | 
				
			||||||
				.HasIndex(x => x.Slug)
 | 
												ShowID = x.ShowId,
 | 
				
			||||||
				.IsUnique();
 | 
												x.SeasonNumber,
 | 
				
			||||||
			modelBuilder.Entity<User>()
 | 
												x.EpisodeNumber,
 | 
				
			||||||
				.HasIndex(x => x.Slug)
 | 
												x.AbsoluteNumber
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
				.IsUnique();
 | 
									.IsUnique();
 | 
				
			||||||
 | 
								modelBuilder.Entity<Episode>().HasIndex(x => x.Slug).IsUnique();
 | 
				
			||||||
 | 
								modelBuilder.Entity<User>().HasIndex(x => x.Slug).IsUnique();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.Entity<Movie>()
 | 
								modelBuilder.Entity<Movie>().Ignore(x => x.Links);
 | 
				
			||||||
				.Ignore(x => x.Links);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -428,8 +441,10 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
 | 
							/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
 | 
				
			||||||
		/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
 | 
							/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
 | 
				
			||||||
		/// <returns>The number of state entries written to the database.</returns>
 | 
							/// <returns>The number of state entries written to the database.</returns>
 | 
				
			||||||
		public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
 | 
							public override async Task<int> SaveChangesAsync(
 | 
				
			||||||
			CancellationToken cancellationToken = default)
 | 
								bool acceptAllChangesOnSuccess,
 | 
				
			||||||
 | 
								CancellationToken cancellationToken = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			try
 | 
								try
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -450,7 +465,9 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
 | 
							/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
 | 
				
			||||||
		/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
 | 
							/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
 | 
				
			||||||
		/// <returns>The number of state entries written to the database.</returns>
 | 
							/// <returns>The number of state entries written to the database.</returns>
 | 
				
			||||||
		public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
 | 
							public override async Task<int> SaveChangesAsync(
 | 
				
			||||||
 | 
								CancellationToken cancellationToken = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			try
 | 
								try
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -475,7 +492,8 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// <returns>The number of state entries written to the database.</returns>
 | 
							/// <returns>The number of state entries written to the database.</returns>
 | 
				
			||||||
		public async Task<int> SaveChangesAsync<T>(
 | 
							public async Task<int> SaveChangesAsync<T>(
 | 
				
			||||||
			Func<Task<T>> getExisting,
 | 
								Func<Task<T>> getExisting,
 | 
				
			||||||
			CancellationToken cancellationToken = default)
 | 
								CancellationToken cancellationToken = default
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			try
 | 
								try
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -523,9 +541,7 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		public T? LocalEntity<T>(string slug)
 | 
							public T? LocalEntity<T>(string slug)
 | 
				
			||||||
			where T : class, IResource
 | 
								where T : class, IResource
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return ChangeTracker.Entries<T>()
 | 
								return ChangeTracker.Entries<T>().FirstOrDefault(x => x.Entity.Slug == slug)?.Entity;
 | 
				
			||||||
				.FirstOrDefault(x => x.Entity.Slug == slug)
 | 
					 | 
				
			||||||
				?.Entity;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
@ -540,7 +556,11 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		public void DiscardChanges()
 | 
							public void DiscardChanges()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Detached))
 | 
								foreach (
 | 
				
			||||||
 | 
									EntityEntry entry in ChangeTracker
 | 
				
			||||||
 | 
										.Entries()
 | 
				
			||||||
 | 
										.Where(x => x.State != EntityState.Detached)
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				entry.State = EntityState.Detached;
 | 
									entry.State = EntityState.Detached;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
				
			|||||||
@ -31,71 +31,122 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		protected override void Up(MigrationBuilder migrationBuilder)
 | 
							protected override void Up(MigrationBuilder migrationBuilder)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			migrationBuilder.AlterDatabase()
 | 
								migrationBuilder
 | 
				
			||||||
				.Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western")
 | 
									.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:status", "unknown,finished,airing,planned");
 | 
									.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "collections",
 | 
									name: "collections",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
											slug = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(256)",
 | 
				
			||||||
 | 
												maxLength: 256,
 | 
				
			||||||
 | 
												nullable: false
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						name = table.Column<string>(type: "text", nullable: false),
 | 
											name = table.Column<string>(type: "text", nullable: false),
 | 
				
			||||||
						overview = table.Column<string>(type: "text", nullable: true),
 | 
											overview = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						poster_source = 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),
 | 
											poster_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
											thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, 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_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
											logo_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						external_id = table.Column<string>(type: "json", nullable: false)
 | 
											external_id = table.Column<string>(type: "json", nullable: false)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					table.PrimaryKey("pk_collections", x => x.id);
 | 
										table.PrimaryKey("pk_collections", x => x.id);
 | 
				
			||||||
				});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "studios",
 | 
									name: "studios",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
											slug = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(256)",
 | 
				
			||||||
 | 
												maxLength: 256,
 | 
				
			||||||
 | 
												nullable: false
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						name = table.Column<string>(type: "text", nullable: false),
 | 
											name = table.Column<string>(type: "text", nullable: false),
 | 
				
			||||||
						external_id = table.Column<string>(type: "json", nullable: false)
 | 
											external_id = table.Column<string>(type: "json", nullable: false)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					table.PrimaryKey("pk_studios", x => x.id);
 | 
										table.PrimaryKey("pk_studios", x => x.id);
 | 
				
			||||||
				});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "users",
 | 
									name: "users",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
											slug = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(256)",
 | 
				
			||||||
 | 
												maxLength: 256,
 | 
				
			||||||
 | 
												nullable: false
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						username = table.Column<string>(type: "text", nullable: false),
 | 
											username = table.Column<string>(type: "text", nullable: false),
 | 
				
			||||||
						email = table.Column<string>(type: "text", nullable: false),
 | 
											email = table.Column<string>(type: "text", nullable: false),
 | 
				
			||||||
						password = table.Column<string>(type: "text", nullable: false),
 | 
											password = table.Column<string>(type: "text", nullable: false),
 | 
				
			||||||
						permissions = table.Column<string[]>(type: "text[]", nullable: false),
 | 
											permissions = table.Column<string[]>(type: "text[]", nullable: false),
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						logo_source = table.Column<string>(type: "text", nullable: true),
 | 
											logo_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true)
 | 
											logo_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					table.PrimaryKey("pk_users", x => x.id);
 | 
										table.PrimaryKey("pk_users", x => x.id);
 | 
				
			||||||
				});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "movies",
 | 
									name: "movies",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
											slug = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(256)",
 | 
				
			||||||
 | 
												maxLength: 256,
 | 
				
			||||||
 | 
												nullable: false
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						name = table.Column<string>(type: "text", nullable: false),
 | 
											name = table.Column<string>(type: "text", nullable: false),
 | 
				
			||||||
						tagline = table.Column<string>(type: "text", nullable: true),
 | 
											tagline = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
						aliases = table.Column<string[]>(type: "text[]", nullable: false),
 | 
											aliases = table.Column<string[]>(type: "text[]", nullable: false),
 | 
				
			||||||
@ -106,14 +157,33 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						status = table.Column<Status>(type: "status", nullable: false),
 | 
											status = table.Column<Status>(type: "status", nullable: false),
 | 
				
			||||||
						rating = table.Column<int>(type: "integer", nullable: false),
 | 
											rating = table.Column<int>(type: "integer", nullable: false),
 | 
				
			||||||
						runtime = table.Column<int>(type: "integer", nullable: false),
 | 
											runtime = table.Column<int>(type: "integer", nullable: false),
 | 
				
			||||||
					air_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
											air_date = table.Column<DateTime>(
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						poster_source = 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),
 | 
											poster_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
											thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, 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_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
											logo_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						trailer = table.Column<string>(type: "text", nullable: true),
 | 
											trailer = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
						external_id = table.Column<string>(type: "json", nullable: false),
 | 
											external_id = table.Column<string>(type: "json", nullable: false),
 | 
				
			||||||
						studio_id = table.Column<Guid>(type: "uuid", nullable: true)
 | 
											studio_id = table.Column<Guid>(type: "uuid", nullable: true)
 | 
				
			||||||
@ -126,15 +196,22 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						column: x => x.studio_id,
 | 
											column: x => x.studio_id,
 | 
				
			||||||
						principalTable: "studios",
 | 
											principalTable: "studios",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.SetNull);
 | 
											onDelete: ReferentialAction.SetNull
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "shows",
 | 
									name: "shows",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
											slug = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(256)",
 | 
				
			||||||
 | 
												maxLength: 256,
 | 
				
			||||||
 | 
												nullable: false
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						name = table.Column<string>(type: "text", nullable: false),
 | 
											name = table.Column<string>(type: "text", nullable: false),
 | 
				
			||||||
						tagline = table.Column<string>(type: "text", nullable: true),
 | 
											tagline = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
						aliases = table.Column<List<string>>(type: "text[]", nullable: false),
 | 
											aliases = table.Column<List<string>>(type: "text[]", nullable: false),
 | 
				
			||||||
@ -143,15 +220,37 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						genres = table.Column<List<Genre>>(type: "genre[]", nullable: false),
 | 
											genres = table.Column<List<Genre>>(type: "genre[]", nullable: false),
 | 
				
			||||||
						status = table.Column<Status>(type: "status", nullable: false),
 | 
											status = table.Column<Status>(type: "status", nullable: false),
 | 
				
			||||||
						rating = table.Column<int>(type: "integer", nullable: false),
 | 
											rating = table.Column<int>(type: "integer", nullable: false),
 | 
				
			||||||
					start_air = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
											start_air = table.Column<DateTime>(
 | 
				
			||||||
					end_air = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											end_air = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						poster_source = 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),
 | 
											poster_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
											thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, 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_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
											logo_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						trailer = table.Column<string>(type: "text", nullable: true),
 | 
											trailer = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
						external_id = table.Column<string>(type: "json", nullable: false),
 | 
											external_id = table.Column<string>(type: "json", nullable: false),
 | 
				
			||||||
						studio_id = table.Column<Guid>(type: "uuid", nullable: true)
 | 
											studio_id = table.Column<Guid>(type: "uuid", nullable: true)
 | 
				
			||||||
@ -164,76 +263,119 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						column: x => x.studio_id,
 | 
											column: x => x.studio_id,
 | 
				
			||||||
						principalTable: "studios",
 | 
											principalTable: "studios",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.SetNull);
 | 
											onDelete: ReferentialAction.SetNull
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "link_collection_movie",
 | 
									name: "link_collection_movie",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						collection_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											collection_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
						movie_id = table.Column<Guid>(type: "uuid", nullable: false)
 | 
											movie_id = table.Column<Guid>(type: "uuid", nullable: false)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					table.PrimaryKey("pk_link_collection_movie", x => new { x.collection_id, x.movie_id });
 | 
										table.PrimaryKey(
 | 
				
			||||||
 | 
											"pk_link_collection_movie",
 | 
				
			||||||
 | 
											x => new { x.collection_id, x.movie_id }
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_link_collection_movie_collections_collection_id",
 | 
											name: "fk_link_collection_movie_collections_collection_id",
 | 
				
			||||||
						column: x => x.collection_id,
 | 
											column: x => x.collection_id,
 | 
				
			||||||
						principalTable: "collections",
 | 
											principalTable: "collections",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_link_collection_movie_movies_movie_id",
 | 
											name: "fk_link_collection_movie_movies_movie_id",
 | 
				
			||||||
						column: x => x.movie_id,
 | 
											column: x => x.movie_id,
 | 
				
			||||||
						principalTable: "movies",
 | 
											principalTable: "movies",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "link_collection_show",
 | 
									name: "link_collection_show",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						collection_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											collection_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
						show_id = table.Column<Guid>(type: "uuid", nullable: false)
 | 
											show_id = table.Column<Guid>(type: "uuid", nullable: false)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					table.PrimaryKey("pk_link_collection_show", x => new { x.collection_id, x.show_id });
 | 
										table.PrimaryKey(
 | 
				
			||||||
 | 
											"pk_link_collection_show",
 | 
				
			||||||
 | 
											x => new { x.collection_id, x.show_id }
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_link_collection_show_collections_collection_id",
 | 
											name: "fk_link_collection_show_collections_collection_id",
 | 
				
			||||||
						column: x => x.collection_id,
 | 
											column: x => x.collection_id,
 | 
				
			||||||
						principalTable: "collections",
 | 
											principalTable: "collections",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_link_collection_show_shows_show_id",
 | 
											name: "fk_link_collection_show_shows_show_id",
 | 
				
			||||||
						column: x => x.show_id,
 | 
											column: x => x.show_id,
 | 
				
			||||||
						principalTable: "shows",
 | 
											principalTable: "shows",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "seasons",
 | 
									name: "seasons",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
											slug = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(256)",
 | 
				
			||||||
 | 
												maxLength: 256,
 | 
				
			||||||
 | 
												nullable: false
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						show_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											show_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
						season_number = table.Column<int>(type: "integer", nullable: false),
 | 
											season_number = table.Column<int>(type: "integer", nullable: false),
 | 
				
			||||||
						name = table.Column<string>(type: "text", nullable: true),
 | 
											name = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
						overview = 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),
 | 
											start_date = table.Column<DateTime>(
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
					end_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											end_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						poster_source = 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),
 | 
											poster_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
											thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, 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_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
											logo_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						external_id = table.Column<string>(type: "json", nullable: false)
 | 
											external_id = table.Column<string>(type: "json", nullable: false)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
@ -244,15 +386,22 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						column: x => x.show_id,
 | 
											column: x => x.show_id,
 | 
				
			||||||
						principalTable: "shows",
 | 
											principalTable: "shows",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "episodes",
 | 
									name: "episodes",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
 | 
											slug = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(256)",
 | 
				
			||||||
 | 
												maxLength: 256,
 | 
				
			||||||
 | 
												nullable: false
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						show_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											show_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
						season_id = table.Column<Guid>(type: "uuid", nullable: true),
 | 
											season_id = table.Column<Guid>(type: "uuid", nullable: true),
 | 
				
			||||||
						season_number = table.Column<int>(type: "integer", nullable: true),
 | 
											season_number = table.Column<int>(type: "integer", nullable: true),
 | 
				
			||||||
@ -262,14 +411,33 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						name = table.Column<string>(type: "text", nullable: true),
 | 
											name = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
						overview = table.Column<string>(type: "text", nullable: true),
 | 
											overview = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
						runtime = table.Column<int>(type: "integer", nullable: false),
 | 
											runtime = table.Column<int>(type: "integer", nullable: false),
 | 
				
			||||||
					release_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
											release_date = table.Column<DateTime>(
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						poster_source = 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),
 | 
											poster_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
											thumbnail_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, 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_source = table.Column<string>(type: "text", nullable: true),
 | 
				
			||||||
					logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
											logo_blurhash = table.Column<string>(
 | 
				
			||||||
 | 
												type: "character varying(32)",
 | 
				
			||||||
 | 
												maxLength: 32,
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						external_id = table.Column<string>(type: "json", nullable: false)
 | 
											external_id = table.Column<string>(type: "json", nullable: false)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
@ -280,124 +448,132 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						column: x => x.season_id,
 | 
											column: x => x.season_id,
 | 
				
			||||||
						principalTable: "seasons",
 | 
											principalTable: "seasons",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_episodes_shows_show_id",
 | 
											name: "fk_episodes_shows_show_id",
 | 
				
			||||||
						column: x => x.show_id,
 | 
											column: x => x.show_id,
 | 
				
			||||||
						principalTable: "shows",
 | 
											principalTable: "shows",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_collections_slug",
 | 
									name: "ix_collections_slug",
 | 
				
			||||||
				table: "collections",
 | 
									table: "collections",
 | 
				
			||||||
				column: "slug",
 | 
									column: "slug",
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_episodes_season_id",
 | 
									name: "ix_episodes_season_id",
 | 
				
			||||||
				table: "episodes",
 | 
									table: "episodes",
 | 
				
			||||||
				column: "season_id");
 | 
									column: "season_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_episodes_show_id_season_number_episode_number_absolute_numb",
 | 
									name: "ix_episodes_show_id_season_number_episode_number_absolute_numb",
 | 
				
			||||||
				table: "episodes",
 | 
									table: "episodes",
 | 
				
			||||||
				columns: new[] { "show_id", "season_number", "episode_number", "absolute_number" },
 | 
									columns: new[] { "show_id", "season_number", "episode_number", "absolute_number" },
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_episodes_slug",
 | 
									name: "ix_episodes_slug",
 | 
				
			||||||
				table: "episodes",
 | 
									table: "episodes",
 | 
				
			||||||
				column: "slug",
 | 
									column: "slug",
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_link_collection_movie_movie_id",
 | 
									name: "ix_link_collection_movie_movie_id",
 | 
				
			||||||
				table: "link_collection_movie",
 | 
									table: "link_collection_movie",
 | 
				
			||||||
				column: "movie_id");
 | 
									column: "movie_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_link_collection_show_show_id",
 | 
									name: "ix_link_collection_show_show_id",
 | 
				
			||||||
				table: "link_collection_show",
 | 
									table: "link_collection_show",
 | 
				
			||||||
				column: "show_id");
 | 
									column: "show_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_movies_slug",
 | 
									name: "ix_movies_slug",
 | 
				
			||||||
				table: "movies",
 | 
									table: "movies",
 | 
				
			||||||
				column: "slug",
 | 
									column: "slug",
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_movies_studio_id",
 | 
									name: "ix_movies_studio_id",
 | 
				
			||||||
				table: "movies",
 | 
									table: "movies",
 | 
				
			||||||
				column: "studio_id");
 | 
									column: "studio_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_seasons_show_id_season_number",
 | 
									name: "ix_seasons_show_id_season_number",
 | 
				
			||||||
				table: "seasons",
 | 
									table: "seasons",
 | 
				
			||||||
				columns: new[] { "show_id", "season_number" },
 | 
									columns: new[] { "show_id", "season_number" },
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_seasons_slug",
 | 
									name: "ix_seasons_slug",
 | 
				
			||||||
				table: "seasons",
 | 
									table: "seasons",
 | 
				
			||||||
				column: "slug",
 | 
									column: "slug",
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_shows_slug",
 | 
									name: "ix_shows_slug",
 | 
				
			||||||
				table: "shows",
 | 
									table: "shows",
 | 
				
			||||||
				column: "slug",
 | 
									column: "slug",
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_shows_studio_id",
 | 
									name: "ix_shows_studio_id",
 | 
				
			||||||
				table: "shows",
 | 
									table: "shows",
 | 
				
			||||||
				column: "studio_id");
 | 
									column: "studio_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_studios_slug",
 | 
									name: "ix_studios_slug",
 | 
				
			||||||
				table: "studios",
 | 
									table: "studios",
 | 
				
			||||||
				column: "slug",
 | 
									column: "slug",
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_users_slug",
 | 
									name: "ix_users_slug",
 | 
				
			||||||
				table: "users",
 | 
									table: "users",
 | 
				
			||||||
				column: "slug",
 | 
									column: "slug",
 | 
				
			||||||
				unique: true);
 | 
									unique: true
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		protected override void Down(MigrationBuilder migrationBuilder)
 | 
							protected override void Down(MigrationBuilder migrationBuilder)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "episodes");
 | 
				
			||||||
				name: "episodes");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "link_collection_movie");
 | 
				
			||||||
				name: "link_collection_movie");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "link_collection_show");
 | 
				
			||||||
				name: "link_collection_show");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "users");
 | 
				
			||||||
				name: "users");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "seasons");
 | 
				
			||||||
				name: "seasons");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "movies");
 | 
				
			||||||
				name: "movies");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "collections");
 | 
				
			||||||
				name: "collections");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "shows");
 | 
				
			||||||
				name: "shows");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "studios");
 | 
				
			||||||
				name: "studios");
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -30,50 +30,79 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		protected override void Up(MigrationBuilder migrationBuilder)
 | 
							protected override void Up(MigrationBuilder migrationBuilder)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			migrationBuilder.AlterDatabase()
 | 
								migrationBuilder
 | 
				
			||||||
				.Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western")
 | 
									.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:status", "unknown,finished,airing,planned")
 | 
									.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
 | 
				
			||||||
				.Annotation("Npgsql:Enum:watch_status", "completed,watching,droped,planned")
 | 
									.Annotation("Npgsql:Enum:watch_status", "completed,watching,droped,planned")
 | 
				
			||||||
				.OldAnnotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western")
 | 
									.OldAnnotation(
 | 
				
			||||||
 | 
										"Npgsql:Enum:genre",
 | 
				
			||||||
 | 
										"action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western"
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
				.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned");
 | 
									.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "episode_watch_status",
 | 
									name: "episode_watch_status",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						user_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											user_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
						episode_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											episode_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
					played_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											played_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
 | 
											status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
 | 
				
			||||||
						watched_time = table.Column<int>(type: "integer", nullable: true),
 | 
											watched_time = table.Column<int>(type: "integer", nullable: true),
 | 
				
			||||||
						watched_percent = table.Column<int>(type: "integer", nullable: true)
 | 
											watched_percent = table.Column<int>(type: "integer", nullable: true)
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				constraints: table =>
 | 
									constraints: table =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					table.PrimaryKey("pk_episode_watch_status", x => new { x.user_id, x.episode_id });
 | 
										table.PrimaryKey(
 | 
				
			||||||
 | 
											"pk_episode_watch_status",
 | 
				
			||||||
 | 
											x => new { x.user_id, x.episode_id }
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_episode_watch_status_episodes_episode_id",
 | 
											name: "fk_episode_watch_status_episodes_episode_id",
 | 
				
			||||||
						column: x => x.episode_id,
 | 
											column: x => x.episode_id,
 | 
				
			||||||
						principalTable: "episodes",
 | 
											principalTable: "episodes",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_episode_watch_status_users_user_id",
 | 
											name: "fk_episode_watch_status_users_user_id",
 | 
				
			||||||
						column: x => x.user_id,
 | 
											column: x => x.user_id,
 | 
				
			||||||
						principalTable: "users",
 | 
											principalTable: "users",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "movie_watch_status",
 | 
									name: "movie_watch_status",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						user_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											user_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
						movie_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											movie_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
					played_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											played_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
 | 
											status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
 | 
				
			||||||
						watched_time = table.Column<int>(type: "integer", nullable: true),
 | 
											watched_time = table.Column<int>(type: "integer", nullable: true),
 | 
				
			||||||
						watched_percent = table.Column<int>(type: "integer", nullable: true)
 | 
											watched_percent = table.Column<int>(type: "integer", nullable: true)
 | 
				
			||||||
@ -86,23 +115,34 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						column: x => x.movie_id,
 | 
											column: x => x.movie_id,
 | 
				
			||||||
						principalTable: "movies",
 | 
											principalTable: "movies",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_movie_watch_status_users_user_id",
 | 
											name: "fk_movie_watch_status_users_user_id",
 | 
				
			||||||
						column: x => x.user_id,
 | 
											column: x => x.user_id,
 | 
				
			||||||
						principalTable: "users",
 | 
											principalTable: "users",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateTable(
 | 
								migrationBuilder.CreateTable(
 | 
				
			||||||
				name: "show_watch_status",
 | 
									name: "show_watch_status",
 | 
				
			||||||
				columns: table => new
 | 
									columns: table =>
 | 
				
			||||||
 | 
										new
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						user_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											user_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
						show_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
											show_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
					added_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
 | 
											added_date = table.Column<DateTime>(
 | 
				
			||||||
					played_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: false,
 | 
				
			||||||
 | 
												defaultValueSql: "now() at time zone 'utc'"
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											played_date = table.Column<DateTime>(
 | 
				
			||||||
 | 
												type: "timestamp with time zone",
 | 
				
			||||||
 | 
												nullable: true
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
						status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
 | 
											status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
 | 
				
			||||||
						unseen_episodes_count = table.Column<int>(type: "integer", nullable: false),
 | 
											unseen_episodes_count = table.Column<int>(type: "integer", nullable: false),
 | 
				
			||||||
						next_episode_id = table.Column<Guid>(type: "uuid", nullable: true),
 | 
											next_episode_id = table.Column<Guid>(type: "uuid", nullable: true),
 | 
				
			||||||
@ -116,58 +156,70 @@ namespace Kyoo.Postgresql.Migrations
 | 
				
			|||||||
						name: "fk_show_watch_status_episodes_next_episode_id",
 | 
											name: "fk_show_watch_status_episodes_next_episode_id",
 | 
				
			||||||
						column: x => x.next_episode_id,
 | 
											column: x => x.next_episode_id,
 | 
				
			||||||
						principalTable: "episodes",
 | 
											principalTable: "episodes",
 | 
				
			||||||
						principalColumn: "id");
 | 
											principalColumn: "id"
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_show_watch_status_shows_show_id",
 | 
											name: "fk_show_watch_status_shows_show_id",
 | 
				
			||||||
						column: x => x.show_id,
 | 
											column: x => x.show_id,
 | 
				
			||||||
						principalTable: "shows",
 | 
											principalTable: "shows",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					table.ForeignKey(
 | 
										table.ForeignKey(
 | 
				
			||||||
						name: "fk_show_watch_status_users_user_id",
 | 
											name: "fk_show_watch_status_users_user_id",
 | 
				
			||||||
						column: x => x.user_id,
 | 
											column: x => x.user_id,
 | 
				
			||||||
						principalTable: "users",
 | 
											principalTable: "users",
 | 
				
			||||||
						principalColumn: "id",
 | 
											principalColumn: "id",
 | 
				
			||||||
						onDelete: ReferentialAction.Cascade);
 | 
											onDelete: ReferentialAction.Cascade
 | 
				
			||||||
				});
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_episode_watch_status_episode_id",
 | 
									name: "ix_episode_watch_status_episode_id",
 | 
				
			||||||
				table: "episode_watch_status",
 | 
									table: "episode_watch_status",
 | 
				
			||||||
				column: "episode_id");
 | 
									column: "episode_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_movie_watch_status_movie_id",
 | 
									name: "ix_movie_watch_status_movie_id",
 | 
				
			||||||
				table: "movie_watch_status",
 | 
									table: "movie_watch_status",
 | 
				
			||||||
				column: "movie_id");
 | 
									column: "movie_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_show_watch_status_next_episode_id",
 | 
									name: "ix_show_watch_status_next_episode_id",
 | 
				
			||||||
				table: "show_watch_status",
 | 
									table: "show_watch_status",
 | 
				
			||||||
				column: "next_episode_id");
 | 
									column: "next_episode_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.CreateIndex(
 | 
								migrationBuilder.CreateIndex(
 | 
				
			||||||
				name: "ix_show_watch_status_show_id",
 | 
									name: "ix_show_watch_status_show_id",
 | 
				
			||||||
				table: "show_watch_status",
 | 
									table: "show_watch_status",
 | 
				
			||||||
				column: "show_id");
 | 
									column: "show_id"
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		protected override void Down(MigrationBuilder migrationBuilder)
 | 
							protected override void Down(MigrationBuilder migrationBuilder)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "episode_watch_status");
 | 
				
			||||||
				name: "episode_watch_status");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "movie_watch_status");
 | 
				
			||||||
				name: "movie_watch_status");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.DropTable(
 | 
								migrationBuilder.DropTable(name: "show_watch_status");
 | 
				
			||||||
				name: "show_watch_status");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			migrationBuilder.AlterDatabase()
 | 
								migrationBuilder
 | 
				
			||||||
				.Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western")
 | 
									.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:status", "unknown,finished,airing,planned")
 | 
									.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
 | 
				
			||||||
				.OldAnnotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western")
 | 
									.OldAnnotation(
 | 
				
			||||||
 | 
										"Npgsql:Enum:genre",
 | 
				
			||||||
 | 
										"action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western"
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
				.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
 | 
									.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
 | 
				
			||||||
				.OldAnnotation("Npgsql:Enum:watch_status", "completed,watching,droped,planned");
 | 
									.OldAnnotation("Npgsql:Enum:watch_status", "completed,watching,droped,planned");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -56,8 +56,7 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// Design time constructor (dotnet ef migrations add). Do not use
 | 
							/// Design time constructor (dotnet ef migrations add). Do not use
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		public PostgresContext()
 | 
							public PostgresContext()
 | 
				
			||||||
			: base(null!)
 | 
								: base(null!) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public PostgresContext(DbContextOptions options, IHttpContextAccessor accessor)
 | 
							public PostgresContext(DbContextOptions options, IHttpContextAccessor accessor)
 | 
				
			||||||
			: base(options, accessor)
 | 
								: base(options, accessor)
 | 
				
			||||||
@ -98,8 +97,10 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
			modelBuilder.HasPostgresEnum<Genre>();
 | 
								modelBuilder.HasPostgresEnum<Genre>();
 | 
				
			||||||
			modelBuilder.HasPostgresEnum<WatchStatus>();
 | 
								modelBuilder.HasPostgresEnum<WatchStatus>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!)
 | 
								modelBuilder
 | 
				
			||||||
				.HasTranslation(args =>
 | 
									.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!)
 | 
				
			||||||
 | 
									.HasTranslation(
 | 
				
			||||||
 | 
										args =>
 | 
				
			||||||
						new SqlFunctionExpression(
 | 
											new SqlFunctionExpression(
 | 
				
			||||||
							"md5",
 | 
												"md5",
 | 
				
			||||||
							args,
 | 
												args,
 | 
				
			||||||
@ -130,7 +131,12 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		protected override bool IsDuplicateException(Exception ex)
 | 
							protected override bool IsDuplicateException(Exception ex)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return ex.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation or PostgresErrorCodes.ForeignKeyViolation };
 | 
								return ex.InnerException
 | 
				
			||||||
 | 
									is PostgresException
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										SqlState: PostgresErrorCodes.UniqueViolation
 | 
				
			||||||
 | 
											or PostgresErrorCodes.ForeignKeyViolation
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -79,14 +79,24 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			SqlMapper.TypeMapProvider = (type) =>
 | 
								SqlMapper.TypeMapProvider = (type) =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				return new CustomPropertyTypeMap(type, (type, name) =>
 | 
									return new CustomPropertyTypeMap(
 | 
				
			||||||
 | 
										type,
 | 
				
			||||||
 | 
										(type, name) =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
					string newName = Regex.Replace(name, "(^|_)([a-z])", (match) => match.Groups[2].Value.ToUpperInvariant());
 | 
											string newName = Regex.Replace(
 | 
				
			||||||
 | 
												name,
 | 
				
			||||||
 | 
												"(^|_)([a-z])",
 | 
				
			||||||
 | 
												(match) => match.Groups[2].Value.ToUpperInvariant()
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
						// TODO: Add images handling here (name: poster_source, newName: PosterSource) should set Poster.Source
 | 
											// TODO: Add images handling here (name: poster_source, newName: PosterSource) should set Poster.Source
 | 
				
			||||||
						return type.GetProperty(newName)!;
 | 
											return type.GetProperty(newName)!;
 | 
				
			||||||
				});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			SqlMapper.AddTypeHandler(typeof(Dictionary<string, MetadataId>), new JsonTypeHandler<Dictionary<string, MetadataId>>());
 | 
								SqlMapper.AddTypeHandler(
 | 
				
			||||||
 | 
									typeof(Dictionary<string, MetadataId>),
 | 
				
			||||||
 | 
									new JsonTypeHandler<Dictionary<string, MetadataId>>()
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			SqlMapper.AddTypeHandler(typeof(List<string>), new ListTypeHandler<string>());
 | 
								SqlMapper.AddTypeHandler(typeof(List<string>), new ListTypeHandler<string>());
 | 
				
			||||||
			SqlMapper.AddTypeHandler(typeof(List<Genre>), new ListTypeHandler<Genre>());
 | 
								SqlMapper.AddTypeHandler(typeof(List<Genre>), new ListTypeHandler<Genre>());
 | 
				
			||||||
			SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler());
 | 
								SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler());
 | 
				
			||||||
@ -97,7 +107,8 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void Configure(IServiceCollection services)
 | 
							public void Configure(IServiceCollection services)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			DbConnectionStringBuilder builder = new()
 | 
								DbConnectionStringBuilder builder =
 | 
				
			||||||
 | 
									new()
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"),
 | 
										["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"),
 | 
				
			||||||
					["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"),
 | 
										["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"),
 | 
				
			||||||
@ -109,14 +120,18 @@ namespace Kyoo.Postgresql
 | 
				
			|||||||
					["TIMEOUT"] = "30"
 | 
										["TIMEOUT"] = "30"
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			services.AddDbContext<DatabaseContext, PostgresContext>(x =>
 | 
								services.AddDbContext<DatabaseContext, PostgresContext>(
 | 
				
			||||||
 | 
									x =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
				x.UseNpgsql(builder.ConnectionString)
 | 
										x.UseNpgsql(builder.ConnectionString).UseProjectables();
 | 
				
			||||||
					.UseProjectables();
 | 
					 | 
				
			||||||
					if (_environment.IsDevelopment())
 | 
										if (_environment.IsDevelopment())
 | 
				
			||||||
						x.EnableDetailedErrors().EnableSensitiveDataLogging();
 | 
											x.EnableDetailedErrors().EnableSensitiveDataLogging();
 | 
				
			||||||
			}, ServiceLifetime.Transient);
 | 
									},
 | 
				
			||||||
			services.AddTransient<DbConnection>((_) => new NpgsqlConnection(builder.ConnectionString));
 | 
									ServiceLifetime.Transient
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								services.AddTransient<DbConnection>(
 | 
				
			||||||
 | 
									(_) => new NpgsqlConnection(builder.ConnectionString)
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			services.AddHealthChecks().AddDbContextCheck<DatabaseContext>();
 | 
								services.AddHealthChecks().AddDbContextCheck<DatabaseContext>();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,8 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
			options.PostProcess += postProcess =>
 | 
								options.PostProcess += postProcess =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				// We can't reorder items by assigning the sorted value to the Paths variable since it has no setter.
 | 
									// We can't reorder items by assigning the sorted value to the Paths variable since it has no setter.
 | 
				
			||||||
				List<KeyValuePair<string, OpenApiPathItem>> sorted = postProcess.Paths
 | 
									List<KeyValuePair<string, OpenApiPathItem>> sorted = postProcess
 | 
				
			||||||
 | 
										.Paths
 | 
				
			||||||
					.OrderBy(x => x.Key)
 | 
										.OrderBy(x => x.Key)
 | 
				
			||||||
					.ToList();
 | 
										.ToList();
 | 
				
			||||||
				postProcess.Paths.Clear();
 | 
									postProcess.Paths.Clear();
 | 
				
			||||||
@ -56,9 +57,7 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
					.Select(x =>
 | 
										.Select(x =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						x.Name = x.Name[(x.Name.IndexOf(':') + 1)..];
 | 
											x.Name = x.Name[(x.Name.IndexOf(':') + 1)..];
 | 
				
			||||||
						x.Tags = x.Tags
 | 
											x.Tags = x.Tags.OrderBy(y => y).ToList();
 | 
				
			||||||
							.OrderBy(y => y)
 | 
					 | 
				
			||||||
							.ToList();
 | 
					 | 
				
			||||||
						return x;
 | 
											return x;
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
					.ToList();
 | 
										.ToList();
 | 
				
			||||||
 | 
				
			|||||||
@ -21,10 +21,10 @@ using System.Linq;
 | 
				
			|||||||
using System.Reflection;
 | 
					using System.Reflection;
 | 
				
			||||||
using Kyoo.Abstractions.Models.Attributes;
 | 
					using Kyoo.Abstractions.Models.Attributes;
 | 
				
			||||||
using Kyoo.Swagger.Models;
 | 
					using Kyoo.Swagger.Models;
 | 
				
			||||||
using Namotion.Reflection;
 | 
					 | 
				
			||||||
using NSwag;
 | 
					using NSwag;
 | 
				
			||||||
using NSwag.Generation.AspNetCore;
 | 
					using NSwag.Generation.AspNetCore;
 | 
				
			||||||
using NSwag.Generation.Processors.Contexts;
 | 
					using NSwag.Generation.Processors.Contexts;
 | 
				
			||||||
 | 
					using Namotion.Reflection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Kyoo.Swagger
 | 
					namespace Kyoo.Swagger
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -42,21 +42,30 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
		/// <returns>This always return <c>true</c> since it should not remove operations.</returns>
 | 
							/// <returns>This always return <c>true</c> since it should not remove operations.</returns>
 | 
				
			||||||
		public static bool OperationFilter(OperationProcessorContext context)
 | 
							public static bool OperationFilter(OperationProcessorContext context)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute<ApiDefinitionAttribute>();
 | 
								ApiDefinitionAttribute def = context
 | 
				
			||||||
 | 
									.ControllerType
 | 
				
			||||||
 | 
									.GetCustomAttribute<ApiDefinitionAttribute>();
 | 
				
			||||||
			string name = def?.Name ?? context.ControllerType.Name;
 | 
								string name = def?.Name ?? context.ControllerType.Name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ApiDefinitionAttribute methodOverride = context.MethodInfo.GetCustomAttribute<ApiDefinitionAttribute>();
 | 
								ApiDefinitionAttribute methodOverride = context
 | 
				
			||||||
 | 
									.MethodInfo
 | 
				
			||||||
 | 
									.GetCustomAttribute<ApiDefinitionAttribute>();
 | 
				
			||||||
			if (methodOverride != null)
 | 
								if (methodOverride != null)
 | 
				
			||||||
				name = methodOverride.Name;
 | 
									name = methodOverride.Name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			context.OperationDescription.Operation.Tags.Add(name);
 | 
								context.OperationDescription.Operation.Tags.Add(name);
 | 
				
			||||||
			if (context.Document.Tags.All(x => x.Name != name))
 | 
								if (context.Document.Tags.All(x => x.Name != name))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				context.Document.Tags.Add(new OpenApiTag
 | 
									context
 | 
				
			||||||
 | 
										.Document
 | 
				
			||||||
 | 
										.Tags
 | 
				
			||||||
 | 
										.Add(
 | 
				
			||||||
 | 
											new OpenApiTag
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Name = name,
 | 
												Name = name,
 | 
				
			||||||
							Description = context.ControllerType.GetXmlDocsSummary()
 | 
												Description = context.ControllerType.GetXmlDocsSummary()
 | 
				
			||||||
				});
 | 
											}
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (def?.Group == null)
 | 
								if (def?.Group == null)
 | 
				
			||||||
@ -73,11 +82,13 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			else
 | 
								else
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				obj.Add(new TagGroups
 | 
									obj.Add(
 | 
				
			||||||
 | 
										new TagGroups
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						Name = def.Group,
 | 
											Name = def.Group,
 | 
				
			||||||
						Tags = new List<string> { def.Name }
 | 
											Tags = new List<string> { def.Name }
 | 
				
			||||||
				});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return true;
 | 
								return true;
 | 
				
			||||||
@ -94,19 +105,14 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
		public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess)
 | 
							public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			List<TagGroups> tagGroups = (List<TagGroups>)postProcess.ExtensionData["x-tagGroups"];
 | 
								List<TagGroups> tagGroups = (List<TagGroups>)postProcess.ExtensionData["x-tagGroups"];
 | 
				
			||||||
			List<string> tagsWithoutGroup = postProcess.Tags
 | 
								List<string> tagsWithoutGroup = postProcess
 | 
				
			||||||
 | 
									.Tags
 | 
				
			||||||
				.Select(x => x.Name)
 | 
									.Select(x => x.Name)
 | 
				
			||||||
				.Where(x => tagGroups
 | 
									.Where(x => tagGroups.SelectMany(y => y.Tags).All(y => y != x))
 | 
				
			||||||
					.SelectMany(y => y.Tags)
 | 
					 | 
				
			||||||
					.All(y => y != x))
 | 
					 | 
				
			||||||
				.ToList();
 | 
									.ToList();
 | 
				
			||||||
			if (tagsWithoutGroup.Any())
 | 
								if (tagsWithoutGroup.Any())
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				tagGroups.Add(new TagGroups
 | 
									tagGroups.Add(new TagGroups { Name = "Others", Tags = tagsWithoutGroup });
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Name = "Others",
 | 
					 | 
				
			||||||
					Tags = tagsWithoutGroup
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -41,22 +41,27 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
		public int Order => -1;
 | 
							public int Order => -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void OnProvidersExecuted(ApplicationModelProviderContext context)
 | 
							public void OnProvidersExecuted(ApplicationModelProviderContext context) { }
 | 
				
			||||||
		{ }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public void OnProvidersExecuting(ApplicationModelProviderContext context)
 | 
							public void OnProvidersExecuting(ApplicationModelProviderContext context)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			foreach (ActionModel action in context.Result.Controllers.SelectMany(x => x.Actions))
 | 
								foreach (ActionModel action in context.Result.Controllers.SelectMany(x => x.Actions))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				IEnumerable<ProducesResponseTypeAttribute> responses = action.Filters
 | 
									IEnumerable<ProducesResponseTypeAttribute> responses = action
 | 
				
			||||||
 | 
										.Filters
 | 
				
			||||||
					.OfType<ProducesResponseTypeAttribute>()
 | 
										.OfType<ProducesResponseTypeAttribute>()
 | 
				
			||||||
					.Where(x => x.Type == typeof(ActionResult<>));
 | 
										.Where(x => x.Type == typeof(ActionResult<>));
 | 
				
			||||||
				foreach (ProducesResponseTypeAttribute response in responses)
 | 
									foreach (ProducesResponseTypeAttribute response in responses)
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					Type type = action.ActionMethod.ReturnType;
 | 
										Type type = action.ActionMethod.ReturnType;
 | 
				
			||||||
					type = Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0] ?? type;
 | 
										type =
 | 
				
			||||||
					type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type;
 | 
											Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0]
 | 
				
			||||||
 | 
											?? type;
 | 
				
			||||||
 | 
										type =
 | 
				
			||||||
 | 
											Utility
 | 
				
			||||||
 | 
												.GetGenericDefinition(type, typeof(ActionResult<>))
 | 
				
			||||||
 | 
												?.GetGenericArguments()[0] ?? type;
 | 
				
			||||||
					response.Type = type;
 | 
										response.Type = type;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
				
			|||||||
@ -17,8 +17,8 @@
 | 
				
			|||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
					// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using Newtonsoft.Json;
 | 
					 | 
				
			||||||
using NSwag;
 | 
					using NSwag;
 | 
				
			||||||
 | 
					using Newtonsoft.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Kyoo.Swagger.Models
 | 
					namespace Kyoo.Swagger.Models
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
				
			|||||||
@ -36,49 +36,69 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public bool Process(OperationProcessorContext context)
 | 
							public bool Process(OperationProcessorContext context)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
 | 
								context.OperationDescription.Operation.Security ??=
 | 
				
			||||||
			OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
 | 
									new List<OpenApiSecurityRequirement>();
 | 
				
			||||||
				.Aggregate(new OpenApiSecurityRequirement(), (agg, _) =>
 | 
								OpenApiSecurityRequirement perms = context
 | 
				
			||||||
 | 
									.MethodInfo
 | 
				
			||||||
 | 
									.GetCustomAttributes<UserOnlyAttribute>()
 | 
				
			||||||
 | 
									.Aggregate(
 | 
				
			||||||
 | 
										new OpenApiSecurityRequirement(),
 | 
				
			||||||
 | 
										(agg, _) =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						agg[nameof(Kyoo)] = Array.Empty<string>();
 | 
											agg[nameof(Kyoo)] = Array.Empty<string>();
 | 
				
			||||||
						return agg;
 | 
											return agg;
 | 
				
			||||||
				});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			perms = context.MethodInfo.GetCustomAttributes<PermissionAttribute>()
 | 
								perms = context
 | 
				
			||||||
				.Aggregate(perms, (agg, cur) =>
 | 
									.MethodInfo
 | 
				
			||||||
 | 
									.GetCustomAttributes<PermissionAttribute>()
 | 
				
			||||||
 | 
									.Aggregate(
 | 
				
			||||||
 | 
										perms,
 | 
				
			||||||
 | 
										(agg, cur) =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						ICollection<string> permissions = _GetPermissionsList(agg, cur.Group);
 | 
											ICollection<string> permissions = _GetPermissionsList(agg, cur.Group);
 | 
				
			||||||
						permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}");
 | 
											permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}");
 | 
				
			||||||
						agg[nameof(Kyoo)] = permissions;
 | 
											agg[nameof(Kyoo)] = permissions;
 | 
				
			||||||
						return agg;
 | 
											return agg;
 | 
				
			||||||
				});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			PartialPermissionAttribute controller = context.ControllerType
 | 
								PartialPermissionAttribute controller = context
 | 
				
			||||||
 | 
									.ControllerType
 | 
				
			||||||
				.GetCustomAttribute<PartialPermissionAttribute>();
 | 
									.GetCustomAttribute<PartialPermissionAttribute>();
 | 
				
			||||||
			if (controller != null)
 | 
								if (controller != null)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				perms = context.MethodInfo.GetCustomAttributes<PartialPermissionAttribute>()
 | 
									perms = context
 | 
				
			||||||
					.Aggregate(perms, (agg, cur) =>
 | 
										.MethodInfo
 | 
				
			||||||
 | 
										.GetCustomAttributes<PartialPermissionAttribute>()
 | 
				
			||||||
 | 
										.Aggregate(
 | 
				
			||||||
 | 
											perms,
 | 
				
			||||||
 | 
											(agg, cur) =>
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
						Group? group = controller.Group != Group.Overall
 | 
												Group? group =
 | 
				
			||||||
							? controller.Group
 | 
													controller.Group != Group.Overall ? controller.Group : cur.Group;
 | 
				
			||||||
							: cur.Group;
 | 
					 | 
				
			||||||
							string type = controller.Type ?? cur.Type;
 | 
												string type = controller.Type ?? cur.Type;
 | 
				
			||||||
						Kind? kind = controller.Type == null
 | 
												Kind? kind = controller.Type == null ? controller.Kind : cur.Kind;
 | 
				
			||||||
							? controller.Kind
 | 
												ICollection<string> permissions = _GetPermissionsList(
 | 
				
			||||||
							: cur.Kind;
 | 
													agg,
 | 
				
			||||||
						ICollection<string> permissions = _GetPermissionsList(agg, group ?? Group.Overall);
 | 
													group ?? Group.Overall
 | 
				
			||||||
 | 
												);
 | 
				
			||||||
							permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}");
 | 
												permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}");
 | 
				
			||||||
							agg[nameof(Kyoo)] = permissions;
 | 
												agg[nameof(Kyoo)] = permissions;
 | 
				
			||||||
							return agg;
 | 
												return agg;
 | 
				
			||||||
					});
 | 
											}
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			context.OperationDescription.Operation.Security.Add(perms);
 | 
								context.OperationDescription.Operation.Security.Add(perms);
 | 
				
			||||||
			return true;
 | 
								return true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private static ICollection<string> _GetPermissionsList(OpenApiSecurityRequirement security, Group group)
 | 
							private static ICollection<string> _GetPermissionsList(
 | 
				
			||||||
 | 
								OpenApiSecurityRequirement security,
 | 
				
			||||||
 | 
								Group group
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return security.TryGetValue(group.ToString(), out IEnumerable<string> perms)
 | 
								return security.TryGetValue(group.ToString(), out IEnumerable<string> perms)
 | 
				
			||||||
				? perms.ToList()
 | 
									? perms.ToList()
 | 
				
			||||||
 | 
				
			|||||||
@ -78,39 +78,58 @@ namespace Kyoo.Swagger
 | 
				
			|||||||
				document.AddOperationFilter(x =>
 | 
									document.AddOperationFilter(x =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					if (x is AspNetCoreOperationProcessorContext ctx)
 | 
										if (x is AspNetCoreOperationProcessorContext ctx)
 | 
				
			||||||
						return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute;
 | 
											return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order
 | 
				
			||||||
 | 
												!= AlternativeRoute;
 | 
				
			||||||
					return true;
 | 
										return true;
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				document.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x =>
 | 
									document
 | 
				
			||||||
 | 
										.SchemaGenerator
 | 
				
			||||||
 | 
										.Settings
 | 
				
			||||||
 | 
										.TypeMappers
 | 
				
			||||||
 | 
										.Add(
 | 
				
			||||||
 | 
											new PrimitiveTypeMapper(
 | 
				
			||||||
 | 
												typeof(Identifier),
 | 
				
			||||||
 | 
												x =>
 | 
				
			||||||
							{
 | 
												{
 | 
				
			||||||
								x.IsNullableRaw = false;
 | 
													x.IsNullableRaw = false;
 | 
				
			||||||
								x.Type = JsonObjectType.String | JsonObjectType.Integer;
 | 
													x.Type = JsonObjectType.String | JsonObjectType.Integer;
 | 
				
			||||||
				}));
 | 
												}
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme
 | 
									document.AddSecurity(
 | 
				
			||||||
 | 
										nameof(Kyoo),
 | 
				
			||||||
 | 
										new OpenApiSecurityScheme
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						Type = OpenApiSecuritySchemeType.Http,
 | 
											Type = OpenApiSecuritySchemeType.Http,
 | 
				
			||||||
						Scheme = "Bearer",
 | 
											Scheme = "Bearer",
 | 
				
			||||||
						BearerFormat = "JWT",
 | 
											BearerFormat = "JWT",
 | 
				
			||||||
						Description = "The user's bearer"
 | 
											Description = "The user's bearer"
 | 
				
			||||||
				});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
				document.OperationProcessors.Add(new OperationPermissionProcessor());
 | 
									document.OperationProcessors.Add(new OperationPermissionProcessor());
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/// <inheritdoc />
 | 
							/// <inheritdoc />
 | 
				
			||||||
		public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
 | 
							public IEnumerable<IStartupAction> ConfigureSteps =>
 | 
				
			||||||
 | 
								new IStartupAction[]
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				SA.New<IApplicationBuilder>(app => app.UseOpenApi(), SA.Before + 1),
 | 
									SA.New<IApplicationBuilder>(app => app.UseOpenApi(), SA.Before + 1),
 | 
				
			||||||
			SA.New<IApplicationBuilder>(app => app.UseReDoc(x =>
 | 
									SA.New<IApplicationBuilder>(
 | 
				
			||||||
 | 
										app =>
 | 
				
			||||||
 | 
											app.UseReDoc(x =>
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							x.Path = "/doc";
 | 
												x.Path = "/doc";
 | 
				
			||||||
				x.TransformToExternalPath = (internalUiRoute, _) => "/api" + internalUiRoute;
 | 
												x.TransformToExternalPath = (internalUiRoute, _) =>
 | 
				
			||||||
 | 
													"/api" + internalUiRoute;
 | 
				
			||||||
							x.AdditionalSettings["theme"] = new
 | 
												x.AdditionalSettings["theme"] = new
 | 
				
			||||||
							{
 | 
												{
 | 
				
			||||||
								colors = new { primary = new { main = "#e13e13" } }
 | 
													colors = new { primary = new { main = "#e13e13" } }
 | 
				
			||||||
							};
 | 
												};
 | 
				
			||||||
			}), SA.Before)
 | 
											}),
 | 
				
			||||||
 | 
										SA.Before
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -48,9 +48,12 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Mock<IThumbnailsManager> thumbs = new();
 | 
								Mock<IThumbnailsManager> thumbs = new();
 | 
				
			||||||
			CollectionRepository collection = new(_NewContext(), thumbs.Object);
 | 
								CollectionRepository collection = new(_NewContext(), thumbs.Object);
 | 
				
			||||||
			StudioRepository studio = new(_NewContext(), thumbs.Object);
 | 
								StudioRepository studio = new(_NewContext(), thumbs.Object);
 | 
				
			||||||
			PeopleRepository people = new(_NewContext(),
 | 
								PeopleRepository people =
 | 
				
			||||||
 | 
									new(
 | 
				
			||||||
 | 
										_NewContext(),
 | 
				
			||||||
					new Lazy<IRepository<Show>>(() => LibraryManager.Shows),
 | 
										new Lazy<IRepository<Show>>(() => LibraryManager.Shows),
 | 
				
			||||||
				thumbs.Object);
 | 
										thumbs.Object
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object);
 | 
								MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object);
 | 
				
			||||||
			ShowRepository show = new(_NewContext(), studio, people, thumbs.Object);
 | 
								ShowRepository show = new(_NewContext(), studio, people, thumbs.Object);
 | 
				
			||||||
			SeasonRepository season = new(_NewContext(), thumbs.Object);
 | 
								SeasonRepository season = new(_NewContext(), thumbs.Object);
 | 
				
			||||||
 | 
				
			|||||||
@ -72,16 +72,8 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Collection collection = TestSample.GetNew<Collection>();
 | 
								Collection collection = TestSample.GetNew<Collection>();
 | 
				
			||||||
			collection.ExternalId = new Dictionary<string, MetadataId>
 | 
								collection.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["1"] = new()
 | 
									["1"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
									["2"] = new() { Link = "new-provider-link", DataId = "new-id" }
 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				["2"] = new()
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "new-provider-link",
 | 
					 | 
				
			||||||
					DataId = "new-id"
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Create(collection);
 | 
								await _repository.Create(collection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -111,11 +103,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
 | 
								Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
								value.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["test"] = new()
 | 
									["test"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,11 +119,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
 | 
								Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
								value.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["toto"] = new()
 | 
									["toto"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -146,11 +130,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
				KAssert.DeepEqual(value, retrieved);
 | 
									KAssert.DeepEqual(value, retrieved);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			value.ExternalId.Add("test", new MetadataId
 | 
								value.ExternalId.Add("test", new MetadataId { Link = "link", DataId = "id" });
 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Link = "link",
 | 
					 | 
				
			||||||
				DataId = "id"
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -169,11 +149,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		[InlineData("SuPeR")]
 | 
							[InlineData("SuPeR")]
 | 
				
			||||||
		public async Task SearchTest(string query)
 | 
							public async Task SearchTest(string query)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Collection value = new()
 | 
								Collection value = new() { Slug = "super-test", Name = "This is a test title", };
 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Slug = "super-test",
 | 
					 | 
				
			||||||
				Name = "This is a test title",
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			await _repository.Create(value);
 | 
								await _repository.Create(value);
 | 
				
			||||||
			ICollection<Collection> ret = await _repository.Search(query);
 | 
								ICollection<Collection> ret = await _repository.Search(query);
 | 
				
			||||||
			KAssert.DeepEqual(value, ret.First());
 | 
								KAssert.DeepEqual(value, ret.First());
 | 
				
			||||||
 | 
				
			|||||||
@ -46,7 +46,6 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		protected AEpisodeTests(RepositoryActivator repositories)
 | 
							protected AEpisodeTests(RepositoryActivator repositories)
 | 
				
			||||||
			: base(repositories)
 | 
								: base(repositories)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
					 | 
				
			||||||
			_repository = repositories.LibraryManager.Episodes;
 | 
								_repository = repositories.LibraryManager.Episodes;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -55,11 +54,17 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			Episode episode = await _repository.Get(1.AsGuid());
 | 
								Episode episode = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
 | 
				
			||||||
			await Repositories.LibraryManager.Shows.Patch(episode.ShowId, (x) =>
 | 
								await Repositories
 | 
				
			||||||
 | 
									.LibraryManager
 | 
				
			||||||
 | 
									.Shows
 | 
				
			||||||
 | 
									.Patch(
 | 
				
			||||||
 | 
										episode.ShowId,
 | 
				
			||||||
 | 
										(x) =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						x.Slug = "new-slug";
 | 
											x.Slug = "new-slug";
 | 
				
			||||||
						return Task.FromResult(true);
 | 
											return Task.FromResult(true);
 | 
				
			||||||
			});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			episode = await _repository.Get(1.AsGuid());
 | 
								episode = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal("new-slug-s1e1", episode.Slug);
 | 
								Assert.Equal("new-slug-s1e1", episode.Slug);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -69,11 +74,14 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			Episode episode = await _repository.Get(1.AsGuid());
 | 
								Episode episode = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
 | 
				
			||||||
			episode = await _repository.Patch(1.AsGuid(), (x) =>
 | 
								episode = await _repository.Patch(
 | 
				
			||||||
 | 
									1.AsGuid(),
 | 
				
			||||||
 | 
									(x) =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					x.SeasonNumber = 2;
 | 
										x.SeasonNumber = 2;
 | 
				
			||||||
					return Task.FromResult(true);
 | 
										return Task.FromResult(true);
 | 
				
			||||||
			});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
 | 
				
			||||||
			episode = await _repository.Get(1.AsGuid());
 | 
								episode = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
 | 
				
			||||||
@ -84,11 +92,17 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			Episode episode = await _repository.Get(1.AsGuid());
 | 
								Episode episode = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
 | 
				
			||||||
			episode = await Repositories.LibraryManager.Episodes.Patch(episode.Id, (x) =>
 | 
								episode = await Repositories
 | 
				
			||||||
 | 
									.LibraryManager
 | 
				
			||||||
 | 
									.Episodes
 | 
				
			||||||
 | 
									.Patch(
 | 
				
			||||||
 | 
										episode.Id,
 | 
				
			||||||
 | 
										(x) =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						x.EpisodeNumber = 2;
 | 
											x.EpisodeNumber = 2;
 | 
				
			||||||
						return Task.FromResult(true);
 | 
											return Task.FromResult(true);
 | 
				
			||||||
			});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
				
			||||||
			episode = await _repository.Get(1.AsGuid());
 | 
								episode = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
				
			||||||
@ -109,26 +123,37 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void AbsoluteSlugTest()
 | 
							public void AbsoluteSlugTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}",
 | 
								Assert.Equal(
 | 
				
			||||||
				TestSample.GetAbsoluteEpisode().Slug);
 | 
									$"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}",
 | 
				
			||||||
 | 
									TestSample.GetAbsoluteEpisode().Slug
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public async Task EpisodeCreationAbsoluteSlugTest()
 | 
							public async Task EpisodeCreationAbsoluteSlugTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
								Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", episode.Slug);
 | 
								Assert.Equal(
 | 
				
			||||||
 | 
									$"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}",
 | 
				
			||||||
 | 
									episode.Slug
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public async Task SlugEditAbsoluteTest()
 | 
							public async Task SlugEditAbsoluteTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
								Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
				
			||||||
			await Repositories.LibraryManager.Shows.Patch(episode.ShowId, (x) =>
 | 
								await Repositories
 | 
				
			||||||
 | 
									.LibraryManager
 | 
				
			||||||
 | 
									.Shows
 | 
				
			||||||
 | 
									.Patch(
 | 
				
			||||||
 | 
										episode.ShowId,
 | 
				
			||||||
 | 
										(x) =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						x.Slug = "new-slug";
 | 
											x.Slug = "new-slug";
 | 
				
			||||||
						return Task.FromResult(true);
 | 
											return Task.FromResult(true);
 | 
				
			||||||
			});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			episode = await _repository.Get(2.AsGuid());
 | 
								episode = await _repository.Get(2.AsGuid());
 | 
				
			||||||
			Assert.Equal($"new-slug-3", episode.Slug);
 | 
								Assert.Equal($"new-slug-3", episode.Slug);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -137,11 +162,14 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		public async Task AbsoluteNumberEditTest()
 | 
							public async Task AbsoluteNumberEditTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
								await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
				
			||||||
			Episode episode = await _repository.Patch(2.AsGuid(), (x) =>
 | 
								Episode episode = await _repository.Patch(
 | 
				
			||||||
 | 
									2.AsGuid(),
 | 
				
			||||||
 | 
									(x) =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					x.AbsoluteNumber = 56;
 | 
										x.AbsoluteNumber = 56;
 | 
				
			||||||
					return Task.FromResult(true);
 | 
										return Task.FromResult(true);
 | 
				
			||||||
			});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
 | 
				
			||||||
			episode = await _repository.Get(2.AsGuid());
 | 
								episode = await _repository.Get(2.AsGuid());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
 | 
				
			||||||
@ -151,12 +179,15 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		public async Task AbsoluteToNormalEditTest()
 | 
							public async Task AbsoluteToNormalEditTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
								await _repository.Create(TestSample.GetAbsoluteEpisode());
 | 
				
			||||||
			Episode episode = await _repository.Patch(2.AsGuid(), (x) =>
 | 
								Episode episode = await _repository.Patch(
 | 
				
			||||||
 | 
									2.AsGuid(),
 | 
				
			||||||
 | 
									(x) =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					x.SeasonNumber = 1;
 | 
										x.SeasonNumber = 1;
 | 
				
			||||||
					x.EpisodeNumber = 2;
 | 
										x.EpisodeNumber = 2;
 | 
				
			||||||
					return Task.FromResult(true);
 | 
										return Task.FromResult(true);
 | 
				
			||||||
			});
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
				
			||||||
			episode = await _repository.Get(2.AsGuid());
 | 
								episode = await _repository.Get(2.AsGuid());
 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
 | 
				
			||||||
@ -180,16 +211,8 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Episode value = TestSample.GetNew<Episode>();
 | 
								Episode value = TestSample.GetNew<Episode>();
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
								value.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["2"] = new()
 | 
									["2"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
									["3"] = new() { Link = "new-provider-link", DataId = "new-id" }
 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				["3"] = new()
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "new-provider-link",
 | 
					 | 
				
			||||||
					DataId = "new-id"
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Create(value);
 | 
								await _repository.Create(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -219,11 +242,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
 | 
								Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
								value.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["1"] = new()
 | 
									["1"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -239,11 +258,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
 | 
								Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
								value.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["toto"] = new()
 | 
									["toto"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -254,11 +269,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
				KAssert.DeepEqual(value, retrieved);
 | 
									KAssert.DeepEqual(value, retrieved);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			value.ExternalId.Add("test", new MetadataId
 | 
								value.ExternalId.Add("test", new MetadataId { Link = "link", DataId = "id" });
 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Link = "link",
 | 
					 | 
				
			||||||
				DataId = "id"
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -289,13 +300,19 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public async Task CreateTest()
 | 
							public async Task CreateTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			await Assert.ThrowsAsync<DuplicatedItemException>(() => _repository.Create(TestSample.Get<Episode>()));
 | 
								await Assert.ThrowsAsync<DuplicatedItemException>(
 | 
				
			||||||
 | 
									() => _repository.Create(TestSample.Get<Episode>())
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			await _repository.Delete(TestSample.Get<Episode>());
 | 
								await _repository.Delete(TestSample.Get<Episode>());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			Episode expected = TestSample.Get<Episode>();
 | 
								Episode expected = TestSample.Get<Episode>();
 | 
				
			||||||
			expected.Id = 0.AsGuid();
 | 
								expected.Id = 0.AsGuid();
 | 
				
			||||||
			expected.ShowId = (await Repositories.LibraryManager.Shows.Create(TestSample.Get<Show>())).Id;
 | 
								expected.ShowId = (
 | 
				
			||||||
			expected.SeasonId = (await Repositories.LibraryManager.Seasons.Create(TestSample.Get<Season>())).Id;
 | 
									await Repositories.LibraryManager.Shows.Create(TestSample.Get<Show>())
 | 
				
			||||||
 | 
								).Id;
 | 
				
			||||||
 | 
								expected.SeasonId = (
 | 
				
			||||||
 | 
									await Repositories.LibraryManager.Seasons.Create(TestSample.Get<Season>())
 | 
				
			||||||
 | 
								).Id;
 | 
				
			||||||
			await _repository.Create(expected);
 | 
								await _repository.Create(expected);
 | 
				
			||||||
			KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
 | 
								KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -304,10 +321,17 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		public override async Task CreateIfNotExistTest()
 | 
							public override async Task CreateIfNotExistTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Episode expected = TestSample.Get<Episode>();
 | 
								Episode expected = TestSample.Get<Episode>();
 | 
				
			||||||
			KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<Episode>()));
 | 
								KAssert.DeepEqual(
 | 
				
			||||||
 | 
									expected,
 | 
				
			||||||
 | 
									await _repository.CreateIfNotExists(TestSample.Get<Episode>())
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			await _repository.Delete(TestSample.Get<Episode>());
 | 
								await _repository.Delete(TestSample.Get<Episode>());
 | 
				
			||||||
			expected.ShowId = (await Repositories.LibraryManager.Shows.Create(TestSample.Get<Show>())).Id;
 | 
								expected.ShowId = (
 | 
				
			||||||
			expected.SeasonId = (await Repositories.LibraryManager.Seasons.Create(TestSample.Get<Season>())).Id;
 | 
									await Repositories.LibraryManager.Shows.Create(TestSample.Get<Show>())
 | 
				
			||||||
 | 
								).Id;
 | 
				
			||||||
 | 
								expected.SeasonId = (
 | 
				
			||||||
 | 
									await Repositories.LibraryManager.Seasons.Create(TestSample.Get<Season>())
 | 
				
			||||||
 | 
								).Id;
 | 
				
			||||||
			KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected));
 | 
								KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -53,11 +53,17 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			Season season = await _repository.Get(1.AsGuid());
 | 
								Season season = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal("anohana-s1", season.Slug);
 | 
								Assert.Equal("anohana-s1", season.Slug);
 | 
				
			||||||
			await Repositories.LibraryManager.Shows.Patch(season.ShowId, (x) =>
 | 
								await Repositories
 | 
				
			||||||
 | 
									.LibraryManager
 | 
				
			||||||
 | 
									.Shows
 | 
				
			||||||
 | 
									.Patch(
 | 
				
			||||||
 | 
										season.ShowId,
 | 
				
			||||||
 | 
										(x) =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						x.Slug = "new-slug";
 | 
											x.Slug = "new-slug";
 | 
				
			||||||
						return Task.FromResult(true);
 | 
											return Task.FromResult(true);
 | 
				
			||||||
			});
 | 
										}
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			season = await _repository.Get(1.AsGuid());
 | 
								season = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal("new-slug-s1", season.Slug);
 | 
								Assert.Equal("new-slug-s1", season.Slug);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -67,7 +73,9 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			Season season = await _repository.Get(1.AsGuid());
 | 
								Season season = await _repository.Get(1.AsGuid());
 | 
				
			||||||
			Assert.Equal("anohana-s1", season.Slug);
 | 
								Assert.Equal("anohana-s1", season.Slug);
 | 
				
			||||||
			await _repository.Patch(season.Id, (x) =>
 | 
								await _repository.Patch(
 | 
				
			||||||
 | 
									season.Id,
 | 
				
			||||||
 | 
									(x) =>
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					x.SeasonNumber = 2;
 | 
										x.SeasonNumber = 2;
 | 
				
			||||||
					return Task.FromResult(true);
 | 
										return Task.FromResult(true);
 | 
				
			||||||
@ -80,11 +88,9 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public async Task SeasonCreationSlugTest()
 | 
							public async Task SeasonCreationSlugTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Season season = await _repository.Create(new Season
 | 
								Season season = await _repository.Create(
 | 
				
			||||||
			{
 | 
									new Season { ShowId = TestSample.Get<Show>().Id, SeasonNumber = 2 }
 | 
				
			||||||
				ShowId = TestSample.Get<Show>().Id,
 | 
								);
 | 
				
			||||||
				SeasonNumber = 2
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			Assert.Equal($"{TestSample.Get<Show>().Slug}-s2", season.Slug);
 | 
								Assert.Equal($"{TestSample.Get<Show>().Slug}-s2", season.Slug);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -94,16 +100,8 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Season season = TestSample.GetNew<Season>();
 | 
								Season season = TestSample.GetNew<Season>();
 | 
				
			||||||
			season.ExternalId = new Dictionary<string, MetadataId>
 | 
								season.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["2"] = new()
 | 
									["2"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
									["1"] = new() { Link = "new-provider-link", DataId = "new-id" }
 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				["1"] = new()
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "new-provider-link",
 | 
					 | 
				
			||||||
					DataId = "new-id"
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Create(season);
 | 
								await _repository.Create(season);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -133,11 +131,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Season value = await _repository.Get(TestSample.Get<Season>().Slug);
 | 
								Season value = await _repository.Get(TestSample.Get<Season>().Slug);
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
								value.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["toto"] = new()
 | 
									["toto"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -153,11 +147,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Season value = await _repository.Get(TestSample.Get<Season>().Slug);
 | 
								Season value = await _repository.Get(TestSample.Get<Season>().Slug);
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>
 | 
								value.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["1"] = new()
 | 
									["1"] = new() { Link = "link", DataId = "id" },
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Link = "link",
 | 
					 | 
				
			||||||
					DataId = "id"
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -168,11 +158,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
				KAssert.DeepEqual(value, retrieved);
 | 
									KAssert.DeepEqual(value, retrieved);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			value.ExternalId.Add("toto", new MetadataId
 | 
								value.ExternalId.Add("toto", new MetadataId { Link = "link", DataId = "id" });
 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Link = "link",
 | 
					 | 
				
			||||||
				DataId = "id"
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			await _repository.Edit(value);
 | 
								await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@ -191,11 +177,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		[InlineData("SuPeR")]
 | 
							[InlineData("SuPeR")]
 | 
				
			||||||
		public async Task SearchTest(string query)
 | 
							public async Task SearchTest(string query)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Season value = new()
 | 
								Season value = new() { Name = "This is a test super title", ShowId = 1.AsGuid() };
 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Name = "This is a test super title",
 | 
					 | 
				
			||||||
				ShowId = 1.AsGuid()
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			await _repository.Create(value);
 | 
								await _repository.Create(value);
 | 
				
			||||||
			ICollection<Season> ret = await _repository.Search(query);
 | 
								ICollection<Season> ret = await _repository.Search(query);
 | 
				
			||||||
			KAssert.DeepEqual(value, ret.First());
 | 
								KAssert.DeepEqual(value, ret.First());
 | 
				
			||||||
 | 
				
			|||||||
@ -172,10 +172,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			Show value = await _repository.Get(TestSample.Get<Show>().Slug);
 | 
								Show value = await _repository.Get(TestSample.Get<Show>().Slug);
 | 
				
			||||||
			value.ExternalId = new Dictionary<string, MetadataId>()
 | 
								value.ExternalId = new Dictionary<string, MetadataId>()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["test"] = new()
 | 
									["test"] = new() { DataId = "1234" }
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					DataId = "1234"
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			Show edited = await _repository.Edit(value);
 | 
								Show edited = await _repository.Edit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -197,10 +194,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			expected.Slug = "created-relation-test";
 | 
								expected.Slug = "created-relation-test";
 | 
				
			||||||
			expected.ExternalId = new Dictionary<string, MetadataId>
 | 
								expected.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["test"] = new()
 | 
									["test"] = new() { DataId = "ID" }
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					DataId = "ID"
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			expected.Genres = new List<Genre>() { Genre.Action };
 | 
								expected.Genres = new List<Genre>() { Genre.Action };
 | 
				
			||||||
			// expected.People = new[]
 | 
								// expected.People = new[]
 | 
				
			||||||
@ -219,7 +213,8 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			KAssert.DeepEqual(expected, created);
 | 
								KAssert.DeepEqual(expected, created);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await using DatabaseContext context = Repositories.Context.New();
 | 
								await using DatabaseContext context = Repositories.Context.New();
 | 
				
			||||||
			Show retrieved = await context.Shows
 | 
								Show retrieved = await context
 | 
				
			||||||
 | 
									.Shows
 | 
				
			||||||
				// .Include(x => x.People)
 | 
									// .Include(x => x.People)
 | 
				
			||||||
				// .ThenInclude(x => x.People)
 | 
									// .ThenInclude(x => x.People)
 | 
				
			||||||
				.Include(x => x.Studio)
 | 
									.Include(x => x.Studio)
 | 
				
			||||||
@ -253,10 +248,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
			expected.Slug = "created-relation-test";
 | 
								expected.Slug = "created-relation-test";
 | 
				
			||||||
			expected.ExternalId = new Dictionary<string, MetadataId>
 | 
								expected.ExternalId = new Dictionary<string, MetadataId>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				["test"] = new()
 | 
									["test"] = new() { DataId = "ID" }
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					DataId = "ID"
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			Show created = await _repository.Create(expected);
 | 
								Show created = await _repository.Create(expected);
 | 
				
			||||||
			KAssert.DeepEqual(expected, created);
 | 
								KAssert.DeepEqual(expected, created);
 | 
				
			||||||
@ -285,11 +277,7 @@ namespace Kyoo.Tests.Database
 | 
				
			|||||||
		[InlineData("SuPeR")]
 | 
							[InlineData("SuPeR")]
 | 
				
			||||||
		public async Task SearchTest(string query)
 | 
							public async Task SearchTest(string query)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Show value = new()
 | 
								Show value = new() { Slug = "super-test", Name = "This is a test title?" };
 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Slug = "super-test",
 | 
					 | 
				
			||||||
				Name = "This is a test title?"
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			await _repository.Create(value);
 | 
								await _repository.Create(value);
 | 
				
			||||||
			ICollection<Show> ret = await _repository.Search(query);
 | 
								ICollection<Show> ret = await _repository.Search(query);
 | 
				
			||||||
			KAssert.DeepEqual(value, ret.First());
 | 
								KAssert.DeepEqual(value, ret.First());
 | 
				
			||||||
 | 
				
			|||||||
@ -29,8 +29,7 @@ using Xunit.Abstractions;
 | 
				
			|||||||
namespace Kyoo.Tests
 | 
					namespace Kyoo.Tests
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	[CollectionDefinition(nameof(Postgresql))]
 | 
						[CollectionDefinition(nameof(Postgresql))]
 | 
				
			||||||
	public class PostgresCollection : ICollectionFixture<PostgresFixture>
 | 
						public class PostgresCollection : ICollectionFixture<PostgresFixture> { }
 | 
				
			||||||
	{ }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public sealed class PostgresFixture : IDisposable
 | 
						public sealed class PostgresFixture : IDisposable
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
@ -45,9 +44,7 @@ namespace Kyoo.Tests
 | 
				
			|||||||
			string id = Guid.NewGuid().ToString().Replace('-', '_');
 | 
								string id = Guid.NewGuid().ToString().Replace('-', '_');
 | 
				
			||||||
			Template = $"kyoo_template_{id}";
 | 
								Template = $"kyoo_template_{id}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			_options = new DbContextOptionsBuilder<DatabaseContext>()
 | 
								_options = new DbContextOptionsBuilder<DatabaseContext>().UseNpgsql(Connection).Options;
 | 
				
			||||||
				.UseNpgsql(Connection)
 | 
					 | 
				
			||||||
				.Options;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			using PostgresContext context = new(_options, null);
 | 
								using PostgresContext context = new(_options, null);
 | 
				
			||||||
			context.Database.Migrate();
 | 
								context.Database.Migrate();
 | 
				
			||||||
@ -80,17 +77,23 @@ namespace Kyoo.Tests
 | 
				
			|||||||
			using (NpgsqlConnection connection = new(template.Connection))
 | 
								using (NpgsqlConnection connection = new(template.Connection))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				connection.Open();
 | 
									connection.Open();
 | 
				
			||||||
				using NpgsqlCommand cmd = new($"CREATE DATABASE {_database} WITH TEMPLATE {template.Template}", connection);
 | 
									using NpgsqlCommand cmd =
 | 
				
			||||||
 | 
										new(
 | 
				
			||||||
 | 
											$"CREATE DATABASE {_database} WITH TEMPLATE {template.Template}",
 | 
				
			||||||
 | 
											connection
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				cmd.ExecuteNonQuery();
 | 
									cmd.ExecuteNonQuery();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			_context = new DbContextOptionsBuilder<DatabaseContext>()
 | 
								_context = new DbContextOptionsBuilder<DatabaseContext>()
 | 
				
			||||||
				.UseNpgsql(GetConnectionString(_database))
 | 
									.UseNpgsql(GetConnectionString(_database))
 | 
				
			||||||
				.UseLoggerFactory(LoggerFactory.Create(x =>
 | 
									.UseLoggerFactory(
 | 
				
			||||||
 | 
										LoggerFactory.Create(x =>
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						x.ClearProviders();
 | 
											x.ClearProviders();
 | 
				
			||||||
						x.AddXunit(output);
 | 
											x.AddXunit(output);
 | 
				
			||||||
				}))
 | 
										})
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
				.EnableSensitiveDataLogging()
 | 
									.EnableSensitiveDataLogging()
 | 
				
			||||||
				.EnableDetailedErrors()
 | 
									.EnableDetailedErrors()
 | 
				
			||||||
				.Options;
 | 
									.Options;
 | 
				
			||||||
@ -101,7 +104,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
			string server = Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "127.0.0.1";
 | 
								string server = Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "127.0.0.1";
 | 
				
			||||||
			string port = Environment.GetEnvironmentVariable("POSTGRES_PORT") ?? "5432";
 | 
								string port = Environment.GetEnvironmentVariable("POSTGRES_PORT") ?? "5432";
 | 
				
			||||||
			string username = Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "KyooUser";
 | 
								string username = Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "KyooUser";
 | 
				
			||||||
			string password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "KyooPassword";
 | 
								string password =
 | 
				
			||||||
 | 
									Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "KyooPassword";
 | 
				
			||||||
			return $"Server={server};Port={port};Database={database};User ID={username};Password={password};Include Error Detail=true";
 | 
								return $"Server={server};Port={port};Database={database};User ID={username};Password={password};Include Error Detail=true";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -25,11 +25,13 @@ namespace Kyoo.Tests
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
	public static class TestSample
 | 
						public static class TestSample
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		private static readonly Dictionary<Type, Func<object>> NewSamples = new()
 | 
							private static readonly Dictionary<Type, Func<object>> NewSamples =
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Collection),
 | 
										typeof(Collection),
 | 
				
			||||||
				() => new Collection
 | 
										() =>
 | 
				
			||||||
 | 
											new Collection
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 2.AsGuid(),
 | 
												Id = 2.AsGuid(),
 | 
				
			||||||
							Slug = "new-collection",
 | 
												Slug = "new-collection",
 | 
				
			||||||
@ -40,7 +42,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Show),
 | 
										typeof(Show),
 | 
				
			||||||
				() => new Show
 | 
										() =>
 | 
				
			||||||
 | 
											new Show
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 2.AsGuid(),
 | 
												Id = 2.AsGuid(),
 | 
				
			||||||
							Slug = "new-show",
 | 
												Slug = "new-show",
 | 
				
			||||||
@ -57,7 +60,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Season),
 | 
										typeof(Season),
 | 
				
			||||||
				() => new Season
 | 
										() =>
 | 
				
			||||||
 | 
											new Season
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 2.AsGuid(),
 | 
												Id = 2.AsGuid(),
 | 
				
			||||||
							ShowId = 1.AsGuid(),
 | 
												ShowId = 1.AsGuid(),
 | 
				
			||||||
@ -72,7 +76,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Episode),
 | 
										typeof(Episode),
 | 
				
			||||||
				() => new Episode
 | 
										() =>
 | 
				
			||||||
 | 
											new Episode
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 2.AsGuid(),
 | 
												Id = 2.AsGuid(),
 | 
				
			||||||
							ShowId = 1.AsGuid(),
 | 
												ShowId = 1.AsGuid(),
 | 
				
			||||||
@ -90,7 +95,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(People),
 | 
										typeof(People),
 | 
				
			||||||
				() => new People
 | 
										() =>
 | 
				
			||||||
 | 
											new People
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 2.AsGuid(),
 | 
												Id = 2.AsGuid(),
 | 
				
			||||||
							Slug = "new-person-name",
 | 
												Slug = "new-person-name",
 | 
				
			||||||
@ -101,11 +107,13 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private static readonly Dictionary<Type, Func<object>> Samples = new()
 | 
							private static readonly Dictionary<Type, Func<object>> Samples =
 | 
				
			||||||
 | 
								new()
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Collection),
 | 
										typeof(Collection),
 | 
				
			||||||
				() => new Collection
 | 
										() =>
 | 
				
			||||||
 | 
											new Collection
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 1.AsGuid(),
 | 
												Id = 1.AsGuid(),
 | 
				
			||||||
							Slug = "collection",
 | 
												Slug = "collection",
 | 
				
			||||||
@ -116,7 +124,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Show),
 | 
										typeof(Show),
 | 
				
			||||||
				() => new Show
 | 
										() =>
 | 
				
			||||||
 | 
											new Show
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 1.AsGuid(),
 | 
												Id = 1.AsGuid(),
 | 
				
			||||||
							Slug = "anohana",
 | 
												Slug = "anohana",
 | 
				
			||||||
@ -127,9 +136,10 @@ namespace Kyoo.Tests
 | 
				
			|||||||
								"AnoHana",
 | 
													"AnoHana",
 | 
				
			||||||
								"We Still Don't Know the Name of the Flower We Saw That Day."
 | 
													"We Still Don't Know the Name of the Flower We Saw That Day."
 | 
				
			||||||
							},
 | 
												},
 | 
				
			||||||
					Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " +
 | 
												Overview =
 | 
				
			||||||
						"In time, however, these childhood friends drifted apart, and when they became high " +
 | 
													"When Yadomi Jinta was a child, he was a central piece in a group of close friends. "
 | 
				
			||||||
						"school students, they had long ceased to think of each other as friends.",
 | 
													+ "In time, however, these childhood friends drifted apart, and when they became high "
 | 
				
			||||||
 | 
													+ "school students, they had long ceased to think of each other as friends.",
 | 
				
			||||||
							Status = Status.Finished,
 | 
												Status = Status.Finished,
 | 
				
			||||||
							StudioId = 1.AsGuid(),
 | 
												StudioId = 1.AsGuid(),
 | 
				
			||||||
							StartAir = new DateTime(2011, 1, 1).ToUniversalTime(),
 | 
												StartAir = new DateTime(2011, 1, 1).ToUniversalTime(),
 | 
				
			||||||
@ -142,7 +152,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Season),
 | 
										typeof(Season),
 | 
				
			||||||
				() => new Season
 | 
										() =>
 | 
				
			||||||
 | 
											new Season
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 1.AsGuid(),
 | 
												Id = 1.AsGuid(),
 | 
				
			||||||
							ShowSlug = "anohana",
 | 
												ShowSlug = "anohana",
 | 
				
			||||||
@ -159,7 +170,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Episode),
 | 
										typeof(Episode),
 | 
				
			||||||
				() => new Episode
 | 
										() =>
 | 
				
			||||||
 | 
											new Episode
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 1.AsGuid(),
 | 
												Id = 1.AsGuid(),
 | 
				
			||||||
							ShowSlug = "anohana",
 | 
												ShowSlug = "anohana",
 | 
				
			||||||
@ -179,7 +191,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(People),
 | 
										typeof(People),
 | 
				
			||||||
				() => new People
 | 
										() =>
 | 
				
			||||||
 | 
											new People
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 1.AsGuid(),
 | 
												Id = 1.AsGuid(),
 | 
				
			||||||
							Slug = "the-actor",
 | 
												Slug = "the-actor",
 | 
				
			||||||
@ -191,7 +204,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(Studio),
 | 
										typeof(Studio),
 | 
				
			||||||
				() => new Studio
 | 
										() =>
 | 
				
			||||||
 | 
											new Studio
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 1.AsGuid(),
 | 
												Id = 1.AsGuid(),
 | 
				
			||||||
							Slug = "hyper-studio",
 | 
												Slug = "hyper-studio",
 | 
				
			||||||
@ -200,7 +214,8 @@ namespace Kyoo.Tests
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					typeof(User),
 | 
										typeof(User),
 | 
				
			||||||
				() => new User
 | 
										() =>
 | 
				
			||||||
 | 
											new User
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							Id = 1.AsGuid(),
 | 
												Id = 1.AsGuid(),
 | 
				
			||||||
							Slug = "user",
 | 
												Slug = "user",
 | 
				
			||||||
 | 
				
			|||||||
@ -29,9 +29,12 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		public void IfEmptyTest()
 | 
							public void IfEmptyTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			int[] list = { 1, 2, 3, 4 };
 | 
								int[] list = { 1, 2, 3, 4 };
 | 
				
			||||||
			list = list.IfEmpty(() => KAssert.Fail("Empty action should not be triggered.")).ToArray();
 | 
								list = list.IfEmpty(() => KAssert.Fail("Empty action should not be triggered."))
 | 
				
			||||||
 | 
									.ToArray();
 | 
				
			||||||
			list = Array.Empty<int>();
 | 
								list = Array.Empty<int>();
 | 
				
			||||||
			Assert.Throws<ArgumentException>(() => list.IfEmpty(() => throw new ArgumentException()).ToList());
 | 
								Assert.Throws<ArgumentException>(
 | 
				
			||||||
 | 
									() => list.IfEmpty(() => throw new ArgumentException()).ToList()
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			Assert.Empty(list.IfEmpty(() => { }));
 | 
								Assert.Empty(list.IfEmpty(() => { }));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,15 +29,8 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void CompleteTest()
 | 
							public void CompleteTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Studio genre = new()
 | 
								Studio genre = new() { Name = "merged" };
 | 
				
			||||||
			{
 | 
								Studio genre2 = new() { Name = "test", Id = 5.AsGuid(), };
 | 
				
			||||||
				Name = "merged"
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			Studio genre2 = new()
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Name = "test",
 | 
					 | 
				
			||||||
				Id = 5.AsGuid(),
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			Studio ret = Merger.Complete(genre, genre2);
 | 
								Studio ret = Merger.Complete(genre, genre2);
 | 
				
			||||||
			Assert.True(ReferenceEquals(genre, ret));
 | 
								Assert.True(ReferenceEquals(genre, ret));
 | 
				
			||||||
			Assert.Equal(5.AsGuid(), ret.Id);
 | 
								Assert.Equal(5.AsGuid(), ret.Id);
 | 
				
			||||||
@ -48,15 +41,8 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void CompleteDictionaryTest()
 | 
							public void CompleteDictionaryTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Collection collection = new()
 | 
								Collection collection = new() { Name = "merged", };
 | 
				
			||||||
			{
 | 
								Collection collection2 = new() { Id = 5.AsGuid(), Name = "test", };
 | 
				
			||||||
				Name = "merged",
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			Collection collection2 = new()
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Id = 5.AsGuid(),
 | 
					 | 
				
			||||||
				Name = "test",
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			Collection ret = Merger.Complete(collection, collection2);
 | 
								Collection ret = Merger.Complete(collection, collection2);
 | 
				
			||||||
			Assert.True(ReferenceEquals(collection, ret));
 | 
								Assert.True(ReferenceEquals(collection, ret));
 | 
				
			||||||
			Assert.Equal(5.AsGuid(), ret.Id);
 | 
								Assert.Equal(5.AsGuid(), ret.Id);
 | 
				
			||||||
@ -67,17 +53,14 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void CompleteDictionaryOutParam()
 | 
							public void CompleteDictionaryOutParam()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Dictionary<string, string> first = new()
 | 
								Dictionary<string, string> first = new() { ["logo"] = "logo", ["poster"] = "poster" };
 | 
				
			||||||
			{
 | 
								Dictionary<string, string> second =
 | 
				
			||||||
				["logo"] = "logo",
 | 
									new() { ["poster"] = "new-poster", ["thumbnail"] = "thumbnails" };
 | 
				
			||||||
				["poster"] = "poster"
 | 
								IDictionary<string, string> ret = Merger.CompleteDictionaries(
 | 
				
			||||||
			};
 | 
									first,
 | 
				
			||||||
			Dictionary<string, string> second = new()
 | 
									second,
 | 
				
			||||||
			{
 | 
									out bool changed
 | 
				
			||||||
				["poster"] = "new-poster",
 | 
								);
 | 
				
			||||||
				["thumbnail"] = "thumbnails"
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
 | 
					 | 
				
			||||||
			Assert.True(changed);
 | 
								Assert.True(changed);
 | 
				
			||||||
			Assert.Equal(3, ret.Count);
 | 
								Assert.Equal(3, ret.Count);
 | 
				
			||||||
			Assert.Equal("new-poster", ret["poster"]);
 | 
								Assert.Equal("new-poster", ret["poster"]);
 | 
				
			||||||
@ -88,15 +71,13 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void CompleteDictionaryEqualTest()
 | 
							public void CompleteDictionaryEqualTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Dictionary<string, string> first = new()
 | 
								Dictionary<string, string> first = new() { ["poster"] = "poster" };
 | 
				
			||||||
			{
 | 
								Dictionary<string, string> second = new() { ["poster"] = "new-poster", };
 | 
				
			||||||
				["poster"] = "poster"
 | 
								IDictionary<string, string> ret = Merger.CompleteDictionaries(
 | 
				
			||||||
			};
 | 
									first,
 | 
				
			||||||
			Dictionary<string, string> second = new()
 | 
									second,
 | 
				
			||||||
			{
 | 
									out bool changed
 | 
				
			||||||
				["poster"] = "new-poster",
 | 
								);
 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
 | 
					 | 
				
			||||||
			Assert.True(changed);
 | 
								Assert.True(changed);
 | 
				
			||||||
			Assert.Single(ret);
 | 
								Assert.Single(ret);
 | 
				
			||||||
			Assert.Equal("new-poster", ret["poster"]);
 | 
								Assert.Equal("new-poster", ret["poster"]);
 | 
				
			||||||
@ -121,17 +102,8 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void CompleteDictionaryNoChangeNoSetTest()
 | 
							public void CompleteDictionaryNoChangeNoSetTest()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			TestMergeSetter first = new()
 | 
								TestMergeSetter first = new() { Backing = new Dictionary<int, int> { [2] = 3 } };
 | 
				
			||||||
			{
 | 
								TestMergeSetter second = new() { Backing = new Dictionary<int, int>() };
 | 
				
			||||||
				Backing = new Dictionary<int, int>
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					[2] = 3
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			TestMergeSetter second = new()
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				Backing = new Dictionary<int, int>()
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			Merger.Complete(first, second);
 | 
								Merger.Complete(first, second);
 | 
				
			||||||
			// This should no call the setter of first so the test should pass.
 | 
								// This should no call the setter of first so the test should pass.
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -139,17 +111,14 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void CompleteDictionaryNullValue()
 | 
							public void CompleteDictionaryNullValue()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Dictionary<string, string> first = new()
 | 
								Dictionary<string, string> first = new() { ["logo"] = "logo", ["poster"] = null };
 | 
				
			||||||
			{
 | 
								Dictionary<string, string> second =
 | 
				
			||||||
				["logo"] = "logo",
 | 
									new() { ["poster"] = "new-poster", ["thumbnail"] = "thumbnails" };
 | 
				
			||||||
				["poster"] = null
 | 
								IDictionary<string, string> ret = Merger.CompleteDictionaries(
 | 
				
			||||||
			};
 | 
									first,
 | 
				
			||||||
			Dictionary<string, string> second = new()
 | 
									second,
 | 
				
			||||||
			{
 | 
									out bool changed
 | 
				
			||||||
				["poster"] = "new-poster",
 | 
								);
 | 
				
			||||||
				["thumbnail"] = "thumbnails"
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
 | 
					 | 
				
			||||||
			Assert.True(changed);
 | 
								Assert.True(changed);
 | 
				
			||||||
			Assert.Equal(3, ret.Count);
 | 
								Assert.Equal(3, ret.Count);
 | 
				
			||||||
			Assert.Equal("new-poster", ret["poster"]);
 | 
								Assert.Equal("new-poster", ret["poster"]);
 | 
				
			||||||
@ -160,16 +129,13 @@ namespace Kyoo.Tests.Utility
 | 
				
			|||||||
		[Fact]
 | 
							[Fact]
 | 
				
			||||||
		public void CompleteDictionaryNullValueNoChange()
 | 
							public void CompleteDictionaryNullValueNoChange()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Dictionary<string, string> first = new()
 | 
								Dictionary<string, string> first = new() { ["logo"] = "logo", ["poster"] = null };
 | 
				
			||||||
			{
 | 
								Dictionary<string, string> second = new() { ["poster"] = null, };
 | 
				
			||||||
				["logo"] = "logo",
 | 
								IDictionary<string, string> ret = Merger.CompleteDictionaries(
 | 
				
			||||||
				["poster"] = null
 | 
									first,
 | 
				
			||||||
			};
 | 
									second,
 | 
				
			||||||
			Dictionary<string, string> second = new()
 | 
									out bool changed
 | 
				
			||||||
			{
 | 
								);
 | 
				
			||||||
				["poster"] = null,
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
 | 
					 | 
				
			||||||
			Assert.False(changed);
 | 
								Assert.False(changed);
 | 
				
			||||||
			Assert.Equal(2, ret.Count);
 | 
								Assert.Equal(2, ret.Count);
 | 
				
			||||||
			Assert.Null(ret["poster"]);
 | 
								Assert.Null(ret["poster"]);
 | 
				
			||||||
 | 
				
			|||||||
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