mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-01 04:34:50 -04: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