diff --git a/back/.config/dotnet-tools.json b/back/.config/dotnet-tools.json index 2be6730d..7076f7c1 100644 --- a/back/.config/dotnet-tools.json +++ b/back/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "dotnet-ef" ] + }, + "csharpier": { + "version": "0.26.4", + "commands": [ + "dotnet-csharpier" + ] } } } \ No newline at end of file diff --git a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs index 65f6df39..7533f43e 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs @@ -100,11 +100,13 @@ namespace Kyoo.Abstractions.Controllers /// Reverse the sort. /// Select the first element after this id if it was in a list. /// The resource found - Task GetOrDefault(Filter? filter, + Task GetOrDefault( + Filter? filter, Include? include = default, Sort? sortBy = default, bool reverse = false, - Guid? afterId = default); + Guid? afterId = default + ); /// /// Search for resources with the database. @@ -122,10 +124,12 @@ namespace Kyoo.Abstractions.Controllers /// The related fields to include. /// How pagination should be done (where to start and how many to return) /// A list of resources that match every filters - Task> GetAll(Filter? filter = null, + Task> GetAll( + Filter? filter = null, Sort? sort = default, Include? include = default, - Pagination? limit = default); + Pagination? limit = default + ); /// /// Get the number of resources that match the filter's predicate. @@ -166,8 +170,8 @@ namespace Kyoo.Abstractions.Controllers /// /// The resource newly created. /// A representing the asynchronous operation. - protected static Task OnResourceCreated(T obj) - => OnCreated?.Invoke(obj) ?? Task.CompletedTask; + protected static Task OnResourceCreated(T obj) => + OnCreated?.Invoke(obj) ?? Task.CompletedTask; /// /// Edit a resource and replace every property @@ -199,8 +203,8 @@ namespace Kyoo.Abstractions.Controllers /// /// The resource newly edited. /// A representing the asynchronous operation. - protected static Task OnResourceEdited(T obj) - => OnEdited?.Invoke(obj) ?? Task.CompletedTask; + protected static Task OnResourceEdited(T obj) => + OnEdited?.Invoke(obj) ?? Task.CompletedTask; /// /// Delete a resource by it's ID @@ -243,8 +247,8 @@ namespace Kyoo.Abstractions.Controllers /// /// The resource newly deleted. /// A representing the asynchronous operation. - protected static Task OnResourceDeleted(T obj) - => OnDeleted?.Invoke(obj) ?? Task.CompletedTask; + protected static Task OnResourceDeleted(T obj) => + OnDeleted?.Invoke(obj) ?? Task.CompletedTask; } /// diff --git a/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs b/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs index 71a4656d..e6175018 100644 --- a/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs @@ -35,10 +35,12 @@ public interface ISearchManager /// How pagination should be done (where to start and how many to return) /// The related fields to include. /// A list of resources that match every filters - public Task.SearchResult> SearchItems(string? query, + public Task.SearchResult> SearchItems( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default); + Include? include = default + ); /// /// Search for movies. @@ -48,10 +50,12 @@ public interface ISearchManager /// How pagination should be done (where to start and how many to return) /// The related fields to include. /// A list of resources that match every filters - public Task.SearchResult> SearchMovies(string? query, + public Task.SearchResult> SearchMovies( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default); + Include? include = default + ); /// /// Search for shows. @@ -61,10 +65,12 @@ public interface ISearchManager /// How pagination should be done (where to start and how many to return) /// The related fields to include. /// A list of resources that match every filters - public Task.SearchResult> SearchShows(string? query, + public Task.SearchResult> SearchShows( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default); + Include? include = default + ); /// /// Search for collections. @@ -74,10 +80,12 @@ public interface ISearchManager /// How pagination should be done (where to start and how many to return) /// The related fields to include. /// A list of resources that match every filters - public Task.SearchResult> SearchCollections(string? query, + public Task.SearchResult> SearchCollections( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default); + Include? include = default + ); /// /// Search for episodes. @@ -87,10 +95,12 @@ public interface ISearchManager /// How pagination should be done (where to start and how many to return) /// The related fields to include. /// A list of resources that match every filters - public Task.SearchResult> SearchEpisodes(string? query, + public Task.SearchResult> SearchEpisodes( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default); + Include? include = default + ); /// /// Search for studios. @@ -100,8 +110,10 @@ public interface ISearchManager /// How pagination should be done (where to start and how many to return) /// The related fields to include. /// A list of resources that match every filters - public Task.SearchResult> SearchStudios(string? query, + public Task.SearchResult> SearchStudios( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default); + Include? include = default + ); } diff --git a/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs index b65dc794..d9d9dcbf 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs @@ -39,11 +39,17 @@ public interface IWatchStatusRepository Task> GetAll( Filter? filter = default, Include? include = default, - Pagination? limit = default); + Pagination? limit = default + ); Task GetMovieStatus(Guid movieId, Guid userId); - Task SetMovieStatus(Guid movieId, Guid userId, WatchStatus status, int? watchedTime); + Task SetMovieStatus( + Guid movieId, + Guid userId, + WatchStatus status, + int? watchedTime + ); Task DeleteMovieStatus(Guid movieId, Guid userId); @@ -57,7 +63,12 @@ public interface IWatchStatusRepository /// Where the user has stopped watching. Only usable if Status /// is - Task SetEpisodeStatus(Guid episodeId, Guid userId, WatchStatus status, int? watchedTime); + Task SetEpisodeStatus( + Guid episodeId, + Guid userId, + WatchStatus status, + int? watchedTime + ); Task DeleteEpisodeStatus(Guid episodeId, Guid userId); } diff --git a/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs b/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs index 279ec516..ea9d9acf 100644 --- a/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs +++ b/back/src/Kyoo.Abstractions/Controllers/StartupAction.cs @@ -17,7 +17,6 @@ // along with Kyoo. If not, see . using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Abstractions.Controllers @@ -26,8 +25,6 @@ namespace Kyoo.Abstractions.Controllers /// A list of constant priorities used for 's . /// It also contains helper methods for creating new . /// - [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 { /// @@ -72,8 +69,7 @@ namespace Kyoo.Abstractions.Controllers /// The action to run /// The priority of the new action /// A new - public static StartupAction New(Action action, int priority) - => new(action, priority); + public static StartupAction New(Action action, int priority) => new(action, priority); /// /// Create a new . @@ -83,8 +79,7 @@ namespace Kyoo.Abstractions.Controllers /// A dependency that this action will use. /// A new public static StartupAction New(Action action, int priority) - where T : notnull - => new(action, priority); + where T : notnull => new(action, priority); /// /// Create a new . @@ -96,8 +91,7 @@ namespace Kyoo.Abstractions.Controllers /// A new public static StartupAction New(Action action, int priority) where T : notnull - where T2 : notnull - => new(action, priority); + where T2 : notnull => new(action, priority); /// /// Create a new . @@ -108,11 +102,13 @@ namespace Kyoo.Abstractions.Controllers /// A second dependency that this action will use. /// A third dependency that this action will use. /// A new - public static StartupAction New(Action action, int priority) + public static StartupAction New( + Action action, + int priority + ) where T : notnull where T2 : notnull - where T3 : notnull - => new(action, priority); + where T3 : notnull => new(action, priority); /// /// A with no dependencies. @@ -209,10 +205,7 @@ namespace Kyoo.Abstractions.Controllers /// public void Run(IServiceProvider provider) { - _action.Invoke( - provider.GetRequiredService(), - provider.GetRequiredService() - ); + _action.Invoke(provider.GetRequiredService(), provider.GetRequiredService()); } } diff --git a/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs b/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs index f0aa4c1c..635d4f71 100644 --- a/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs +++ b/back/src/Kyoo.Abstractions/Models/Exceptions/DuplicatedItemException.cs @@ -48,7 +48,6 @@ namespace Kyoo.Abstractions.Models.Exceptions /// Serialization infos /// The serialization context protected DuplicatedItemException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + : base(info, context) { } } } diff --git a/back/src/Kyoo.Abstractions/Models/Exceptions/ItemNotFoundException.cs b/back/src/Kyoo.Abstractions/Models/Exceptions/ItemNotFoundException.cs index 96499762..f8a79486 100644 --- a/back/src/Kyoo.Abstractions/Models/Exceptions/ItemNotFoundException.cs +++ b/back/src/Kyoo.Abstractions/Models/Exceptions/ItemNotFoundException.cs @@ -37,8 +37,7 @@ namespace Kyoo.Abstractions.Models.Exceptions /// /// The message of the exception public ItemNotFoundException(string message) - : base(message) - { } + : base(message) { } /// /// The serialization constructor @@ -46,7 +45,6 @@ namespace Kyoo.Abstractions.Models.Exceptions /// Serialization infos /// The serialization context protected ItemNotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + : base(info, context) { } } } diff --git a/back/src/Kyoo.Abstractions/Models/Exceptions/UnauthorizedException.cs b/back/src/Kyoo.Abstractions/Models/Exceptions/UnauthorizedException.cs index d1d7b339..27415a97 100644 --- a/back/src/Kyoo.Abstractions/Models/Exceptions/UnauthorizedException.cs +++ b/back/src/Kyoo.Abstractions/Models/Exceptions/UnauthorizedException.cs @@ -25,15 +25,12 @@ namespace Kyoo.Abstractions.Models.Exceptions public class UnauthorizedException : Exception { public UnauthorizedException() - : base("User not authenticated or token invalid.") - { } + : base("User not authenticated or token invalid.") { } public UnauthorizedException(string message) - : base(message) - { } + : base(message) { } protected UnauthorizedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + : base(info, context) { } } } diff --git a/back/src/Kyoo.Abstractions/Models/IWatchlist.cs b/back/src/Kyoo.Abstractions/Models/IWatchlist.cs index 8e2b76b8..03022701 100644 --- a/back/src/Kyoo.Abstractions/Models/IWatchlist.cs +++ b/back/src/Kyoo.Abstractions/Models/IWatchlist.cs @@ -24,5 +24,4 @@ namespace Kyoo.Abstractions.Models; /// A watch list item. /// [OneOf(Types = new[] { typeof(Show), typeof(Movie) })] -public interface IWatchlist : IResource, IThumbnails, IMetadata, IAddedDate -{ } +public interface IWatchlist : IResource, IThumbnails, IMetadata, IAddedDate { } diff --git a/back/src/Kyoo.Abstractions/Models/Page.cs b/back/src/Kyoo.Abstractions/Models/Page.cs index f624370e..8f64d258 100644 --- a/back/src/Kyoo.Abstractions/Models/Page.cs +++ b/back/src/Kyoo.Abstractions/Models/Page.cs @@ -67,7 +67,13 @@ namespace Kyoo.Abstractions.Models /// The link of the previous page. /// The link of the next page. /// The link of the first page. - public Page(ICollection items, string @this, string? previous, string? next, string first) + public Page( + ICollection items, + string @this, + string? previous, + string? next, + string first + ) { Items = items; This = @this; @@ -83,10 +89,7 @@ namespace Kyoo.Abstractions.Models /// The base url of the resources available from this page. /// The list of query strings of the current page /// The number of items requested for the current page. - public Page(ICollection items, - string url, - Dictionary query, - int limit) + public Page(ICollection items, string url, Dictionary query, int limit) { Items = items; This = url + query.ToQueryString(); diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs index 6634cb9e..75f8b5d9 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs @@ -37,7 +37,8 @@ namespace Kyoo.Abstractions.Models public Guid Id { get; set; } /// - [MaxLength(256)] public string Slug { get; set; } + [MaxLength(256)] + public string Slug { get; set; } /// /// The name of this collection. @@ -64,12 +65,14 @@ namespace Kyoo.Abstractions.Models /// /// The list of movies contained in this collection. /// - [SerializeIgnore] public ICollection? Movies { get; set; } + [SerializeIgnore] + public ICollection? Movies { get; set; } /// /// The list of shows contained in this collection. /// - [SerializeIgnore] public ICollection? Shows { get; set; } + [SerializeIgnore] + public ICollection? Shows { get; set; } /// public Dictionary ExternalId { get; set; } = new(); diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs index 23c9101b..8cd63b1a 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs @@ -34,11 +34,12 @@ namespace Kyoo.Abstractions.Models public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, INews { // Use absolute numbers by default and fallback to season/episodes if it does not exists. - public static Sort DefaultSort => new Sort.Conglomerate( - new Sort.By(x => x.AbsoluteNumber), - new Sort.By(x => x.SeasonNumber), - new Sort.By(x => x.EpisodeNumber) - ); + public static Sort DefaultSort => + new Sort.Conglomerate( + new Sort.By(x => x.AbsoluteNumber), + new Sort.By(x => x.SeasonNumber), + new Sort.By(x => x.EpisodeNumber) + ); /// public Guid Id { get; set; } @@ -51,10 +52,14 @@ namespace Kyoo.Abstractions.Models get { 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); } - [UsedImplicitly] private set { @@ -85,7 +90,8 @@ namespace Kyoo.Abstractions.Models /// /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. /// - [SerializeIgnore] public string? ShowSlug { private get; set; } + [SerializeIgnore] + public string? ShowSlug { private get; set; } /// /// The ID of the Show containing this episode. @@ -95,7 +101,8 @@ namespace Kyoo.Abstractions.Models /// /// The show that contains this episode. /// - [LoadableRelation(nameof(ShowId))] public Show? Show { get; set; } + [LoadableRelation(nameof(ShowId))] + public Show? Show { get; set; } /// /// 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 /// by it's . /// - [LoadableRelation(nameof(SeasonId))] public Season? Season { get; set; } + [LoadableRelation(nameof(SeasonId))] + public Season? Season { get; set; } /// /// The season in witch this episode is in. @@ -192,16 +200,19 @@ namespace Kyoo.Abstractions.Models )] public Episode? PreviousEpisode { get; set; } - private Episode? _PreviousEpisode => Show!.Episodes! - .OrderBy(x => x.AbsoluteNumber == null) - .ThenByDescending(x => x.AbsoluteNumber) - .ThenByDescending(x => x.SeasonNumber) - .ThenByDescending(x => x.EpisodeNumber) - .FirstOrDefault(x => - x.AbsoluteNumber < AbsoluteNumber - || x.SeasonNumber < SeasonNumber - || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber) - ); + private Episode? _PreviousEpisode => + Show! + .Episodes! + .OrderBy(x => x.AbsoluteNumber == null) + .ThenByDescending(x => x.AbsoluteNumber) + .ThenByDescending(x => x.SeasonNumber) + .ThenByDescending(x => x.EpisodeNumber) + .FirstOrDefault( + x => + x.AbsoluteNumber < AbsoluteNumber + || x.SeasonNumber < SeasonNumber + || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber) + ); /// /// The next episode to watch after this one. @@ -229,17 +240,21 @@ namespace Kyoo.Abstractions.Models )] public Episode? NextEpisode { get; set; } - private Episode? _NextEpisode => Show!.Episodes! - .OrderBy(x => x.AbsoluteNumber) - .ThenBy(x => x.SeasonNumber) - .ThenBy(x => x.EpisodeNumber) - .FirstOrDefault(x => - x.AbsoluteNumber > AbsoluteNumber - || x.SeasonNumber > SeasonNumber - || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber) - ); + private Episode? _NextEpisode => + Show! + .Episodes! + .OrderBy(x => x.AbsoluteNumber) + .ThenBy(x => x.SeasonNumber) + .ThenBy(x => x.EpisodeNumber) + .FirstOrDefault( + x => + x.AbsoluteNumber > AbsoluteNumber + || x.SeasonNumber > SeasonNumber + || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber) + ); - [SerializeIgnore] public ICollection? Watched { get; set; } + [SerializeIgnore] + public ICollection? Watched { get; set; } /// /// Metadata of what an user as started/planned to watch. @@ -257,11 +272,12 @@ namespace Kyoo.Abstractions.Models /// /// Links to watch this episode. /// - public VideoLinks Links => new() - { - Direct = $"/video/episode/{Slug}/direct", - Hls = $"/video/episode/{Slug}/master.m3u8", - }; + public VideoLinks Links => + new() + { + Direct = $"/video/episode/{Slug}/direct", + Hls = $"/video/episode/{Slug}/master.m3u8", + }; /// /// Get the slug of an episode. @@ -280,10 +296,12 @@ namespace Kyoo.Abstractions.Models /// If you don't know it or this is a movie, use null /// /// The slug corresponding to the given arguments - public static string GetSlug(string showSlug, + public static string GetSlug( + string showSlug, int? seasonNumber, int? episodeNumber, - int? absoluteNumber = null) + int? absoluteNumber = null + ) { return seasonNumber switch { diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs index 057601b7..b590c98d 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -82,7 +82,11 @@ namespace Kyoo.Abstractions.Models } /// - 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) return base.ConvertFrom(context, culture, value)!; @@ -90,7 +94,10 @@ namespace Kyoo.Abstractions.Models } /// - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + public override bool CanConvertTo( + ITypeDescriptorContext? context, + Type? destinationType + ) { return false; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs index fff39628..bfaf4bb5 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs @@ -31,7 +31,16 @@ namespace Kyoo.Abstractions.Models /// /// A series or a movie. /// - 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.By(x => x.Name); @@ -120,12 +129,14 @@ namespace Kyoo.Abstractions.Models /// /// The ID of the Studio that made this show. /// - [SerializeIgnore] public Guid? StudioId { get; set; } + [SerializeIgnore] + public Guid? StudioId { get; set; } /// /// The Studio that made this show. /// - [LoadableRelation(nameof(StudioId))] public Studio? Studio { get; set; } + [LoadableRelation(nameof(StudioId))] + public Studio? Studio { get; set; } // /// // /// The list of people that made this show. @@ -135,18 +146,21 @@ namespace Kyoo.Abstractions.Models /// /// The list of collections that contains this show. /// - [SerializeIgnore] public ICollection? Collections { get; set; } + [SerializeIgnore] + public ICollection? Collections { get; set; } /// /// Links to watch this movie. /// - public VideoLinks Links => new() - { - Direct = $"/video/movie/{Slug}/direct", - Hls = $"/video/movie/{Slug}/master.m3u8", - }; + public VideoLinks Links => + new() + { + Direct = $"/video/movie/{Slug}/direct", + Hls = $"/video/movie/{Slug}/master.m3u8", + }; - [SerializeIgnore] public ICollection? Watched { get; set; } + [SerializeIgnore] + public ICollection? Watched { get; set; } /// /// Metadata of what an user as started/planned to watch. diff --git a/back/src/Kyoo.Abstractions/Models/Resources/People.cs b/back/src/Kyoo.Abstractions/Models/Resources/People.cs index d1412506..336b7e6b 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/People.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/People.cs @@ -62,7 +62,8 @@ namespace Kyoo.Abstractions.Models /// /// The list of roles this person has played in. See for more information. /// - [SerializeIgnore] public ICollection? Roles { get; set; } + [SerializeIgnore] + public ICollection? Roles { get; set; } public People() { } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs index 2b7d38d2..c7d4c459 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs @@ -49,7 +49,6 @@ namespace Kyoo.Abstractions.Models return $"{ShowId}-s{SeasonNumber}"; return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}"; } - [UsedImplicitly] [NotNull] private set @@ -57,7 +56,9 @@ namespace Kyoo.Abstractions.Models Match match = Regex.Match(value, @"(?.+)-s(?\d+)"); 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; SeasonNumber = int.Parse(match.Groups["season"].Value); } @@ -66,7 +67,8 @@ namespace Kyoo.Abstractions.Models /// /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. /// - [SerializeIgnore] public string? ShowSlug { private get; set; } + [SerializeIgnore] + public string? ShowSlug { private get; set; } /// /// The ID of the Show containing this season. @@ -76,7 +78,8 @@ namespace Kyoo.Abstractions.Models /// /// The show that contains this season. /// - [LoadableRelation(nameof(ShowId))] public Show? Show { get; set; } + [LoadableRelation(nameof(ShowId))] + public Show? Show { get; set; } /// /// The number of this season. This can be set to 0 to indicate specials. @@ -121,7 +124,8 @@ namespace Kyoo.Abstractions.Models /// /// The list of episodes that this season contains. /// - [SerializeIgnore] public ICollection? Episodes { get; set; } + [SerializeIgnore] + public ICollection? Episodes { get; set; } /// /// The number of episodes in this season. diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs index bef33e61..31a82c20 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -32,7 +32,15 @@ namespace Kyoo.Abstractions.Models /// /// A series or a movie. /// - 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.By(x => x.Name); @@ -121,12 +129,14 @@ namespace Kyoo.Abstractions.Models /// /// The ID of the Studio that made this show. /// - [SerializeIgnore] public Guid? StudioId { get; set; } + [SerializeIgnore] + public Guid? StudioId { get; set; } /// /// The Studio that made this show. /// - [LoadableRelation(nameof(StudioId))] public Studio? Studio { get; set; } + [LoadableRelation(nameof(StudioId))] + public Studio? Studio { get; set; } // /// // /// The list of people that made this show. @@ -136,19 +146,22 @@ namespace Kyoo.Abstractions.Models /// /// The different seasons in this show. If this is a movie, this list is always null or empty. /// - [SerializeIgnore] public ICollection? Seasons { get; set; } + [SerializeIgnore] + public ICollection? Seasons { get; set; } /// /// The list of episodes in this show. /// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null). /// Having an episode is necessary to store metadata and tracks. /// - [SerializeIgnore] public ICollection? Episodes { get; set; } + [SerializeIgnore] + public ICollection? Episodes { get; set; } /// /// The list of collections that contains this show. /// - [SerializeIgnore] public ICollection? Collections { get; set; } + [SerializeIgnore] + public ICollection? Collections { get; set; } /// /// The first episode of this show. @@ -172,11 +185,12 @@ namespace Kyoo.Abstractions.Models )] public Episode? FirstEpisode { get; set; } - private Episode? _FirstEpisode => Episodes! - .OrderBy(x => x.AbsoluteNumber) - .ThenBy(x => x.SeasonNumber) - .ThenBy(x => x.EpisodeNumber) - .FirstOrDefault(); + private Episode? _FirstEpisode => + Episodes! + .OrderBy(x => x.AbsoluteNumber) + .ThenBy(x => x.SeasonNumber) + .ThenBy(x => x.EpisodeNumber) + .FirstOrDefault(); /// /// The number of episodes in this show. @@ -199,7 +213,8 @@ namespace Kyoo.Abstractions.Models private int _EpisodesCount => Episodes!.Count; - [SerializeIgnore] public ICollection? Watched { get; set; } + [SerializeIgnore] + public ICollection? Watched { get; set; } /// /// Metadata of what an user as started/planned to watch. diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs b/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs index 8b7094ed..2f4eef3d 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs @@ -48,12 +48,14 @@ namespace Kyoo.Abstractions.Models /// /// The list of shows that are made by this studio. /// - [SerializeIgnore] public ICollection? Shows { get; set; } + [SerializeIgnore] + public ICollection? Shows { get; set; } /// /// The list of movies that are made by this studio. /// - [SerializeIgnore] public ICollection? Movies { get; set; } + [SerializeIgnore] + public ICollection? Movies { get; set; } /// public Dictionary ExternalId { get; set; } = new(); diff --git a/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs b/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs index 80216f2d..bc67f0bd 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs @@ -56,22 +56,26 @@ namespace Kyoo.Abstractions.Models /// /// The ID of the user that started watching this episode. /// - [SerializeIgnore] public Guid UserId { get; set; } + [SerializeIgnore] + public Guid UserId { get; set; } /// /// The user that started watching this episode. /// - [SerializeIgnore] public User User { get; set; } + [SerializeIgnore] + public User User { get; set; } /// /// The ID of the movie started. /// - [SerializeIgnore] public Guid MovieId { get; set; } + [SerializeIgnore] + public Guid MovieId { get; set; } /// /// The started. /// - [SerializeIgnore] public Movie Movie { get; set; } + [SerializeIgnore] + public Movie Movie { get; set; } /// public DateTime AddedDate { get; set; } @@ -109,22 +113,26 @@ namespace Kyoo.Abstractions.Models /// /// The ID of the user that started watching this episode. /// - [SerializeIgnore] public Guid UserId { get; set; } + [SerializeIgnore] + public Guid UserId { get; set; } /// /// The user that started watching this episode. /// - [SerializeIgnore] public User User { get; set; } + [SerializeIgnore] + public User User { get; set; } /// /// The ID of the episode started. /// - [SerializeIgnore] public Guid? EpisodeId { get; set; } + [SerializeIgnore] + public Guid? EpisodeId { get; set; } /// /// The started. /// - [SerializeIgnore] public Episode Episode { get; set; } + [SerializeIgnore] + public Episode Episode { get; set; } /// public DateTime AddedDate { get; set; } @@ -162,22 +170,26 @@ namespace Kyoo.Abstractions.Models /// /// The ID of the user that started watching this episode. /// - [SerializeIgnore] public Guid UserId { get; set; } + [SerializeIgnore] + public Guid UserId { get; set; } /// /// The user that started watching this episode. /// - [SerializeIgnore] public User User { get; set; } + [SerializeIgnore] + public User User { get; set; } /// /// The ID of the show started. /// - [SerializeIgnore] public Guid ShowId { get; set; } + [SerializeIgnore] + public Guid ShowId { get; set; } /// /// The started. /// - [SerializeIgnore] public Show Show { get; set; } + [SerializeIgnore] + public Show Show { get; set; } /// public DateTime AddedDate { get; set; } @@ -200,7 +212,8 @@ namespace Kyoo.Abstractions.Models /// /// The ID of the episode started. /// - [SerializeIgnore] public Guid? NextEpisodeId { get; set; } + [SerializeIgnore] + public Guid? NextEpisodeId { get; set; } /// /// The next to watch. diff --git a/back/src/Kyoo.Abstractions/Models/SearchPage.cs b/back/src/Kyoo.Abstractions/Models/SearchPage.cs index 27a29c0d..cf4d858d 100644 --- a/back/src/Kyoo.Abstractions/Models/SearchPage.cs +++ b/back/src/Kyoo.Abstractions/Models/SearchPage.cs @@ -32,7 +32,8 @@ namespace Kyoo.Abstractions.Models string @this, string? previous, string? next, - string first) + string first + ) : base(result.Items, @this, previous, next, first) { Query = result.Query; diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Filter.cs b/back/src/Kyoo.Abstractions/Models/Utils/Filter.cs index 76c6c330..85af8072 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Filter.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Filter.cs @@ -53,24 +53,30 @@ public abstract record Filter { return filters .Where(x => x != null) - .Aggregate((Filter?)null, (acc, filter) => - { - if (acc == null) - return filter; - return new Filter.And(acc, filter!); - }); + .Aggregate( + (Filter?)null, + (acc, filter) => + { + if (acc == null) + return filter; + return new Filter.And(acc, filter!); + } + ); } public static Filter? Or(params Filter?[] filters) { return filters .Where(x => x != null) - .Aggregate((Filter?)null, (acc, filter) => - { - if (acc == null) - return filter; - return new Filter.Or(acc, filter!); - }); + .Aggregate( + (Filter?)null, + (acc, filter) => + { + if (acc == null) + return filter; + return new Filter.Or(acc, filter!); + } + ); } } @@ -109,21 +115,21 @@ public abstract record Filter : Filter public static class FilterParsers { - public static readonly Parser> Filter = - Parse.Ref(() => Bracket) - .Or(Parse.Ref(() => Not)) - .Or(Parse.Ref(() => Eq)) - .Or(Parse.Ref(() => Ne)) - .Or(Parse.Ref(() => Gt)) - .Or(Parse.Ref(() => Ge)) - .Or(Parse.Ref(() => Lt)) - .Or(Parse.Ref(() => Le)) - .Or(Parse.Ref(() => Has)); + public static readonly Parser> Filter = Parse + .Ref(() => Bracket) + .Or(Parse.Ref(() => Not)) + .Or(Parse.Ref(() => Eq)) + .Or(Parse.Ref(() => Ne)) + .Or(Parse.Ref(() => Gt)) + .Or(Parse.Ref(() => Ge)) + .Or(Parse.Ref(() => Lt)) + .Or(Parse.Ref(() => Le)) + .Or(Parse.Ref(() => Has)); - public static readonly Parser> CompleteFilter = - Parse.Ref(() => Or) - .Or(Parse.Ref(() => And)) - .Or(Filter); + public static readonly Parser> CompleteFilter = Parse + .Ref(() => Or) + .Or(Parse.Ref(() => And)) + .Or(Filter); public static readonly Parser> Bracket = from open in Parse.Char('(').Token() @@ -131,22 +137,30 @@ public abstract record Filter : Filter from close in Parse.Char(')').Token() select filter; - public static readonly Parser> AndOperator = Parse.IgnoreCase("and") + public static readonly Parser> AndOperator = Parse + .IgnoreCase("and") .Or(Parse.String("&&")) .Token(); - public static readonly Parser> OrOperator = Parse.IgnoreCase("or") + public static readonly Parser> OrOperator = Parse + .IgnoreCase("or") .Or(Parse.String("||")) .Token(); - public static readonly Parser> And = Parse.ChainOperator(AndOperator, Filter, (_, a, b) => new And(a, b)); + public static readonly Parser> And = Parse.ChainOperator( + AndOperator, + Filter, + (_, a, b) => new And(a, b) + ); - public static readonly Parser> Or = Parse.ChainOperator(OrOperator, And.Or(Filter), (_, a, b) => new Or(a, b)); + public static readonly Parser> Or = Parse.ChainOperator( + OrOperator, + And.Or(Filter), + (_, a, b) => new Or(a, b) + ); public static readonly Parser> Not = - from not in Parse.IgnoreCase("not") - .Or(Parse.String("!")) - .Token() + from not in Parse.IgnoreCase("not").Or(Parse.String("!")).Token() from filter in CompleteFilter select new Not(filter); @@ -155,9 +169,7 @@ public abstract record Filter : Filter Type? nullable = Nullable.GetUnderlyingType(type); if (nullable != null) { - return - from value in _GetValueParser(nullable) - select value; + return from value in _GetValueParser(nullable) select value; } if (type == typeof(int)) @@ -165,8 +177,7 @@ public abstract record Filter : Filter if (type == typeof(float)) { - return - from a in Parse.Number + return from a in Parse.Number from dot in Parse.Char('.') from b in Parse.Number select float.Parse($"{a}.{b}") as object; @@ -174,8 +185,10 @@ public abstract record Filter : Filter if (type == typeof(Guid)) { - return - from guid in Parse.Regex(@"[({]?[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12}[})]?", "Guid") + return from guid in Parse.Regex( + @"[({]?[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12}[})]?", + "Guid" + ) select Guid.Parse(guid) as object; } @@ -191,18 +204,21 @@ public abstract record Filter : Filter if (type.IsEnum) { - return Parse.LetterOrDigit.Many().Text().Then(x => - { - if (Enum.TryParse(type, x, true, out object? value)) - return Parse.Return(value); - return ParseHelper.Error($"Invalid enum value. Unexpected {x}"); - }); + return Parse + .LetterOrDigit + .Many() + .Text() + .Then(x => + { + if (Enum.TryParse(type, x, true, out object? value)) + return Parse.Return(value); + return ParseHelper.Error($"Invalid enum value. Unexpected {x}"); + }); } if (type == typeof(DateTime)) { - return - from year in Parse.Digit.Repeat(4).Text().Select(int.Parse) + return from year in Parse.Digit.Repeat(4).Text().Select(int.Parse) from yd in Parse.Char('-') from mouth in Parse.Digit.Repeat(2).Text().Select(int.Parse) from md in Parse.Char('-') @@ -211,43 +227,57 @@ public abstract record Filter : Filter } if (typeof(IEnumerable).IsAssignableFrom(type)) - return ParseHelper.Error("Can't filter a list with a default comparator, use the 'has' filter."); + return ParseHelper.Error( + "Can't filter a list with a default comparator, use the 'has' filter." + ); return ParseHelper.Error("Unfilterable field found"); } private static Parser> _GetOperationParser( Parser op, Func> apply, - Func>? customTypeParser = null) + Func>? customTypeParser = null + ) { Parser property = Parse.LetterOrDigit.AtLeastOnce().Text(); return property.Then(prop => { - Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; + Type[] types = + typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; if (string.Equals(prop, "kind", StringComparison.OrdinalIgnoreCase)) { - return - from eq in op + return from eq in op from val in types .Select(x => Parse.IgnoreCase(x.Name).Text()) - .Aggregate(null as Parser, (acc, x) => acc == null ? x : Parse.Or(acc, x)) + .Aggregate( + null as Parser, + (acc, x) => acc == null ? x : Parse.Or(acc, x) + ) select apply("kind", val); } 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(); if (propInfo == null) return ParseHelper.Error>($"The given filter '{prop}' is invalid."); - Parser value = customTypeParser != null - ? customTypeParser(propInfo.PropertyType) - : _GetValueParser(propInfo.PropertyType); + Parser value = + customTypeParser != null + ? customTypeParser(propInfo.PropertyType) + : _GetValueParser(propInfo.PropertyType); - return - from eq in op + return from eq in op from val in value select apply(propInfo.Name, val); }); @@ -261,7 +291,10 @@ public abstract record Filter : Filter Type? inner = Nullable.GetUnderlyingType(type); if (inner == null) return _GetValueParser(type); - return Parse.String("null").Token().Return((object?)null) + return Parse + .String("null") + .Token() + .Return((object?)null) .Or(_GetValueParser(inner)); } ); @@ -274,7 +307,10 @@ public abstract record Filter : Filter Type? inner = Nullable.GetUnderlyingType(type); if (inner == null) return _GetValueParser(type); - return Parse.String("null").Token().Return((object?)null) + return Parse + .String("null") + .Token() + .Return((object?)null) .Or(_GetValueParser(inner)); } ); @@ -305,7 +341,9 @@ public abstract record Filter : Filter (Type type) => { 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("Can't use 'has' on a non-list."); } ); @@ -321,7 +359,9 @@ public abstract record Filter : Filter IResult> ret = FilterParsers.CompleteFilter.End().TryParse(filter); if (ret.WasSuccessful) 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) { diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs index 44097fb0..07973867 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -82,9 +82,7 @@ namespace Kyoo.Abstractions.Models.Utils /// public T Match(Func idFunc, Func slugFunc) { - return _id.HasValue - ? idFunc(_id.Value) - : slugFunc(_slug!); + return _id.HasValue ? idFunc(_id.Value) : slugFunc(_slug!); } /// @@ -99,12 +97,19 @@ namespace Kyoo.Abstractions.Models.Utils /// identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug) /// /// - public Filter Matcher(Expression> idGetter, - Expression> slugGetter) + public Filter Matcher( + Expression> idGetter, + Expression> slugGetter + ) { ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug); - BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self); - ICollection parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; + BinaryExpression equal = Expression.Equal( + _id.HasValue ? idGetter.Body : slugGetter.Body, + self + ); + ICollection parameters = _id.HasValue + ? idGetter.Parameters + : slugGetter.Parameters; Expression> lambda = Expression.Lambda>(equal, parameters); return new Filter.Lambda(lambda); } @@ -118,12 +123,19 @@ namespace Kyoo.Abstractions.Models.Utils /// An expression to retrieve a slug from the type . /// The type to match against this identifier. /// An expression to match the type to this identifier. - public Filter Matcher(Expression> idGetter, - Expression> slugGetter) + public Filter Matcher( + Expression> idGetter, + Expression> slugGetter + ) { ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug); - BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self); - ICollection parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; + BinaryExpression equal = Expression.Equal( + _id.HasValue ? idGetter.Body : slugGetter.Body, + self + ); + ICollection parameters = _id.HasValue + ? idGetter.Parameters + : slugGetter.Parameters; Expression> lambda = Expression.Lambda>(equal, parameters); return new Filter.Lambda(lambda); } @@ -137,10 +149,7 @@ namespace Kyoo.Abstractions.Models.Utils /// public bool IsSame(IResource resource) { - return Match( - id => resource.Id == id, - slug => resource.Slug == slug - ); + return Match(id => resource.Id == id, slug => resource.Slug == slug); } /// @@ -161,9 +170,7 @@ namespace Kyoo.Abstractions.Models.Utils private Expression> _IsSameExpression() where T : IResource { - return _id.HasValue - ? x => x.Id == _id.Value - : x => x.Slug == _slug; + return _id.HasValue ? x => x.Id == _id.Value : x => x.Slug == _slug; } /// @@ -181,17 +188,23 @@ namespace Kyoo.Abstractions.Models.Utils .Where(x => x.Name == nameof(Enumerable.Any)) .FirstOrDefault(x => x.GetParameters().Length == 2)! .MakeGenericMethod(typeof(T2)); - MethodCallExpression call = Expression.Call(null, method, listGetter.Body, _IsSameExpression()); - Expression> lambda = Expression.Lambda>(call, listGetter.Parameters); + MethodCallExpression call = Expression.Call( + null, + method, + listGetter.Body, + _IsSameExpression() + ); + Expression> lambda = Expression.Lambda>( + call, + listGetter.Parameters + ); return new Filter.Lambda(lambda); } /// public override string ToString() { - return _id.HasValue - ? _id.Value.ToString() - : _slug!; + return _id.HasValue ? _id.Value.ToString() : _slug!; } /// @@ -208,15 +221,17 @@ namespace Kyoo.Abstractions.Models.Utils } /// - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + public override object ConvertFrom( + ITypeDescriptorContext? context, + CultureInfo? culture, + object value + ) { if (value is Guid id) return new Identifier(id); if (value is not string slug) return base.ConvertFrom(context, culture, value)!; - return Guid.TryParse(slug, out id) - ? new Identifier(id) - : new Identifier(slug); + return Guid.TryParse(slug, out id) ? new Identifier(id) : new Identifier(slug); } } } diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs index 8b902995..8801a869 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs @@ -36,7 +36,8 @@ public class Include 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); } @@ -57,30 +58,49 @@ public class Include : Include public Include(params string[] fields) { Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; - Metadatas = fields.SelectMany(key => - { - var relations = types - .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)!) - .Select(prop => (prop, attr: prop?.GetCustomAttribute()!)) - .Where(x => x.prop != null && x.attr != null) - .ToList(); - if (!relations.Any()) - throw new ValidationException($"No loadable relation with the name {key}."); - return relations - .Select(x => - { - (PropertyInfo prop, LoadableRelationAttribute attr) = x; + Metadatas = fields + .SelectMany(key => + { + var relations = types + .Select( + x => + x.GetProperty( + key, + BindingFlags.IgnoreCase + | BindingFlags.Public + | BindingFlags.Instance + )! + ) + .Select( + prop => (prop, attr: prop?.GetCustomAttribute()!) + ) + .Where(x => x.prop != null && x.attr != null) + .ToList(); + if (!relations.Any()) + throw new ValidationException($"No loadable relation with the name {key}."); + return relations + .Select(x => + { + (PropertyInfo prop, LoadableRelationAttribute attr) = x; - if (attr.RelationID != null) - return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID) as Metadata; - if (attr.Sql != null) - return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!); - if (attr.Projected != null) - return new ProjectedRelation(prop.Name, attr.Projected); - throw new NotImplementedException(); - }) - .Distinct(); - }).ToArray(); + if (attr.RelationID != null) + return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID) + as Metadata; + if (attr.Sql != null) + return new CustomRelation( + prop.Name, + prop.PropertyType, + attr.Sql, + attr.On, + prop.DeclaringType! + ); + if (attr.Projected != null) + return new ProjectedRelation(prop.Name, attr.Projected); + throw new NotImplementedException(); + }) + .Distinct(); + }) + .ToArray(); } public static Include From(string? fields) diff --git a/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs b/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs index 2d55d4ed..beca21d5 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/RequestError.cs @@ -51,7 +51,10 @@ namespace Kyoo.Abstractions.Models.Utils public RequestError(string[] errors) { 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; } } diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs index 91079312..26b4a858 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs @@ -111,12 +111,22 @@ namespace Kyoo.Abstractions.Controllers "desc" => true, "asc" => 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()?.Types ?? new[] { typeof(T) }; + Type[] types = + typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; 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); if (property == null) throw new ValidationException("The given sort key is not valid."); diff --git a/back/src/Kyoo.Abstractions/Module.cs b/back/src/Kyoo.Abstractions/Module.cs index 373fcd8c..e3408dd8 100644 --- a/back/src/Kyoo.Abstractions/Module.cs +++ b/back/src/Kyoo.Abstractions/Module.cs @@ -37,11 +37,15 @@ namespace Kyoo.Abstractions /// If your repository implements a special interface, please use /// /// The initial container. - public static IRegistrationBuilder - RegisterRepository(this ContainerBuilder builder) + public static IRegistrationBuilder< + T, + ConcreteReflectionActivatorData, + SingleRegistrationStyle + > RegisterRepository(this ContainerBuilder builder) where T : IBaseRepository { - return builder.RegisterType() + return builder + .RegisterType() .AsSelf() .As() .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 /// /// The initial container. - public static IRegistrationBuilder - RegisterRepository(this ContainerBuilder builder) + public static IRegistrationBuilder< + T2, + ConcreteReflectionActivatorData, + SingleRegistrationStyle + > RegisterRepository(this ContainerBuilder builder) where T : notnull where T2 : IBaseRepository, T { diff --git a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs b/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs index 05e18f49..f75626c2 100644 --- a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs +++ b/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs @@ -50,8 +50,7 @@ namespace Kyoo.Utils do { yield return enumerator.Current; - } - while (enumerator.MoveNext()); + } while (enumerator.MoveNext()); } return Generator(self, action); diff --git a/back/src/Kyoo.Abstractions/Utility/ExpressionParameterReplacer.cs b/back/src/Kyoo.Abstractions/Utility/ExpressionParameterReplacer.cs index adf7e4bd..9e248457 100644 --- a/back/src/Kyoo.Abstractions/Utility/ExpressionParameterReplacer.cs +++ b/back/src/Kyoo.Abstractions/Utility/ExpressionParameterReplacer.cs @@ -38,13 +38,14 @@ public sealed class ExpressionArgumentReplacer : ExpressionVisitor return base.VisitParameter(node); } - public static Expression ReplaceParams(Expression expression, IEnumerable epxParams, params ParameterExpression[] param) + public static Expression ReplaceParams( + Expression expression, + IEnumerable epxParams, + params ParameterExpression[] param + ) { - ExpressionArgumentReplacer replacer = new( - epxParams - .Zip(param) - .ToDictionary(x => x.First, x => x.Second as Expression) - ); + ExpressionArgumentReplacer replacer = + new(epxParams.Zip(param).ToDictionary(x => x.First, x => x.Second as Expression)); return replacer.Visit(expression); } } diff --git a/back/src/Kyoo.Abstractions/Utility/Merger.cs b/back/src/Kyoo.Abstractions/Utility/Merger.cs index ccbe2b48..36ecab06 100644 --- a/back/src/Kyoo.Abstractions/Utility/Merger.cs +++ b/back/src/Kyoo.Abstractions/Utility/Merger.cs @@ -45,9 +45,11 @@ namespace Kyoo.Utils /// set to those of . /// [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] - public static IDictionary? CompleteDictionaries(IDictionary? first, + public static IDictionary? CompleteDictionaries( + IDictionary? first, IDictionary? second, - out bool hasChanged) + out bool hasChanged + ) { if (first == null) { @@ -58,7 +60,9 @@ namespace Kyoo.Utils hasChanged = false; if (second == null) 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) second.TryAdd(key, value); return second; @@ -83,17 +87,22 @@ namespace Kyoo.Utils /// /// Fields of T will be completed /// - public static T Complete(T first, + public static T Complete( + T first, T? second, - [InstantHandle] Func? where = null) + [InstantHandle] Func? where = null + ) { if (second == null) return first; Type type = typeof(T); IEnumerable properties = type.GetProperties() - .Where(x => x is { CanRead: true, CanWrite: true } - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); + .Where( + x => + x is { CanRead: true, CanWrite: true } + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null + ); if (where != null) properties = properties.Where(where); @@ -107,19 +116,16 @@ namespace Kyoo.Utils if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) { - Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))! + Type[] dictionaryTypes = Utility + .GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))! .GenericTypeArguments; - object?[] parameters = - { - property.GetValue(first), - value, - false - }; + object?[] parameters = { property.GetValue(first), value, false }; object newDictionary = Utility.RunGenericMethod( typeof(Merger), nameof(CompleteDictionaries), dictionaryTypes, - parameters)!; + parameters + )!; if ((bool)parameters[2]!) property.SetValue(first, newDictionary); } diff --git a/back/src/Kyoo.Abstractions/Utility/Utility.cs b/back/src/Kyoo.Abstractions/Utility/Utility.cs index 8ba02d98..0e4fa9a4 100644 --- a/back/src/Kyoo.Abstractions/Utility/Utility.cs +++ b/back/src/Kyoo.Abstractions/Utility/Utility.cs @@ -60,13 +60,17 @@ namespace Kyoo.Utils { case UnicodeCategory.UppercaseLetter: case UnicodeCategory.TitlecaseLetter: - if (previousCategory == UnicodeCategory.SpaceSeparator || - previousCategory == UnicodeCategory.LowercaseLetter || - (previousCategory != UnicodeCategory.DecimalDigitNumber && - previousCategory != null && - currentIndex > 0 && - currentIndex + 1 < name.Length && - char.IsLower(name[currentIndex + 1]))) + if ( + previousCategory == UnicodeCategory.SpaceSeparator + || previousCategory == UnicodeCategory.LowercaseLetter + || ( + previousCategory != UnicodeCategory.DecimalDigitNumber + && previousCategory != null + && currentIndex > 0 + && currentIndex + 1 < name.Length + && char.IsLower(name[currentIndex + 1]) + ) + ) { builder.Append('_'); } @@ -105,7 +109,10 @@ namespace Kyoo.Utils public static bool IsPropertyExpression(LambdaExpression ex) { 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 + ); } /// @@ -118,9 +125,10 @@ namespace Kyoo.Utils { if (!IsPropertyExpression(ex)) throw new ArgumentException($"{ex} is not a property expression."); - MemberExpression? member = ex.Body.NodeType == ExpressionType.Convert - ? ((UnaryExpression)ex.Body).Operand as MemberExpression - : ex.Body as MemberExpression; + MemberExpression? member = + ex.Body.NodeType == ExpressionType.Convert + ? ((UnaryExpression)ex.Body).Operand as MemberExpression + : ex.Body as MemberExpression; return member!.Member.Name; } @@ -175,7 +183,8 @@ namespace Kyoo.Utils IEnumerable types = genericType.IsInterface ? type.GetInterfaces() : type.GetInheritanceTree(); - return types.Prepend(type) + return types + .Prepend(type) .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); } @@ -195,8 +204,11 @@ namespace Kyoo.Utils IEnumerable types = genericType.IsInterface ? type.GetInterfaces() : type.GetInheritanceTree(); - return types.Prepend(type) - .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); + return types + .Prepend(type) + .FirstOrDefault( + x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType + ); } /// @@ -221,11 +233,13 @@ namespace Kyoo.Utils /// No method match the given constraints. /// The method handle of the matching method. [PublicAPI] - public static MethodInfo GetMethod(Type type, + public static MethodInfo GetMethod( + Type type, BindingFlags flag, string name, Type[] generics, - object?[] args) + object?[] args + ) { MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) .Where(x => x.Name == name) @@ -233,9 +247,11 @@ namespace Kyoo.Utils .Where(x => x.GetParameters().Length == args.Length) .IfEmpty(() => { - throw new ArgumentException($"A method named {name} with " + - $"{args.Length} arguments and {generics.Length} generic " + - $"types could not be found on {type.Name}."); + throw new ArgumentException( + $"A method named {name} with " + + $"{args.Length} arguments and {generics.Length} generic " + + $"types could not be found on {type.Name}." + ); }) // TODO this won't work but I don't know why. // .Where(x => @@ -257,7 +273,9 @@ namespace Kyoo.Utils if (methods.Length == 1) 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." + ); } /// @@ -288,7 +306,8 @@ namespace Kyoo.Utils Type owner, string methodName, Type type, - params object[] args) + params object[] args + ) { return RunGenericMethod(owner, methodName, new[] { type }, args); } @@ -323,10 +342,13 @@ namespace Kyoo.Utils Type owner, string methodName, Type[] types, - params object?[] args) + params object?[] args + ) { 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); return (T?)method.MakeGenericMethod(types).Invoke(null, args); } diff --git a/back/src/Kyoo.Authentication/AuthenticationModule.cs b/back/src/Kyoo.Authentication/AuthenticationModule.cs index 8214c101..542356b0 100644 --- a/back/src/Kyoo.Authentication/AuthenticationModule.cs +++ b/back/src/Kyoo.Authentication/AuthenticationModule.cs @@ -61,22 +61,29 @@ namespace Kyoo.Authentication /// public void Configure(IServiceCollection services) { - string secret = _configuration.GetValue("AUTHENTICATION_SECRET", AuthenticationOption.DefaultSecret)!; - PermissionOption permissions = new() - { - Default = _configuration.GetValue("UNLOGGED_PERMISSIONS", "overall.read")!.Split(','), - NewUser = _configuration.GetValue("DEFAULT_PERMISSIONS", "overall.read")!.Split(','), - ApiKeys = _configuration.GetValue("KYOO_APIKEYS", string.Empty)!.Split(','), - }; + string secret = _configuration.GetValue( + "AUTHENTICATION_SECRET", + AuthenticationOption.DefaultSecret + )!; + PermissionOption permissions = + new() + { + Default = _configuration + .GetValue("UNLOGGED_PERMISSIONS", "overall.read")! + .Split(','), + NewUser = _configuration + .GetValue("DEFAULT_PERMISSIONS", "overall.read")! + .Split(','), + ApiKeys = _configuration.GetValue("KYOO_APIKEYS", string.Empty)!.Split(','), + }; services.AddSingleton(permissions); - services.AddSingleton(new AuthenticationOption() - { - Secret = secret, - Permissions = permissions, - }); + services.AddSingleton( + new AuthenticationOption() { Secret = secret, Permissions = permissions, } + ); // 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 => { options.TokenValidationParameters = new TokenValidationParameters @@ -91,9 +98,10 @@ namespace Kyoo.Authentication } /// - public IEnumerable ConfigureSteps => new IStartupAction[] - { - SA.New(app => app.UseAuthentication(), SA.Authentication), - }; + public IEnumerable ConfigureSteps => + new IStartupAction[] + { + SA.New(app => app.UseAuthentication(), SA.Authentication), + }; } } diff --git a/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs b/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs index 8b137e78..850a2f47 100644 --- a/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs +++ b/back/src/Kyoo.Authentication/Controllers/PermissionValidator.cs @@ -57,13 +57,22 @@ namespace Kyoo.Authentication /// 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 + ); } /// 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 + ); } /// @@ -102,7 +111,8 @@ namespace Kyoo.Authentication string permission, Kind kind, Group group, - PermissionOption options) + PermissionOption options + ) { _permission = permission; _kind = kind; @@ -116,7 +126,11 @@ namespace Kyoo.Authentication /// The partial permission to validate. /// The group of the permission. /// The option containing default values. - public PermissionValidatorFilter(object partialInfo, Group? group, PermissionOption options) + public PermissionValidatorFilter( + object partialInfo, + Group? group, + PermissionOption options + ) { switch (partialInfo) { @@ -127,7 +141,9 @@ namespace Kyoo.Authentication _permission = perm; break; 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) @@ -158,13 +174,17 @@ namespace Kyoo.Authentication context.HttpContext.Items["PermissionType"] = permission; return; default: - throw new ArgumentException("Multiple non-matching partial permission attribute " + - "are not supported."); + throw new ArgumentException( + "Multiple non-matching partial permission attribute " + + "are not supported." + ); } if (permission == null || kind == null) { - throw new ArgumentException("The permission type or kind is still missing after two partial " + - "permission attributes, this is unsupported."); + throw new ArgumentException( + "The permission type or kind is still missing after two partial " + + "permission attributes, this is unsupported." + ); } } @@ -178,25 +198,43 @@ namespace Kyoo.Authentication { ICollection permissions = res.Principal.GetPermissions(); 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) { ICollection permissions = _options.Default ?? Array.Empty(); 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) - context.Result = _ErrorResult(res.Failure.Message, StatusCodes.Status403Forbidden); + context.Result = _ErrorResult( + res.Failure.Message, + StatusCodes.Status403Forbidden + ); else - context.Result = _ErrorResult("Authentication panic", StatusCodes.Status500InternalServerError); + context.Result = _ErrorResult( + "Authentication panic", + StatusCodes.Status500InternalServerError + ); } 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(); if (!_options.ApiKeys.Contains(apiKey!)) return AuthenticateResult.Fail("Invalid API-Key."); @@ -205,11 +243,16 @@ namespace Kyoo.Authentication new ClaimsPrincipal( new[] { - new ClaimsIdentity(new[] - { - // TODO: Make permission configurable, for now every APIKEY as all permissions. - new Claim(Claims.Permissions, string.Join(',', PermissionOption.Admin)) - }) + new ClaimsIdentity( + new[] + { + // TODO: Make permission configurable, for now every APIKEY as all permissions. + new Claim( + Claims.Permissions, + string.Join(',', PermissionOption.Admin) + ) + } + ) } ), "apikey" @@ -219,10 +262,14 @@ namespace Kyoo.Authentication private async Task _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. 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; } } diff --git a/back/src/Kyoo.Authentication/Controllers/TokenController.cs b/back/src/Kyoo.Authentication/Controllers/TokenController.cs index 30c1bdcf..7922f45b 100644 --- a/back/src/Kyoo.Authentication/Controllers/TokenController.cs +++ b/back/src/Kyoo.Authentication/Controllers/TokenController.cs @@ -55,23 +55,24 @@ namespace Kyoo.Authentication SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret)); SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature); - string permissions = user.Permissions != null - ? string.Join(',', user.Permissions) - : string.Empty; - List claims = new() - { - new Claim(Claims.Id, user.Id.ToString()), - new Claim(Claims.Name, user.Username), - new Claim(Claims.Permissions, permissions), - new Claim(Claims.Type, "access") - }; + string permissions = + user.Permissions != null ? string.Join(',', user.Permissions) : string.Empty; + List claims = + new() + { + new Claim(Claims.Id, user.Id.ToString()), + new Claim(Claims.Name, user.Username), + new Claim(Claims.Permissions, permissions), + new Claim(Claims.Type, "access") + }; if (user.Email != null) claims.Add(new Claim(Claims.Email, user.Email)); - JwtSecurityToken token = new( - signingCredentials: credential, - claims: claims, - expires: DateTime.UtcNow.Add(expireIn) - ); + JwtSecurityToken token = + new( + signingCredentials: credential, + claims: claims, + expires: DateTime.UtcNow.Add(expireIn) + ); return new JwtSecurityTokenHandler().WriteToken(token); } @@ -80,16 +81,17 @@ namespace Kyoo.Authentication { SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret)); SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature); - JwtSecurityToken token = new( - signingCredentials: credential, - claims: new[] - { - new Claim(Claims.Id, user.Id.ToString()), - new Claim(Claims.Guid, Guid.NewGuid().ToString()), - new Claim(Claims.Type, "refresh") - }, - expires: DateTime.UtcNow.AddYears(1) - ); + JwtSecurityToken token = + new( + signingCredentials: credential, + claims: new[] + { + new Claim(Claims.Id, user.Id.ToString()), + new Claim(Claims.Guid, Guid.NewGuid().ToString()), + new Claim(Claims.Type, "refresh") + }, + expires: DateTime.UtcNow.AddYears(1) + ); // TODO: refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user. return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token)); } @@ -102,14 +104,18 @@ namespace Kyoo.Authentication ClaimsPrincipal principal; try { - principal = tokenHandler.ValidateToken(refreshToken, new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - IssuerSigningKey = key - }, out SecurityToken _); + principal = tokenHandler.ValidateToken( + refreshToken, + new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + IssuerSigningKey = key + }, + out SecurityToken _ + ); } catch (Exception) { @@ -117,7 +123,9 @@ namespace Kyoo.Authentication } 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); if (Guid.TryParse(identifier.Value, out Guid id)) return id; diff --git a/back/src/Kyoo.Authentication/Models/Options/PermissionOption.cs b/back/src/Kyoo.Authentication/Models/Options/PermissionOption.cs index 3c585b9c..dbc2819d 100644 --- a/back/src/Kyoo.Authentication/Models/Options/PermissionOption.cs +++ b/back/src/Kyoo.Authentication/Models/Options/PermissionOption.cs @@ -40,9 +40,12 @@ namespace Kyoo.Authentication.Models get { return Enum.GetNames() - .SelectMany(group => Enum.GetNames() - .Select(kind => $"{group}.{kind}".ToLowerInvariant()) - ).ToArray(); + .SelectMany( + group => + Enum.GetNames() + .Select(kind => $"{group}.{kind}".ToLowerInvariant()) + ) + .ToArray(); } } diff --git a/back/src/Kyoo.Authentication/Views/AuthApi.cs b/back/src/Kyoo.Authentication/Views/AuthApi.cs index 3652306c..3de4f0e0 100644 --- a/back/src/Kyoo.Authentication/Views/AuthApi.cs +++ b/back/src/Kyoo.Authentication/Views/AuthApi.cs @@ -66,7 +66,11 @@ namespace Kyoo.Authentication.Views /// The repository used to check if the user exists. /// The token generator. /// The permission opitons. - public AuthApi(IRepository users, ITokenController token, PermissionOption permissions) + public AuthApi( + IRepository users, + ITokenController token, + PermissionOption permissions + ) { _users = users; _token = token; @@ -97,7 +101,9 @@ namespace Kyoo.Authentication.Views [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> Login([FromBody] LoginRequest request) { - User? user = await _users.GetOrDefault(new Filter.Eq(nameof(Abstractions.Models.User.Username), request.Username)); + User? user = await _users.GetOrDefault( + new Filter.Eq(nameof(Abstractions.Models.User.Username), request.Username) + ); if (user == null || !BCryptNet.Verify(request.Password, user.Password)) return Forbid(new RequestError("The user and password does not match.")); diff --git a/back/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs b/back/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs index c8a96c75..8053dedb 100644 --- a/back/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs +++ b/back/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs @@ -28,11 +28,13 @@ namespace Kyoo.Core.Controllers public class IdentifierRouteConstraint : IRouteConstraint { /// - public bool Match(HttpContext? httpContext, + public bool Match( + HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, - RouteDirection routeDirection) + RouteDirection routeDirection + ) { return values.ContainsKey(routeKey); } diff --git a/back/src/Kyoo.Core/Controllers/LibraryManager.cs b/back/src/Kyoo.Core/Controllers/LibraryManager.cs index db52bd66..e86e4cad 100644 --- a/back/src/Kyoo.Core/Controllers/LibraryManager.cs +++ b/back/src/Kyoo.Core/Controllers/LibraryManager.cs @@ -40,7 +40,8 @@ namespace Kyoo.Core.Controllers IRepository episodeRepository, IRepository peopleRepository, IRepository studioRepository, - IRepository userRepository) + IRepository userRepository + ) { LibraryItems = libraryItemRepository; News = newsRepository; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index 831e2bfb..a3797d7c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -50,7 +50,10 @@ namespace Kyoo.Core.Controllers } /// - public override async Task> Search(string query, Include? include = default) + public override async Task> Search( + string query, + Include? include = default + ) { return await AddIncludes(_database.Collections, include) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%")) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs index f8d54d37..653b1d37 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs @@ -65,22 +65,33 @@ public static class DapperHelper .Where(x => !x.Key.StartsWith('_')) // If first char is lower, assume manual sql instead of reflection. .Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null) - .Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}") + .Select( + x => + $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}" + ) .ToArray(); if (keys.Length == 1) return keys.First(); return $"coalesce({string.Join(", ", keys)})"; } - public static string ProcessSort(Sort sort, bool reverse, Dictionary config, bool recurse = false) + public static string ProcessSort( + Sort sort, + bool reverse, + Dictionary config, + bool recurse = false + ) where T : IQuery { string ret = sort switch { Sort.Default(var value) => ProcessSort(value, reverse, config, true), - Sort.By(string key, bool desc) => $"{Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}", - Sort.Random(var seed) => $"md5('{seed}' || {Property("id", config)}) {(reverse ? "desc" : "asc")}", - Sort.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))), + Sort.By(string key, bool desc) + => $"{Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}", + Sort.Random(var seed) + => $"md5('{seed}' || {Property("id", config)}) {(reverse ? "desc" : "asc")}", + Sort.Conglomerate(var list) + => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))), _ => throw new SwitchExpressionException(), }; if (recurse) @@ -108,10 +119,14 @@ public static class DapperHelper switch (metadata) { case Include.SingleRelation(var name, var type, var rid): - string tableName = type.GetCustomAttribute()?.Name ?? $"{type.Name.ToSnakeCase()}s"; + string tableName = + type.GetCustomAttribute()?.Name + ?? $"{type.Name.ToSnakeCase()}s"; types.Add(type); 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; case Include.CustomRelation(var name, var type, var sql, var on, var declaring): string owner = config.First(x => x.Value == declaring).Key; @@ -133,7 +148,8 @@ public static class DapperHelper T Map(T item, IEnumerable relations) { - IEnumerable metadatas = include.Metadatas + IEnumerable metadatas = include + .Metadatas .Where(x => x is not Include.ProjectedRelation) .Select(x => x.Name); foreach ((string name, object? value) in metadatas.Zip(relations)) @@ -150,15 +166,23 @@ public static class DapperHelper return (projection.ToString(), join.ToString(), types, Map); } - public static FormattableString ProcessFilter(Filter filter, Dictionary config) + public static FormattableString ProcessFilter( + Filter filter, + Dictionary config + ) { FormattableString Format(string key, FormattableString op) { if (key == "kind") { - string cases = string.Join('\n', config - .Skip(1) - .Select(x => $"when {x.Key}.id is not null then '{x.Value.Name.ToLowerInvariant()}'") + string cases = string.Join( + '\n', + config + .Skip(1) + .Select( + x => + $"when {x.Key}.id is not null then '{x.Value.Name.ToLowerInvariant()}'" + ) ); return $""" case @@ -172,7 +196,10 @@ public static class DapperHelper .Where(x => !x.Key.StartsWith('_')) // If first char is lower, assume manual sql instead of reflection. .Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null) - .Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}"); + .Select( + x => + $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}" + ); FormattableString ret = $"{properties.First():raw} {op}"; foreach (string property in properties.Skip(1)) @@ -194,16 +221,20 @@ public static class DapperHelper Filter.And(var first, var second) => $"({Process(first)} and {Process(second)})", Filter.Or(var first, var second) => $"({Process(first)} or {Process(second)})", Filter.Not(var inner) => $"(not {Process(inner)})", - Filter.Eq(var property, var value) when value is null => Format(property, $"is null"), - Filter.Ne(var property, var value) when value is null => Format(property, $"is not null"), + Filter.Eq(var property, var value) when value is null + => Format(property, $"is null"), + Filter.Ne(var property, var value) when value is null + => Format(property, $"is not null"), Filter.Eq(var property, var value) => Format(property, $"= {P(value!)}"), Filter.Ne(var property, var value) => Format(property, $"!= {P(value!)}"), Filter.Gt(var property, var value) => Format(property, $"> {P(value)}"), Filter.Ge(var property, var value) => Format(property, $">= {P(value)}"), Filter.Lt(var property, var value) => Format(property, $"< {P(value)}"), Filter.Le(var property, var value) => Format(property, $"> {P(value)}"), - Filter.Has(var property, var value) => $"{P(value)} = any({Property(property, config):raw})", - Filter.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.Has(var property, var value) + => $"{P(value)} = any({Property(property, config):raw})", + Filter.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.Lambda(var lambda) => throw new NotSupportedException(), _ => throw new NotImplementedException(), }; @@ -213,7 +244,8 @@ public static class DapperHelper public static string ExpendProjections(Type type, string? prefix, Include include) { - IEnumerable projections = include.Metadatas + IEnumerable projections = include + .Metadatas .Select(x => (x as Include.ProjectedRelation)!) .Where(x => x != null) .Where(x => type.GetProperty(x.Name) != null) @@ -231,26 +263,36 @@ public static class DapperHelper Include? include, Filter? filter, Sort? sort, - Pagination? limit) + Pagination? limit + ) where T : class, IResource, IQuery { SqlBuilder query = new(db, command); // Include handling 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); if (!replaced) query.AppendLiteral(includeJoin); query.Replace("/* includes */", $"{includeProjection:raw}", out 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. if (limit?.AfterID != null) { T reference = await get(limit.AfterID.Value); - Filter? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse); + Filter? keysetFilter = RepositoryHelper.KeysetPaginate( + sort, + reference, + !limit.Reverse + ); filter = Filter.And(filter, keysetFilter); } if (filter != null) @@ -273,34 +315,45 @@ public static class DapperHelper List types = config.Select(x => x.Value).Concat(includeTypes).ToList(); // Expand projections on every types received. - sql = Regex.Replace(sql, @"(,?) -- (\w+)( as (\w+))?", (match) => - { - string leadingComa = match.Groups[1].Value; - string type = match.Groups[2].Value; - string? prefix = match.Groups[4].Value; - prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}." : string.Empty; - - Type typeV = types.First(x => x.Name == type); - - // Only project top level items with explicit includes. - string? projection = config.Any(x => x.Value.Name == type) - ? ExpendProjections(typeV, prefix, include) - : null; - - if (typeV.IsAssignableTo(typeof(IThumbnails))) + sql = Regex.Replace( + sql, + @"(,?) -- (\w+)( as (\w+))?", + (match) => { - string posterProj = string.Join(", ", new[] { "poster", "thumbnail", "logo" } - .Select(x => $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash")); - projection = string.IsNullOrEmpty(projection) - ? posterProj - : $"{projection}, {posterProj}"; - types.InsertRange(types.IndexOf(typeV) + 1, Enumerable.Repeat(typeof(Image), 3)); - } + string leadingComa = match.Groups[1].Value; + string type = match.Groups[2].Value; + string? prefix = match.Groups[4].Value; + prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}." : string.Empty; - if (string.IsNullOrEmpty(projection)) - return leadingComa; - return $", {projection}{leadingComa}"; - }); + Type typeV = types.First(x => x.Name == type); + + // Only project top level items with explicit includes. + string? projection = config.Any(x => x.Value.Name == type) + ? ExpendProjections(typeV, prefix, include) + : null; + + if (typeV.IsAssignableTo(typeof(IThumbnails))) + { + string posterProj = string.Join( + ", ", + new[] { "poster", "thumbnail", "logo" }.Select( + x => $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash" + ) + ); + projection = string.IsNullOrEmpty(projection) + ? posterProj + : $"{projection}, {posterProj}"; + types.InsertRange( + types.IndexOf(typeV) + 1, + Enumerable.Repeat(typeof(Image), 3) + ); + } + + if (string.IsNullOrEmpty(projection)) + return leadingComa; + return $", {projection}{leadingComa}"; + } + ); IEnumerable data = await db.QueryAsync( sql, @@ -322,7 +375,10 @@ public static class DapperHelper return mapIncludes(mapper(nItems), nItems.Skip(config.Count)); }, ParametersDictionary.LoadFrom(cmd), - splitOn: string.Join(',', types.Select(x => x.GetCustomAttribute()?.Name ?? "id")) + splitOn: string.Join( + ',', + types.Select(x => x.GetCustomAttribute()?.Name ?? "id") + ) ); if (limit?.Reverse == true) data = data.Reverse(); @@ -339,7 +395,8 @@ public static class DapperHelper Filter? filter, Sort? sort = null, bool reverse = false, - Guid? afterId = default) + Guid? afterId = default + ) where T : class, IResource, IQuery { ICollection ret = await db.Query( @@ -361,7 +418,8 @@ public static class DapperHelper FormattableString command, Dictionary config, SqlVariableContext context, - Filter? filter) + Filter? filter + ) where T : class, IResource { SqlBuilder query = new(db, command); @@ -374,10 +432,7 @@ public static class DapperHelper // language=postgreSQL string sql = $"select count(*) from ({cmd.Sql}) as query"; - return await db.QuerySingleAsync( - sql, - ParametersDictionary.LoadFrom(cmd) - ); + return await db.QuerySingleAsync(sql, ParametersDictionary.LoadFrom(cmd)); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs index 0a7c05a1..46a7eddc 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs @@ -43,7 +43,6 @@ public abstract class DapperRepository : IRepository protected SqlVariableContext Context { get; init; } - public DapperRepository(DbConnection database, SqlVariableContext context) { Database = database; @@ -69,11 +68,13 @@ public abstract class DapperRepository : IRepository } /// - public virtual async Task Get(Filter? filter, + public virtual async Task Get( + Filter? filter, Include? include = default, Sort? sortBy = default, bool reverse = false, - Guid? afterId = default) + Guid? afterId = default + ) { T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId); if (ret == null) @@ -84,7 +85,8 @@ public abstract class DapperRepository : IRepository /// public async Task> FromIds(IList ids, Include? include = null) { - return (await Database.Query( + return ( + await Database.Query( Sql, Config, Mapper, @@ -94,7 +96,8 @@ public abstract class DapperRepository : IRepository Filter.Or(ids.Select(x => new Filter.Eq("id", x)).ToArray()), sort: null, limit: null - )) + ) + ) .OrderBy(x => ids.IndexOf(x.Id)) .ToList(); } @@ -138,11 +141,13 @@ public abstract class DapperRepository : IRepository } /// - public virtual Task GetOrDefault(Filter? filter, + public virtual Task GetOrDefault( + Filter? filter, Include? include = default, Sort? sortBy = default, bool reverse = false, - Guid? afterId = default) + Guid? afterId = default + ) { return Database.QuerySingle( Sql, @@ -158,10 +163,12 @@ public abstract class DapperRepository : IRepository } /// - public Task> GetAll(Filter? filter = default, + public Task> GetAll( + Filter? filter = default, Sort? sort = default, Include? include = default, - Pagination? limit = default) + Pagination? limit = default + ) { return Database.Query( Sql, @@ -179,16 +186,12 @@ public abstract class DapperRepository : IRepository /// public Task GetCount(Filter? filter = null) { - return Database.Count( - Sql, - Config, - Context, - filter - ); + return Database.Count(Sql, Config, Context, filter); } /// - public Task> Search(string query, Include? include = null) => throw new NotImplementedException(); + public Task> Search(string query, Include? include = null) => + throw new NotImplementedException(); /// public Task Create(T obj) => throw new NotImplementedException(); diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index c3b98c33..22b38c58 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -47,8 +47,12 @@ namespace Kyoo.Core.Controllers IRepository.OnEdited += async (show) => { await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope(); - DatabaseContext database = scope.ServiceProvider.GetRequiredService(); - List episodes = await database.Episodes.AsTracking() + DatabaseContext database = scope + .ServiceProvider + .GetRequiredService(); + List episodes = await database + .Episodes + .AsTracking() .Where(x => x.ShowId == show.Id) .ToListAsync(); foreach (Episode ep in episodes) @@ -66,9 +70,11 @@ namespace Kyoo.Core.Controllers /// The database handle to use. /// A show repository /// The thumbnail manager used to store images. - public EpisodeRepository(DatabaseContext database, + public EpisodeRepository( + DatabaseContext database, IRepository shows, - IThumbnailsManager thumbs) + IThumbnailsManager thumbs + ) : base(database, thumbs) { _database = database; @@ -76,7 +82,10 @@ namespace Kyoo.Core.Controllers } /// - public override async Task> Search(string query, Include? include = default) + public override async Task> Search( + string query, + Include? include = default + ) { return await AddIncludes(_database.Episodes, include) .Where(x => EF.Functions.ILike(x.Name!, $"%{query}%")) @@ -87,14 +96,26 @@ namespace Kyoo.Core.Controllers protected override Task GetDuplicated(Episode item) { 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.Episodes.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber); + return _database + .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 + ); } /// public override async Task 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); _database.Entry(obj).State = EntityState.Added; await _database.SaveChangesAsync(() => GetDuplicated(obj)); @@ -110,22 +131,31 @@ namespace Kyoo.Core.Controllers { if (resource.Show == null) { - throw new ArgumentException($"Can't store an episode not related " + - $"to any show (showID: {resource.ShowId})."); + throw new ArgumentException( + $"Can't store an episode not related " + + $"to any show (showID: {resource.ShowId})." + ); } resource.ShowId = resource.Show.Id; } if (resource.SeasonId == null && resource.SeasonNumber != null) { - resource.Season = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == resource.ShowId - && x.SeasonNumber == resource.SeasonNumber); + resource.Season = await _database + .Seasons + .FirstOrDefaultAsync( + x => x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber + ); } } /// 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; await _database.SaveChangesAsync(); await base.Delete(obj); diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index 2990a412..0164660a 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -33,7 +33,8 @@ namespace Kyoo.Core.Controllers public class LibraryItemRepository : DapperRepository { // language=PostgreSQL - protected override FormattableString Sql => $""" + protected override FormattableString Sql => + $""" select s.*, -- Show as s m.*, @@ -58,12 +59,13 @@ namespace Kyoo.Core.Controllers ) as c on false """; - protected override Dictionary Config => new() - { - { "s", typeof(Show) }, - { "m", typeof(Movie) }, - { "c", typeof(Collection) } - }; + protected override Dictionary Config => + new() + { + { "s", typeof(Show) }, + { "m", typeof(Movie) }, + { "c", typeof(Collection) } + }; protected override ILibraryItem Mapper(List items) { @@ -77,15 +79,15 @@ namespace Kyoo.Core.Controllers } public LibraryItemRepository(DbConnection database, SqlVariableContext context) - : base(database, context) - { } + : base(database, context) { } public async Task> GetAllOfCollection( Guid collectionId, Filter? filter = default, Sort? sort = default, Include? include = default, - Pagination? limit = default) + Pagination? limit = default + ) { // language=PostgreSQL FormattableString sql = $""" @@ -111,11 +113,7 @@ namespace Kyoo.Core.Controllers return await Database.Query( sql, - new() - { - { "s", typeof(Show) }, - { "m", typeof(Movie) }, - }, + new() { { "s", typeof(Show) }, { "m", typeof(Movie) }, }, Mapper, (id) => Get(id), Context, diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index 68d03cf5..90ccb35b 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -75,17 +75,18 @@ namespace Kyoo.Core.Controllers { sortBy ??= new Sort.Default(); - IOrderedQueryable _SortBy(IQueryable qr, Expression> sort, bool desc, bool then) + IOrderedQueryable _SortBy( + IQueryable qr, + Expression> sort, + bool desc, + bool then + ) { if (then && qr is IOrderedQueryable qro) { - return desc - ? qro.ThenByDescending(sort) - : qro.ThenBy(sort); + return desc ? qro.ThenByDescending(sort) : qro.ThenBy(sort); } - return desc - ? qr.OrderByDescending(sort) - : qr.OrderBy(sort); + return desc ? qr.OrderByDescending(sort) : qr.OrderBy(sort); } IOrderedQueryable _Sort(IQueryable query, Sort sortBy, bool then) @@ -98,7 +99,12 @@ namespace Kyoo.Core.Controllers return _SortBy(query, x => EF.Property(x, key), desc, then); case Sort.Random(var seed): // 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.Conglomerate(var sorts): IOrderedQueryable nQuery = _Sort(query, sorts.First(), false); foreach (Sort sort in sorts.Skip(1)) @@ -121,11 +127,28 @@ namespace Kyoo.Core.Controllers Expression CmpRandomHandler(string cmp, string seed, Guid refId) { - MethodInfo concat = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!; - Expression id = Expression.Call(Expression.Property(x, "ID"), nameof(Guid.ToString), null); + MethodInfo concat = typeof(string).GetMethod( + 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 left = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, xrng); - Expression right = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, Expression.Constant($"{seed}{refId}")); + Expression left = Expression.Call( + typeof(DatabaseContext), + nameof(DatabaseContext.MD5), + null, + xrng + ); + Expression right = Expression.Call( + typeof(DatabaseContext), + nameof(DatabaseContext.MD5), + null, + Expression.Constant($"{seed}{refId}") + ); return cmp switch { "=" => Expression.Equal(left, right), @@ -138,17 +161,28 @@ namespace Kyoo.Core.Controllers BinaryExpression StringCompatibleExpression( Func operand, string property, - object value) + object value + ) { var left = Expression.Property(x, property); var right = Expression.Constant(value, ((PropertyInfo)left.Member).PropertyType); if (left.Type != typeof(string)) 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)); } - Expression Exp(Func operand, string property, object? value) + Expression Exp( + Func operand, + string property, + object? value + ) { var prop = Expression.Property(x, property); var val = Expression.Constant(value, ((PropertyInfo)prop.Member).PropertyType); @@ -159,18 +193,42 @@ namespace Kyoo.Core.Controllers { return f switch { - Filter.And(var first, var second) => Expression.AndAlso(Parse(first), Parse(second)), - Filter.Or(var first, var second) => Expression.OrElse(Parse(first), Parse(second)), + Filter.And(var first, var second) + => Expression.AndAlso(Parse(first), Parse(second)), + Filter.Or(var first, var second) + => Expression.OrElse(Parse(first), Parse(second)), Filter.Not(var inner) => Expression.Not(Parse(inner)), Filter.Eq(var property, var value) => Exp(Expression.Equal, property, value), - Filter.Ne(var property, var value) => Exp(Expression.NotEqual, property, value), - Filter.Gt(var property, var value) => StringCompatibleExpression(Expression.GreaterThan, property, value), - Filter.Ge(var property, var value) => StringCompatibleExpression(Expression.GreaterThanOrEqual, property, value), - Filter.Lt(var property, var value) => StringCompatibleExpression(Expression.LessThan, property, value), - Filter.Le(var property, var value) => StringCompatibleExpression(Expression.LessThanOrEqual, property, value), - Filter.Has(var property, var value) => Expression.Call(typeof(Enumerable), "Contains", new[] { value.GetType() }, Expression.Property(x, property), Expression.Constant(value)), - Filter.CmpRandom(var op, var seed, var refId) => CmpRandomHandler(op, seed, refId), - Filter.Lambda(var lambda) => ExpressionArgumentReplacer.ReplaceParams(lambda.Body, lambda.Parameters, x), + Filter.Ne(var property, var value) + => Exp(Expression.NotEqual, property, value), + Filter.Gt(var property, var value) + => StringCompatibleExpression(Expression.GreaterThan, property, value), + Filter.Ge(var property, var value) + => StringCompatibleExpression( + Expression.GreaterThanOrEqual, + property, + value + ), + Filter.Lt(var property, var value) + => StringCompatibleExpression(Expression.LessThan, property, value), + Filter.Le(var property, var value) + => StringCompatibleExpression(Expression.LessThanOrEqual, property, value), + Filter.Has(var property, var value) + => Expression.Call( + typeof(Enumerable), + "Contains", + new[] { value.GetType() }, + Expression.Property(x, property), + Expression.Constant(value) + ), + Filter.CmpRandom(var op, var seed, var refId) + => CmpRandomHandler(op, seed, refId), + Filter.Lambda(var lambda) + => ExpressionArgumentReplacer.ReplaceParams( + lambda.Body, + lambda.Parameters, + x + ), _ => throw new NotImplementedException(), }; } @@ -231,7 +289,9 @@ namespace Kyoo.Core.Controllers { T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId); 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; } @@ -243,8 +303,7 @@ namespace Kyoo.Core.Controllers /// public virtual Task GetOrDefault(Guid id, Include? include = default) { - return AddIncludes(Database.Set(), include) - .FirstOrDefaultAsync(x => x.Id == id); + return AddIncludes(Database.Set(), include).FirstOrDefaultAsync(x => x.Id == id); } /// @@ -256,16 +315,17 @@ namespace Kyoo.Core.Controllers .OrderBy(x => EF.Functions.Random()) .FirstOrDefaultAsync(); } - return AddIncludes(Database.Set(), include) - .FirstOrDefaultAsync(x => x.Slug == slug); + return AddIncludes(Database.Set(), include).FirstOrDefaultAsync(x => x.Slug == slug); } /// - public virtual async Task GetOrDefault(Filter? filter, + public virtual async Task GetOrDefault( + Filter? filter, Include? include = default, Sort? sortBy = default, bool reverse = false, - Guid? afterId = default) + Guid? afterId = default + ) { IQueryable query = await ApplyFilters( Database.Set(), @@ -278,13 +338,16 @@ namespace Kyoo.Core.Controllers } /// - public virtual async Task> FromIds(IList ids, Include? include = default) + public virtual async Task> FromIds( + IList ids, + Include? include = default + ) { return ( await AddIncludes(Database.Set(), include) .Where(x => ids.Contains(x.Id)) .ToListAsync() - ) + ) .OrderBy(x => ids.IndexOf(x.Id)) .ToList(); } @@ -293,12 +356,20 @@ namespace Kyoo.Core.Controllers public abstract Task> Search(string query, Include? include = default); /// - public virtual async Task> GetAll(Filter? filter = null, + public virtual async Task> GetAll( + Filter? filter = null, Sort? sort = default, Include? include = default, - Pagination? limit = default) + Pagination? limit = default + ) { - IQueryable query = await ApplyFilters(Database.Set(), filter, sort, limit, include); + IQueryable query = await ApplyFilters( + Database.Set(), + filter, + sort, + limit, + include + ); return await query.ToListAsync(); } @@ -311,11 +382,13 @@ namespace Kyoo.Core.Controllers /// Pagination information (where to start and how many to get) /// Related fields to also load with this query. /// The filtered query - protected async Task> ApplyFilters(IQueryable query, + protected async Task> ApplyFilters( + IQueryable query, Filter? filter = null, Sort? sort = default, Pagination? limit = default, - Include? include = default) + Include? include = default + ) { query = AddIncludes(query, include); query = Sort(query, sort); @@ -324,7 +397,11 @@ namespace Kyoo.Core.Controllers if (limit.AfterID != null) { T reference = await Get(limit.AfterID.Value); - Filter? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse); + Filter? keysetFilter = RepositoryHelper.KeysetPaginate( + sort, + reference, + !limit.Reverse + ); filter = Filter.And(filter, keysetFilter); } if (filter != null) @@ -364,11 +441,14 @@ namespace Kyoo.Core.Controllers throw new DuplicatedItemException(await GetDuplicated(obj)); } 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) - 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) - Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State = EntityState.Added; + Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State = + EntityState.Added; } return obj; } @@ -399,7 +479,11 @@ namespace Kyoo.Core.Controllers { T old = await GetWithTracking(edited.Id); - Merger.Complete(old, edited, x => x.GetCustomAttribute() == null); + Merger.Complete( + old, + edited, + x => x.GetCustomAttribute() == null + ); await EditRelations(old, edited); await Database.SaveChangesAsync(); await IRepository.OnResourceEdited(old); @@ -450,8 +534,10 @@ namespace Kyoo.Core.Controllers { if (resource is IThumbnails thumbs && changed is IThumbnails chng) { - Database.Entry(thumbs).Reference(x => x.Poster).IsModified = thumbs.Poster != chng.Poster; - Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = thumbs.Thumbnail != chng.Thumbnail; + Database.Entry(thumbs).Reference(x => x.Poster).IsModified = + thumbs.Poster != chng.Poster; + Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = + thumbs.Thumbnail != chng.Thumbnail; Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != chng.Logo; } return Validate(resource); @@ -468,7 +554,11 @@ namespace Kyoo.Core.Controllers /// A representing the asynchronous operation. protected virtual Task Validate(T resource) { - if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute() != null) + if ( + typeof(T) + .GetProperty(nameof(resource.Slug))! + .GetCustomAttribute() != null + ) return Task.CompletedTask; if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("Resource can't have null as a slug."); @@ -476,15 +566,21 @@ namespace Kyoo.Core.Controllers { try { - MethodInfo? setter = typeof(T).GetProperty(nameof(resource.Slug))!.GetSetMethod(); + MethodInfo? setter = typeof(T) + .GetProperty(nameof(resource.Slug))! + .GetSetMethod(); if (setter != null) setter.Invoke(resource, new object[] { resource.Slug + '!' }); 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 { - 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; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index d4c0f7c0..52e0f2ee 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -54,10 +54,12 @@ namespace Kyoo.Core.Controllers /// A studio repository /// A people repository /// The thumbnail manager used to store images. - public MovieRepository(DatabaseContext database, + public MovieRepository( + DatabaseContext database, IRepository studios, IRepository people, - IThumbnailsManager thumbs) + IThumbnailsManager thumbs + ) : base(database, thumbs) { _database = database; @@ -66,7 +68,10 @@ namespace Kyoo.Core.Controllers } /// - public override async Task> Search(string query, Include? include = default) + public override async Task> Search( + string query, + Include? include = default + ) { return await AddIncludes(_database.Movies, include) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%")) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs index 8e12f8c3..c642f061 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs @@ -30,7 +30,8 @@ namespace Kyoo.Core.Controllers public class NewsRepository : DapperRepository { // language=PostgreSQL - protected override FormattableString Sql => $""" + protected override FormattableString Sql => + $""" select e.*, -- Episode as e m.* @@ -45,11 +46,8 @@ namespace Kyoo.Core.Controllers ) as m on false """; - protected override Dictionary Config => new() - { - { "e", typeof(Episode) }, - { "m", typeof(Movie) }, - }; + protected override Dictionary Config => + new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, }; protected override INews Mapper(List items) { @@ -61,7 +59,6 @@ namespace Kyoo.Core.Controllers } public NewsRepository(DbConnection database, SqlVariableContext context) - : base(database, context) - { } + : base(database, context) { } } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs index af078fb2..ab143315 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs @@ -50,9 +50,11 @@ namespace Kyoo.Core.Controllers /// The database handle /// A lazy loaded show repository /// The thumbnail manager used to store images. - public PeopleRepository(DatabaseContext database, + public PeopleRepository( + DatabaseContext database, Lazy> shows, - IThumbnailsManager thumbs) + IThumbnailsManager thumbs + ) : base(database, thumbs) { _database = database; @@ -60,7 +62,10 @@ namespace Kyoo.Core.Controllers } /// - public override Task> Search(string query, Include? include = default) + public override Task> Search( + string query, + Include? include = default + ) { throw new NotImplementedException(); // return await AddIncludes(_database.People, include) @@ -88,7 +93,8 @@ namespace Kyoo.Core.Controllers { foreach (PeopleRole role in resource.Roles) { - role.Show = _database.LocalEntity(role.Show!.Slug) + role.Show = + _database.LocalEntity(role.Show!.Slug) ?? await _shows.Value.CreateIfNotExists(role.Show); role.ShowID = role.Show.Id; _database.Entry(role).State = EntityState.Added; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/RepositoryHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/RepositoryHelper.cs index 8637b6b9..cebb0c9d 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/RepositoryHelper.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/RepositoryHelper.cs @@ -59,9 +59,11 @@ public class RepositoryHelper return sort switch { Sort.Default(var value) => GetSortsBy(value), - Sort.By @sortBy => new[] { new SortIndicator(sortBy.Key, sortBy.Desendant, null) }, + Sort.By @sortBy + => new[] { new SortIndicator(sortBy.Key, sortBy.Desendant, null) }, Sort.Conglomerate(var list) => list.SelectMany(GetSortsBy), - Sort.Random(var seed) => new[] { new SortIndicator("random", false, seed.ToString()) }, + Sort.Random(var seed) + => new[] { new SortIndicator("random", false, seed.ToString()) }, _ => Array.Empty(), }; } @@ -88,9 +90,13 @@ public class RepositoryHelper Filter? equals = null; foreach ((string pKey, bool pDesc, string? pSeed) in previousSteps) { - Filter pEquals = pSeed == null - ? new Filter.Eq(pKey, reference.GetType().GetProperty(pKey)?.GetValue(reference)) - : new Filter.CmpRandom("=", pSeed, reference.Id); + Filter pEquals = + pSeed == null + ? new Filter.Eq( + pKey, + reference.GetType().GetProperty(pKey)?.GetValue(reference) + ) + : new Filter.CmpRandom("=", pSeed, reference.Id); equals = Filter.And(equals, pEquals); } @@ -98,14 +104,18 @@ public class RepositoryHelper Func> comparer = greaterThan ? (prop, val) => new Filter.Gt(prop, val) : (prop, val) => new Filter.Lt(prop, val); - Filter last = seed == null - ? comparer(key, value!) - : new Filter.CmpRandom(greaterThan ? ">" : "<", seed, reference.Id); + Filter last = + seed == null + ? comparer(key, value!) + : new Filter.CmpRandom(greaterThan ? ">" : "<", seed, reference.Id); if (key != "random") { - Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; - PropertyInfo property = types.Select(x => x.GetProperty(key)!).First(x => x != null); + Type[] types = + typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; + PropertyInfo property = types + .Select(x => x.GetProperty(key)!) + .First(x => x != null); if (Nullable.GetUnderlyingType(property.PropertyType) != null) last = new Filter.Or(last, new Filter.Eq(key, null)); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index d97af360..78de4095 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -47,8 +47,12 @@ namespace Kyoo.Core.Controllers IRepository.OnEdited += async (show) => { await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope(); - DatabaseContext database = scope.ServiceProvider.GetRequiredService(); - List seasons = await database.Seasons.AsTracking() + DatabaseContext database = scope + .ServiceProvider + .GetRequiredService(); + List seasons = await database + .Seasons + .AsTracking() .Where(x => x.ShowId == show.Id) .ToListAsync(); foreach (Season season in seasons) @@ -65,8 +69,7 @@ namespace Kyoo.Core.Controllers /// /// The database handle that will be used /// The thumbnail manager used to store images. - public SeasonRepository(DatabaseContext database, - IThumbnailsManager thumbs) + public SeasonRepository(DatabaseContext database, IThumbnailsManager thumbs) : base(database, thumbs) { _database = database; @@ -74,11 +77,18 @@ namespace Kyoo.Core.Controllers protected override Task 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 + ); } /// - public override async Task> Search(string query, Include? include = default) + public override async Task> Search( + string query, + Include? include = default + ) { return await AddIncludes(_database.Seasons, include) .Where(x => EF.Functions.ILike(x.Name!, $"%{query}%")) @@ -90,7 +100,8 @@ namespace Kyoo.Core.Controllers public override async Task Create(Season 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}"); _database.Entry(obj).State = EntityState.Added; await _database.SaveChangesAsync(() => GetDuplicated(obj)); @@ -106,8 +117,10 @@ namespace Kyoo.Core.Controllers { if (resource.Show == null) { - throw new ValidationException($"Can't store a season not related to any show " + - $"(showID: {resource.ShowId})."); + throw new ValidationException( + $"Can't store a season not related to any show " + + $"(showID: {resource.ShowId})." + ); } resource.ShowId = resource.Show.Id; } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index 471a1e85..e6e4c3d4 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -55,10 +55,12 @@ namespace Kyoo.Core.Controllers /// A studio repository /// A people repository /// The thumbnail manager used to store images. - public ShowRepository(DatabaseContext database, + public ShowRepository( + DatabaseContext database, IRepository studios, IRepository people, - IThumbnailsManager thumbs) + IThumbnailsManager thumbs + ) : base(database, thumbs) { _database = database; @@ -67,7 +69,10 @@ namespace Kyoo.Core.Controllers } /// - public override async Task> Search(string query, Include? include = default) + public override async Task> Search( + string query, + Include? include = default + ) { return await AddIncludes(_database.Shows, include) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%")) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs index 1448b7cc..743db76d 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs @@ -50,7 +50,10 @@ namespace Kyoo.Core.Controllers } /// - public override async Task> Search(string query, Include? include = default) + public override async Task> Search( + string query, + Include? include = default + ) { return await AddIncludes(_database.Studios, include) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs index bfb51ea7..dc40ed92 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs @@ -49,7 +49,10 @@ namespace Kyoo.Core.Controllers } /// - public override async Task> Search(string query, Include? include = default) + public override async Task> Search( + string query, + Include? include = default + ) { return await AddIncludes(_database.Users, include) .Where(x => EF.Functions.ILike(x.Username, $"%{query}%")) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs index b3c01597..07981dc6 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs @@ -66,7 +66,9 @@ public class WatchStatusRepository : IWatchStatusRepository { await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope(); DatabaseContext db = scope.ServiceProvider.GetRequiredService(); - WatchStatusRepository repo = scope.ServiceProvider.GetRequiredService(); + WatchStatusRepository repo = scope + .ServiceProvider + .GetRequiredService(); List users = await db.ShowWatchStatus .IgnoreQueryFilters() .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 movies, DbConnection db, - SqlVariableContext context) + SqlVariableContext context + ) { _database = database; _movies = movies; @@ -89,7 +93,8 @@ public class WatchStatusRepository : IWatchStatusRepository } // language=PostgreSQL - protected FormattableString Sql => $""" + protected FormattableString Sql => + $""" select s.*, swe.*, -- Episode as swe @@ -126,7 +131,8 @@ public class WatchStatusRepository : IWatchStatusRepository coalesce(s.id, m.id) asc """; - protected Dictionary Config => new() + protected Dictionary Config => + new() { { "s", typeof(Show) }, { "_sw", typeof(ShowWatchStatus) }, @@ -178,10 +184,14 @@ public class WatchStatusRepository : IWatchStatusRepository public async Task> GetAll( Filter? filter = default, Include? include = default, - Pagination? limit = default) + Pagination? limit = default + ) { 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. if (limit?.AfterID != null) @@ -216,7 +226,9 @@ public class WatchStatusRepository : IWatchStatusRepository /// public Task 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); } /// @@ -224,12 +236,14 @@ public class WatchStatusRepository : IWatchStatusRepository Guid movieId, Guid userId, WatchStatus status, - int? watchedTime) + int? watchedTime + ) { Movie movie = await _movies.Get(movieId); - int? percent = watchedTime != null && movie.Runtime > 0 - ? (int)Math.Round(watchedTime.Value / (movie.Runtime * 60f) * 100f) - : null; + int? percent = + watchedTime != null && movie.Runtime > 0 + ? (int)Math.Round(watchedTime.Value / (movie.Runtime * 60f) * 100f) + : null; if (percent < MinWatchPercent) return null; @@ -241,30 +255,34 @@ public class WatchStatusRepository : IWatchStatusRepository } 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() - { - UserId = userId, - MovieId = movieId, - Status = status, - WatchedTime = watchedTime, - WatchedPercent = percent, - AddedDate = DateTime.UtcNow, - PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, - }; - await _database.MovieWatchStatus.Upsert(ret) + MovieWatchStatus ret = + new() + { + UserId = userId, + MovieId = movieId, + Status = status, + WatchedTime = watchedTime, + WatchedPercent = percent, + AddedDate = DateTime.UtcNow, + PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, + }; + await _database + .MovieWatchStatus + .Upsert(ret) .UpdateIf(x => status != Watching || x.Status != Completed) .RunAsync(); return ret; } /// - public async Task DeleteMovieStatus( - Guid movieId, - Guid userId) + public async Task DeleteMovieStatus(Guid movieId, Guid userId) { - await _database.MovieWatchStatus + await _database + .MovieWatchStatus .Where(x => x.MovieId == movieId && x.UserId == userId) .ExecuteDeleteAsync(); } @@ -272,28 +290,34 @@ public class WatchStatusRepository : IWatchStatusRepository /// public Task 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); } /// - public Task SetShowStatus( - Guid showId, - Guid userId, - WatchStatus status - ) => _SetShowStatus(showId, userId, status); + public Task SetShowStatus(Guid showId, Guid userId, WatchStatus status) => + _SetShowStatus(showId, userId, status); private async Task _SetShowStatus( Guid showId, Guid userId, WatchStatus status, - bool newEpisode = false) + bool newEpisode = false + ) { - int unseenEpisodeCount = status != WatchStatus.Completed - ? await _database.Episodes - .Where(x => x.ShowId == showId) - .Where(x => x.Watched!.First(x => x.UserId == userId)!.Status != WatchStatus.Completed) - .CountAsync() - : 0; + int unseenEpisodeCount = + status != WatchStatus.Completed + ? await _database + .Episodes + .Where(x => x.ShowId == showId) + .Where( + x => + x.Watched!.First(x => x.UserId == userId)!.Status + != WatchStatus.Completed + ) + .CountAsync() + : 0; if (unseenEpisodeCount == 0) status = WatchStatus.Completed; @@ -301,79 +325,105 @@ public class WatchStatusRepository : IWatchStatusRepository Guid? nextEpisodeId = null; if (status == WatchStatus.Watching) { - var cursor = await _database.Episodes + var cursor = await _database + .Episodes .IgnoreQueryFilters() .Where(x => x.ShowId == showId) .OrderByDescending(x => x.AbsoluteNumber) .OrderByDescending(x => x.SeasonNumber) .OrderByDescending(x => x.EpisodeNumber) .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; - nextEpisodeId = cursor?.Status.Status == WatchStatus.Watching - ? cursor.Id - : await _database.Episodes - .IgnoreQueryFilters() - .Where(x => x.ShowId == showId) - .OrderByDescending(x => x.AbsoluteNumber) - .OrderByDescending(x => x.SeasonNumber) - .OrderByDescending(x => x.EpisodeNumber) - .Select(x => new { x.Id, Status = x.Watched!.FirstOrDefault(x => x.UserId == userId) }) - .Where(x => x.Status == null || x.Status.Status != WatchStatus.Completed) - .Select(x => x.Id) - .FirstOrDefaultAsync(); + nextEpisodeId = + cursor?.Status.Status == WatchStatus.Watching + ? cursor.Id + : await _database + .Episodes + .IgnoreQueryFilters() + .Where(x => x.ShowId == showId) + .OrderByDescending(x => x.AbsoluteNumber) + .OrderByDescending(x => x.SeasonNumber) + .OrderByDescending(x => x.EpisodeNumber) + .Select( + x => + new + { + x.Id, + Status = x.Watched!.FirstOrDefault(x => x.UserId == userId) + } + ) + .Where(x => x.Status == null || x.Status.Status != WatchStatus.Completed) + .Select(x => x.Id) + .FirstOrDefaultAsync(); } else if (status == WatchStatus.Completed) { - List episodes = await _database.Episodes + List episodes = await _database + .Episodes .Where(x => x.ShowId == showId) .Select(x => x.Id) .ToListAsync(); - await _database.EpisodeWatchStatus - .UpsertRange(episodes.Select(episodeId => new EpisodeWatchStatus - { - UserId = userId, - EpisodeId = episodeId, - Status = WatchStatus.Completed, - AddedDate = DateTime.UtcNow, - PlayedDate = DateTime.UtcNow - })) + await _database + .EpisodeWatchStatus + .UpsertRange( + episodes.Select( + episodeId => + new EpisodeWatchStatus + { + UserId = userId, + EpisodeId = episodeId, + Status = WatchStatus.Completed, + AddedDate = DateTime.UtcNow, + PlayedDate = DateTime.UtcNow + } + ) + ) .UpdateIf(x => x.Status == Watching || x.Status == Planned) .RunAsync(); } - ShowWatchStatus ret = new() - { - UserId = userId, - ShowId = showId, - Status = status, - AddedDate = DateTime.UtcNow, - NextEpisodeId = nextEpisodeId, - WatchedTime = cursorWatchStatus?.Status == WatchStatus.Watching - ? cursorWatchStatus.WatchedTime - : null, - WatchedPercent = cursorWatchStatus?.Status == WatchStatus.Watching - ? cursorWatchStatus.WatchedPercent - : null, - UnseenEpisodesCount = unseenEpisodeCount, - PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, - }; - await _database.ShowWatchStatus.Upsert(ret) + ShowWatchStatus ret = + new() + { + UserId = userId, + ShowId = showId, + Status = status, + AddedDate = DateTime.UtcNow, + NextEpisodeId = nextEpisodeId, + WatchedTime = + cursorWatchStatus?.Status == WatchStatus.Watching + ? cursorWatchStatus.WatchedTime + : null, + WatchedPercent = + cursorWatchStatus?.Status == WatchStatus.Watching + ? cursorWatchStatus.WatchedPercent + : null, + UnseenEpisodesCount = unseenEpisodeCount, + PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, + }; + await _database + .ShowWatchStatus + .Upsert(ret) .UpdateIf(x => status != Watching || x.Status != Completed || newEpisode) .RunAsync(); return ret; } /// - public async Task DeleteShowStatus( - Guid showId, - Guid userId) + public async Task DeleteShowStatus(Guid showId, Guid userId) { - await _database.ShowWatchStatus + await _database + .ShowWatchStatus .IgnoreAutoIncludes() .Where(x => x.ShowId == showId && x.UserId == userId) .ExecuteDeleteAsync(); - await _database.EpisodeWatchStatus + await _database + .EpisodeWatchStatus .Where(x => x.Episode.ShowId == showId && x.UserId == userId) .ExecuteDeleteAsync(); } @@ -381,7 +431,9 @@ public class WatchStatusRepository : IWatchStatusRepository /// public Task 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); } /// @@ -389,12 +441,14 @@ public class WatchStatusRepository : IWatchStatusRepository Guid episodeId, Guid userId, WatchStatus status, - int? watchedTime) + int? watchedTime + ) { Episode episode = await _database.Episodes.FirstAsync(x => x.Id == episodeId); - int? percent = watchedTime != null && episode.Runtime > 0 - ? (int)Math.Round(watchedTime.Value / (episode.Runtime * 60f) * 100f) - : null; + int? percent = + watchedTime != null && episode.Runtime > 0 + ? (int)Math.Round(watchedTime.Value / (episode.Runtime * 60f) * 100f) + : null; if (percent < MinWatchPercent) return null; @@ -406,19 +460,24 @@ public class WatchStatusRepository : IWatchStatusRepository } 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() - { - UserId = userId, - EpisodeId = episodeId, - Status = status, - WatchedTime = watchedTime, - WatchedPercent = percent, - AddedDate = DateTime.UtcNow, - PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, - }; - await _database.EpisodeWatchStatus.Upsert(ret) + EpisodeWatchStatus ret = + new() + { + UserId = userId, + EpisodeId = episodeId, + Status = status, + WatchedTime = watchedTime, + WatchedPercent = percent, + AddedDate = DateTime.UtcNow, + PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, + }; + await _database + .EpisodeWatchStatus + .Upsert(ret) .UpdateIf(x => status != Watching || x.Status != Completed) .RunAsync(); await SetShowStatus(episode.ShowId, userId, WatchStatus.Watching); @@ -426,11 +485,10 @@ public class WatchStatusRepository : IWatchStatusRepository } /// - public async Task DeleteEpisodeStatus( - Guid episodeId, - Guid userId) + public async Task DeleteEpisodeStatus(Guid episodeId, Guid userId) { - await _database.EpisodeWatchStatus + await _database + .EpisodeWatchStatus .Where(x => x.EpisodeId == episodeId && x.UserId == userId) .ExecuteDeleteAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index 8e7339de..8a0697d4 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -36,7 +36,8 @@ namespace Kyoo.Core.Controllers /// public class ThumbnailsManager : IThumbnailsManager { - private static readonly Dictionary> _downloading = new(); + private static readonly Dictionary> _downloading = + new(); private readonly ILogger _logger; @@ -47,8 +48,10 @@ namespace Kyoo.Core.Controllers /// /// Client factory /// A logger to report errors - public ThumbnailsManager(IHttpClientFactory clientFactory, - ILogger logger) + public ThumbnailsManager( + IHttpClientFactory clientFactory, + ILogger logger + ) { _clientFactory = clientFactory; _logger = logger; @@ -85,14 +88,35 @@ namespace Kyoo.Core.Controllers info.ColorType = SKColorType.Rgba8888; using SKBitmap original = SKBitmap.Decode(codec, info); - using SKBitmap high = original.Resize(new SKSizeI(original.Width, original.Height), SKFilterQuality.High); - await _WriteTo(original, $"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp", 90); + using SKBitmap high = original.Resize( + 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); - await _WriteTo(medium, $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp", 75); + using SKBitmap medium = high.Resize( + new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)), + SKFilterQuality.Medium + ); + await _WriteTo( + medium, + $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp", + 75 + ); - using SKBitmap low = medium.Resize(new SKSizeI(original.Width / 2, original.Height / 2), SKFilterQuality.Low); - await _WriteTo(low, $"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp", 50); + using SKBitmap low = medium.Resize( + 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); } @@ -108,7 +132,8 @@ namespace Kyoo.Core.Controllers { 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; TaskCompletionSource? sync = null; try @@ -128,15 +153,25 @@ namespace Kyoo.Core.Controllers } if (duplicated) { - object? dup = sync != null - ? await sync.Task - : null; + object? dup = sync != null ? await sync.Task : null; throw new DuplicatedItemException(dup); } - await _DownloadImage(item.Poster, _GetBaseImagePath(item, "poster"), $"The poster of {name}"); - await _DownloadImage(item.Thumbnail, _GetBaseImagePath(item, "thumbnail"), $"The poster of {name}"); - await _DownloadImage(item.Logo, _GetBaseImagePath(item, "logo"), $"The poster of {name}"); + await _DownloadImage( + item.Poster, + _GetBaseImagePath(item, "poster"), + $"The poster of {name}" + ); + await _DownloadImage( + item.Thumbnail, + _GetBaseImagePath(item, "thumbnail"), + $"The poster of {name}" + ); + await _DownloadImage( + item.Logo, + _GetBaseImagePath(item, "logo"), + $"The poster of {name}" + ); } finally { @@ -155,7 +190,8 @@ namespace Kyoo.Core.Controllers { 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()) }; Directory.CreateDirectory(directory); @@ -175,12 +211,14 @@ namespace Kyoo.Core.Controllers { IEnumerable images = new[] { "poster", "thumbnail", "logo" } .SelectMany(x => _GetBaseImagePath(item, x)) - .SelectMany(x => new[] - { - ImageQuality.High.ToString().ToLowerInvariant(), - ImageQuality.Medium.ToString().ToLowerInvariant(), - ImageQuality.Low.ToString().ToLowerInvariant(), - }.Select(quality => $"{x}.{quality}.webp") + .SelectMany( + x => + new[] + { + ImageQuality.High.ToString().ToLowerInvariant(), + ImageQuality.Medium.ToString().ToLowerInvariant(), + ImageQuality.Low.ToString().ToLowerInvariant(), + }.Select(quality => $"{x}.{quality}.webp") ); foreach (string image in images) diff --git a/back/src/Kyoo.Core/CoreModule.cs b/back/src/Kyoo.Core/CoreModule.cs index 0339c6ac..5d60f7f0 100644 --- a/back/src/Kyoo.Core/CoreModule.cs +++ b/back/src/Kyoo.Core/CoreModule.cs @@ -54,7 +54,10 @@ namespace Kyoo.Core /// public void Configure(ContainerBuilder builder) { - builder.RegisterType().As().InstancePerLifetimeScope(); + builder + .RegisterType() + .As() + .InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterRepository(); @@ -67,7 +70,11 @@ namespace Kyoo.Core builder.RegisterRepository(); builder.RegisterRepository(); builder.RegisterRepository(); - builder.RegisterType().As().AsSelf().InstancePerLifetimeScope(); + builder + .RegisterType() + .As() + .AsSelf() + .InstancePerLifetimeScope(); builder.RegisterType().InstancePerLifetimeScope(); } @@ -77,7 +84,8 @@ namespace Kyoo.Core services.AddHttpContextAccessor(); services.AddTransient, JsonOptions>(); - services.AddMvcCore(options => + services + .AddMvcCore(options => { options.Filters.Add(); options.ModelBinderProviders.Insert(0, new SortBinder.Provider()); @@ -120,12 +128,16 @@ namespace Kyoo.Core } /// - public IEnumerable ConfigureSteps => new IStartupAction[] - { - SA.New(app => app.UseHsts(), SA.Before), - SA.New(app => app.UseResponseCompression(), SA.Routing + 1), - SA.New(app => app.UseRouting(), SA.Routing), - SA.New(app => app.UseEndpoints(x => x.MapControllers()), SA.Endpoint) - }; + public IEnumerable ConfigureSteps => + new IStartupAction[] + { + SA.New(app => app.UseHsts(), SA.Before), + SA.New(app => app.UseResponseCompression(), SA.Routing + 1), + SA.New(app => app.UseRouting(), SA.Routing), + SA.New( + app => app.UseEndpoints(x => x.MapControllers()), + SA.Endpoint + ) + }; } } diff --git a/back/src/Kyoo.Core/ExceptionFilter.cs b/back/src/Kyoo.Core/ExceptionFilter.cs index 66b767a1..98101283 100644 --- a/back/src/Kyoo.Core/ExceptionFilter.cs +++ b/back/src/Kyoo.Core/ExceptionFilter.cs @@ -66,7 +66,9 @@ namespace Kyoo.Core break; case Exception ex: _logger.LogError(ex, "Unhandled error"); - context.Result = new ServerErrorObjectResult(new RequestError("Internal Server Error")); + context.Result = new ServerErrorObjectResult( + new RequestError("Internal Server Error") + ); break; } } diff --git a/back/src/Kyoo.Core/Views/Helper/BaseApi.cs b/back/src/Kyoo.Core/Views/Helper/BaseApi.cs index 289ca809..25dad197 100644 --- a/back/src/Kyoo.Core/Views/Helper/BaseApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/BaseApi.cs @@ -45,11 +45,13 @@ namespace Kyoo.Core.Api protected Page Page(ICollection resources, int limit) where TResult : IResource { - Dictionary query = Request.Query.ToDictionary( - x => x.Key, - x => x.Value.ToString(), - StringComparer.InvariantCultureIgnoreCase - ); + Dictionary query = Request + .Query + .ToDictionary( + x => x.Key, + x => x.Value.ToString(), + StringComparer.InvariantCultureIgnoreCase + ); // If the query was sorted randomly, add the seed to the url to get reproducible links (next,prev,first...) if (query.ContainsKey("sortBy")) @@ -58,28 +60,27 @@ namespace Kyoo.Core.Api query["sortBy"] = Regex.Replace(query["sortBy"], "random(?!:)", $"random:{seed}"); } - return new Page( - resources, - Request.Path, - query, - limit - ); + return new Page(resources, Request.Path, query, limit); } protected SearchPage SearchPage(SearchPage.SearchResult result) where TResult : IResource { - Dictionary query = Request.Query.ToDictionary( - x => x.Key, - x => x.Value.ToString(), - StringComparer.InvariantCultureIgnoreCase - ); + Dictionary query = Request + .Query + .ToDictionary( + x => x.Key, + x => x.Value.ToString(), + StringComparer.InvariantCultureIgnoreCase + ); string self = Request.Path + query.ToQueryString(); string? previous = null; string? next = null; 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; if (skip != null) @@ -97,13 +98,7 @@ namespace Kyoo.Core.Api query.Remove("skip"); first = Request.Path + query.ToQueryString(); - return new SearchPage( - result, - self, - previous, - next, - first - ); + return new SearchPage(result, self, previous, next, first); } } } diff --git a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs index 18330046..b1679313 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -67,7 +67,10 @@ namespace Kyoo.Core.Api [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get(Identifier identifier, [FromQuery] Include? fields) + public async Task> Get( + Identifier identifier, + [FromQuery] Include? fields + ) { T? ret = await identifier.Match( id => Repository.GetOrDefault(id, fields), @@ -116,14 +119,10 @@ namespace Kyoo.Core.Api [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include? fields) + [FromQuery] Include? fields + ) { - ICollection resources = await Repository.GetAll( - filter, - sortBy, - fields, - pagination - ); + ICollection resources = await Repository.GetAll(filter, sortBy, fields, pagination); return Page(resources, pagination.Limit); } @@ -195,7 +194,9 @@ namespace Kyoo.Core.Api if (resource.Id.HasValue) return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync); if (resource.Slug == null) - throw new ArgumentException("Either the Id or the slug of the resource has to be defined to edit it."); + 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); return await Repository.Patch(old.Id, TryUpdateModelAsync); @@ -216,10 +217,7 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Delete(Identifier identifier) { - await identifier.Match( - id => Repository.Delete(id), - slug => Repository.Delete(slug) - ); + await identifier.Match(id => Repository.Delete(id), slug => Repository.Delete(slug)); return NoContent(); } @@ -239,7 +237,9 @@ namespace Kyoo.Core.Api public async Task Delete([FromQuery] Filter filter) { 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); return NoContent(); diff --git a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 96a76561..190a05ab 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -49,14 +49,17 @@ namespace Kyoo.Core.Api /// The repository to use as a baking store for the type . /// /// The thumbnail manager used to retrieve images paths. - public CrudThumbsApi(IRepository repository, - IThumbnailsManager thumbs) + public CrudThumbsApi(IRepository repository, IThumbnailsManager thumbs) : base(repository) { _thumbs = thumbs; } - private async Task _GetImage(Identifier identifier, string image, ImageQuality? quality) + private async Task _GetImage( + Identifier identifier, + string image, + ImageQuality? quality + ) { T? resource = await identifier.Match( id => Repository.GetOrDefault(id), @@ -94,7 +97,10 @@ namespace Kyoo.Core.Api [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public Task GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality) + public Task GetPoster( + Identifier identifier, + [FromQuery] ImageQuality? quality + ) { return _GetImage(identifier, "poster", quality); } @@ -134,7 +140,10 @@ namespace Kyoo.Core.Api /// [HttpGet("{identifier:id}/thumbnail")] [HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)] - public Task GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality) + public Task GetBackdrop( + Identifier identifier, + [FromQuery] ImageQuality? quality + ) { return _GetImage(identifier, "thumbnail", quality); } diff --git a/back/src/Kyoo.Core/Views/Helper/FilterBinder.cs b/back/src/Kyoo.Core/Views/Helper/FilterBinder.cs index f740ec01..e4eac372 100644 --- a/back/src/Kyoo.Core/Views/Helper/FilterBinder.cs +++ b/back/src/Kyoo.Core/Views/Helper/FilterBinder.cs @@ -28,10 +28,14 @@ public class FilterBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { - ValueProviderResult fields = bindingContext.ValueProvider.GetValue(bindingContext.FieldName); + ValueProviderResult fields = bindingContext + .ValueProvider + .GetValue(bindingContext.FieldName); try { - object? filter = bindingContext.ModelType.GetMethod(nameof(Filter.From))! + object? filter = bindingContext + .ModelType + .GetMethod(nameof(Filter.From))! .Invoke(null, new object?[] { fields.FirstValue }); bindingContext.Result = ModelBindingResult.Success(filter); return Task.CompletedTask; diff --git a/back/src/Kyoo.Core/Views/Helper/IncludeBinder.cs b/back/src/Kyoo.Core/Views/Helper/IncludeBinder.cs index 4ce25b97..c3a9aff8 100644 --- a/back/src/Kyoo.Core/Views/Helper/IncludeBinder.cs +++ b/back/src/Kyoo.Core/Views/Helper/IncludeBinder.cs @@ -31,10 +31,14 @@ public class IncludeBinder : IModelBinder public Task BindModelAsync(ModelBindingContext bindingContext) { - ValueProviderResult fields = bindingContext.ValueProvider.GetValue(bindingContext.FieldName); + ValueProviderResult fields = bindingContext + .ValueProvider + .GetValue(bindingContext.FieldName); try { - object include = bindingContext.ModelType.GetMethod(nameof(Include.From))! + object include = bindingContext + .ModelType + .GetMethod(nameof(Include.From))! .Invoke(null, new object?[] { fields.FirstValue })!; bindingContext.Result = ModelBindingResult.Success(include); bindingContext.HttpContext.Items["fields"] = ((dynamic)include).Fields; diff --git a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs index c272e942..5da2d274 100644 --- a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs +++ b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs @@ -47,7 +47,9 @@ namespace Kyoo.Core.Api /// public void Configure(MvcNewtonsoftJsonOptions options) { - options.SerializerSettings.ContractResolver = new JsonSerializerContract(_httpContextAccessor); + options.SerializerSettings.ContractResolver = new JsonSerializerContract( + _httpContextAccessor + ); options.SerializerSettings.Converters.Add(new PeopleRoleConverter()); } } diff --git a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index eca83dc1..0aad78b4 100644 --- a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -51,16 +51,23 @@ namespace Kyoo.Core.Api } /// - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + protected override JsonProperty CreateProperty( + MemberInfo member, + MemberSerialization memberSerialization + ) { JsonProperty property = base.CreateProperty(member, memberSerialization); - LoadableRelationAttribute? relation = member.GetCustomAttribute(); + LoadableRelationAttribute? relation = + member.GetCustomAttribute(); if (relation != null) { property.ShouldSerialize = _ => { - if (_httpContextAccessor.HttpContext!.Items["fields"] is not ICollection fields) + if ( + _httpContextAccessor.HttpContext!.Items["fields"] + is not ICollection fields + ) return false; return fields.Contains(member.Name); }; @@ -73,23 +80,31 @@ namespace Kyoo.Core.Api return property; } - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + protected override IList CreateProperties( + Type type, + MemberSerialization memberSerialization + ) { IList 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() - { - DeclaringType = type, - PropertyName = "kind", - UnderlyingName = "kind", - PropertyType = typeof(string), - ValueProvider = new FixedValueProvider(type.Name), - Readable = true, - Writable = false, - TypeNameHandling = TypeNameHandling.None, - }); + properties.Add( + new JsonProperty() + { + DeclaringType = type, + PropertyName = "kind", + UnderlyingName = "kind", + PropertyType = typeof(string), + ValueProvider = new FixedValueProvider(type.Name), + Readable = true, + Writable = false, + TypeNameHandling = TypeNameHandling.None, + } + ); } return properties; @@ -104,11 +119,10 @@ namespace Kyoo.Core.Api _value = value; } - public object GetValue(object target) - => _value; + public object GetValue(object target) => _value; - public void SetValue(object target, object? value) - => throw new NotImplementedException(); + public void SetValue(object target, object? value) => + throw new NotImplementedException(); } } } diff --git a/back/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs b/back/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs index 8e4aa173..773a8d9b 100644 --- a/back/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs +++ b/back/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs @@ -31,7 +31,11 @@ namespace Kyoo.Core.Api public class PeopleRoleConverter : JsonConverter { /// - public override void WriteJson(JsonWriter writer, PeopleRole? value, JsonSerializer serializer) + public override void WriteJson( + JsonWriter writer, + PeopleRole? value, + JsonSerializer serializer + ) { // if (value == null) // { @@ -58,11 +62,13 @@ namespace Kyoo.Core.Api } /// - public override PeopleRole ReadJson(JsonReader reader, + public override PeopleRole ReadJson( + JsonReader reader, Type objectType, PeopleRole? existingValue, bool hasExistingValue, - JsonSerializer serializer) + JsonSerializer serializer + ) { throw new NotImplementedException(); } diff --git a/back/src/Kyoo.Core/Views/Helper/SortBinder.cs b/back/src/Kyoo.Core/Views/Helper/SortBinder.cs index 59b70884..b46f26d5 100644 --- a/back/src/Kyoo.Core/Views/Helper/SortBinder.cs +++ b/back/src/Kyoo.Core/Views/Helper/SortBinder.cs @@ -32,14 +32,18 @@ public class SortBinder : IModelBinder public Task BindModelAsync(ModelBindingContext bindingContext) { - ValueProviderResult sortBy = bindingContext.ValueProvider.GetValue(bindingContext.FieldName); + ValueProviderResult sortBy = bindingContext + .ValueProvider + .GetValue(bindingContext.FieldName); uint seed = BitConverter.ToUInt32( BitConverter.GetBytes(_rng.Next(int.MinValue, int.MaxValue)), 0 ); try { - object sort = bindingContext.ModelType.GetMethod(nameof(Sort.From))! + object sort = bindingContext + .ModelType + .GetMethod(nameof(Sort.From))! .Invoke(null, new object?[] { sortBy.FirstValue, seed })!; bindingContext.Result = ModelBindingResult.Success(sort); bindingContext.HttpContext.Items["seed"] = seed; diff --git a/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs b/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs index bfbce70d..7cbf0cfc 100644 --- a/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs +++ b/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs @@ -47,8 +47,7 @@ namespace Kyoo.Core.Api /// The library manager used to modify or retrieve information about the data store. /// /// The thumbnail manager used to retrieve images paths. - public StaffApi(ILibraryManager libraryManager, - IThumbnailsManager thumbs) + public StaffApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) : base(libraryManager.People, thumbs) { _libraryManager = libraryManager; diff --git a/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs b/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs index 362d24ba..53f11d56 100644 --- a/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs +++ b/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs @@ -77,20 +77,30 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetShows(Identifier identifier, + public async Task>> GetShows( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { - ICollection resources = await _libraryManager.Shows.GetAll( - Filter.And(filter, identifier.Matcher(x => x.StudioId, x => x.Studio!.Slug)), - sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Shows + .GetAll( + Filter.And( + filter, + identifier.Matcher(x => x.StudioId, x => x.Studio!.Slug) + ), + sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Studios.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Studios.GetOrDefault(identifier.IsSame()) == null + ) return NotFound(); return Page(resources, pagination.Limit); } diff --git a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs index 4d4644ca..be494cea 100644 --- a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -46,10 +46,12 @@ namespace Kyoo.Core.Api private readonly CollectionRepository _collections; private readonly LibraryItemRepository _items; - public CollectionApi(ILibraryManager libraryManager, + public CollectionApi( + ILibraryManager libraryManager, CollectionRepository collections, LibraryItemRepository items, - IThumbnailsManager thumbs) + IThumbnailsManager thumbs + ) : base(libraryManager.Collections, thumbs) { _libraryManager = libraryManager; @@ -139,11 +141,13 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetItems(Identifier identifier, + public async Task>> GetItems( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include? fields) + [FromQuery] Include? fields + ) { Guid collectionId = await identifier.Match( id => Task.FromResult(id), @@ -152,12 +156,18 @@ namespace Kyoo.Core.Api ICollection resources = await _items.GetAllOfCollection( collectionId, filter, - sortBy == new Sort.Default() ? new Sort.By(nameof(Movie.AirDate)) : sortBy, + sortBy == new Sort.Default() + ? new Sort.By(nameof(Movie.AirDate)) + : sortBy, fields, pagination ); - if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) + == null + ) return NotFound(); return Page(resources, pagination.Limit); } @@ -182,20 +192,31 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetShows(Identifier identifier, + public async Task>> GetShows( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include? fields) + [FromQuery] Include? fields + ) { - ICollection resources = await _libraryManager.Shows.GetAll( - Filter.And(filter, identifier.IsContainedIn(x => x.Collections)), - sortBy == new Sort.Default() ? new Sort.By(x => x.AirDate) : sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Shows + .GetAll( + Filter.And( + filter, + identifier.IsContainedIn(x => x.Collections) + ), + sortBy == new Sort.Default() ? new Sort.By(x => x.AirDate) : sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) + == null + ) return NotFound(); return Page(resources, pagination.Limit); } @@ -220,20 +241,33 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetMovies(Identifier identifier, + public async Task>> GetMovies( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include? fields) + [FromQuery] Include? fields + ) { - ICollection resources = await _libraryManager.Movies.GetAll( - Filter.And(filter, identifier.IsContainedIn(x => x.Collections)), - sortBy == new Sort.Default() ? new Sort.By(x => x.AirDate) : sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Movies + .GetAll( + Filter.And( + filter, + identifier.IsContainedIn(x => x.Collections) + ), + sortBy == new Sort.Default() + ? new Sort.By(x => x.AirDate) + : sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) + == null + ) return NotFound(); return Page(resources, pagination.Limit); } diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs index ceb39f6d..d2a73852 100644 --- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -52,8 +52,7 @@ namespace Kyoo.Core.Api /// The library manager used to modify or retrieve information in the data store. /// /// The thumbnail manager used to retrieve images paths. - public EpisodeApi(ILibraryManager libraryManager, - IThumbnailsManager thumbnails) + public EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails) : base(libraryManager.Episodes, thumbnails) { _libraryManager = libraryManager; @@ -73,9 +72,14 @@ namespace Kyoo.Core.Api [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetShow(Identifier identifier, [FromQuery] Include fields) + public async Task> GetShow( + Identifier identifier, + [FromQuery] Include fields + ) { - return await _libraryManager.Shows.Get(identifier.IsContainedIn(x => x.Episodes!), fields); + return await _libraryManager + .Shows + .Get(identifier.IsContainedIn(x => x.Episodes!), fields); } /// @@ -94,21 +98,21 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetSeason(Identifier identifier, [FromQuery] Include fields) + public async Task> GetSeason( + Identifier identifier, + [FromQuery] Include fields + ) { - Season? ret = await _libraryManager.Seasons.GetOrDefault( - identifier.IsContainedIn(x => x.Episodes!), - fields - ); + Season? ret = await _libraryManager + .Seasons + .GetOrDefault(identifier.IsContainedIn(x => x.Episodes!), fields); if (ret != null) return ret; Episode? episode = await identifier.Match( id => _libraryManager.Episodes.GetOrDefault(id), slug => _libraryManager.Episodes.GetOrDefault(slug) ); - return episode == null - ? NotFound() - : NoContent(); + return episode == null ? NotFound() : NoContent(); } /// @@ -154,18 +158,19 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task SetWatchStatus(Identifier identifier, WatchStatus status, int? watchedTime) + public async Task SetWatchStatus( + Identifier identifier, + WatchStatus status, + int? watchedTime + ) { Guid id = await identifier.Match( id => Task.FromResult(id), async slug => (await _libraryManager.Episodes.Get(slug)).Id ); - return await _libraryManager.WatchStatus.SetEpisodeStatus( - id, - User.GetIdOrThrow(), - status, - watchedTime - ); + return await _libraryManager + .WatchStatus + .SetEpisodeStatus(id, User.GetIdOrThrow(), status, watchedTime); } /// diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs index 52a117c2..db604836 100644 --- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -54,8 +54,7 @@ namespace Kyoo.Core.Api /// The library manager used to modify or retrieve information about the data store. /// /// The thumbnail manager used to retrieve images paths. - public MovieApi(ILibraryManager libraryManager, - IThumbnailsManager thumbs) + public MovieApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) : base(libraryManager.Movies, thumbs) { _libraryManager = libraryManager; @@ -109,9 +108,14 @@ namespace Kyoo.Core.Api [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetStudio(Identifier identifier, [FromQuery] Include fields) + public async Task> GetStudio( + Identifier identifier, + [FromQuery] Include fields + ) { - return await _libraryManager.Studios.Get(identifier.IsContainedIn(x => x.Movies!), fields); + return await _libraryManager + .Studios + .Get(identifier.IsContainedIn(x => x.Movies!), fields); } /// @@ -134,20 +138,27 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetCollections(Identifier identifier, + public async Task>> GetCollections( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { - ICollection resources = await _libraryManager.Collections.GetAll( - Filter.And(filter, identifier.IsContainedIn(x => x.Movies)), - sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Collections + .GetAll( + Filter.And(filter, identifier.IsContainedIn(x => x.Movies)), + sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Movies.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Movies.GetOrDefault(identifier.IsSame()) == null + ) return NotFound(); return Page(resources, pagination.Limit); } @@ -196,18 +207,19 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task SetWatchStatus(Identifier identifier, WatchStatus status, int? watchedTime) + public async Task SetWatchStatus( + Identifier identifier, + WatchStatus status, + int? watchedTime + ) { Guid id = await identifier.Match( id => Task.FromResult(id), async slug => (await _libraryManager.Movies.Get(slug)).Id ); - return await _libraryManager.WatchStatus.SetMovieStatus( - id, - User.GetIdOrThrow(), - status, - watchedTime - ); + return await _libraryManager + .WatchStatus + .SetMovieStatus(id, User.GetIdOrThrow(), status, watchedTime); } /// diff --git a/back/src/Kyoo.Core/Views/Resources/NewsApi.cs b/back/src/Kyoo.Core/Views/Resources/NewsApi.cs index ae4e53f6..ae936e4f 100644 --- a/back/src/Kyoo.Core/Views/Resources/NewsApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/NewsApi.cs @@ -36,7 +36,6 @@ namespace Kyoo.Core.Api public class NewsApi : CrudThumbsApi { public NewsApi(IRepository news, IThumbnailsManager thumbs) - : base(news, thumbs) - { } + : base(news, thumbs) { } } } diff --git a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs index a40e9a3e..46bc24f1 100644 --- a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs @@ -66,9 +66,12 @@ namespace Kyoo.Core.Api [FromQuery] string? q, [FromQuery] Sort sortBy, [FromQuery] SearchPagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { - return SearchPage(await _searchManager.SearchCollections(q, sortBy, pagination, fields)); + return SearchPage( + await _searchManager.SearchCollections(q, sortBy, pagination, fields) + ); } /// @@ -91,7 +94,8 @@ namespace Kyoo.Core.Api [FromQuery] string? q, [FromQuery] Sort sortBy, [FromQuery] SearchPagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { return SearchPage(await _searchManager.SearchShows(q, sortBy, pagination, fields)); } @@ -116,7 +120,8 @@ namespace Kyoo.Core.Api [FromQuery] string? q, [FromQuery] Sort sortBy, [FromQuery] SearchPagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { return SearchPage(await _searchManager.SearchMovies(q, sortBy, pagination, fields)); } @@ -141,7 +146,8 @@ namespace Kyoo.Core.Api [FromQuery] string? q, [FromQuery] Sort sortBy, [FromQuery] SearchPagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { return SearchPage(await _searchManager.SearchItems(q, sortBy, pagination, fields)); } @@ -166,7 +172,8 @@ namespace Kyoo.Core.Api [FromQuery] string? q, [FromQuery] Sort sortBy, [FromQuery] SearchPagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { return SearchPage(await _searchManager.SearchEpisodes(q, sortBy, pagination, fields)); } @@ -191,7 +198,8 @@ namespace Kyoo.Core.Api [FromQuery] string? q, [FromQuery] Sort sortBy, [FromQuery] SearchPagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { return SearchPage(await _searchManager.SearchStudios(q, sortBy, pagination, fields)); } diff --git a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs index df276170..51421035 100644 --- a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs @@ -52,8 +52,7 @@ namespace Kyoo.Core.Api /// The library manager used to modify or retrieve information in the data store. /// /// The thumbnail manager used to retrieve images paths. - public SeasonApi(ILibraryManager libraryManager, - IThumbnailsManager thumbs) + public SeasonApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) : base(libraryManager.Seasons, thumbs) { _libraryManager = libraryManager; @@ -79,20 +78,30 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetEpisode(Identifier identifier, + public async Task>> GetEpisode( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { - ICollection resources = await _libraryManager.Episodes.GetAll( - Filter.And(filter, identifier.Matcher(x => x.SeasonId, x => x.Season!.Slug)), - sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Episodes + .GetAll( + Filter.And( + filter, + identifier.Matcher(x => x.SeasonId, x => x.Season!.Slug) + ), + sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Seasons.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Seasons.GetOrDefault(identifier.IsSame()) == null + ) return NotFound(); return Page(resources, pagination.Limit); } @@ -111,12 +120,14 @@ namespace Kyoo.Core.Api [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetShow(Identifier identifier, [FromQuery] Include fields) + public async Task> GetShow( + Identifier identifier, + [FromQuery] Include fields + ) { - Show? ret = await _libraryManager.Shows.GetOrDefault( - identifier.IsContainedIn(x => x.Seasons!), - fields - ); + Show? ret = await _libraryManager + .Shows + .GetOrDefault(identifier.IsContainedIn(x => x.Seasons!), fields); if (ret == null) return NotFound(); return ret; diff --git a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs index 8859c8f2..dfa4ee19 100644 --- a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -54,8 +54,7 @@ namespace Kyoo.Core.Api /// The library manager used to modify or retrieve information about the data store. /// /// The thumbnail manager used to retrieve images paths. - public ShowApi(ILibraryManager libraryManager, - IThumbnailsManager thumbs) + public ShowApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) : base(libraryManager.Shows, thumbs) { _libraryManager = libraryManager; @@ -81,20 +80,30 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetSeasons(Identifier identifier, + public async Task>> GetSeasons( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { - ICollection resources = await _libraryManager.Seasons.GetAll( - Filter.And(filter, identifier.Matcher(x => x.ShowId, x => x.Show!.Slug)), - sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Seasons + .GetAll( + Filter.And( + filter, + identifier.Matcher(x => x.ShowId, x => x.Show!.Slug) + ), + sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null + ) return NotFound(); return Page(resources, pagination.Limit); } @@ -119,20 +128,30 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetEpisodes(Identifier identifier, + public async Task>> GetEpisodes( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { - ICollection resources = await _libraryManager.Episodes.GetAll( - Filter.And(filter, identifier.Matcher(x => x.ShowId, x => x.Show!.Slug)), - sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Episodes + .GetAll( + Filter.And( + filter, + identifier.Matcher(x => x.ShowId, x => x.Show!.Slug) + ), + sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null + ) return NotFound(); return Page(resources, pagination.Limit); } @@ -186,9 +205,14 @@ namespace Kyoo.Core.Api [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetStudio(Identifier identifier, [FromQuery] Include fields) + public async Task> GetStudio( + Identifier identifier, + [FromQuery] Include fields + ) { - return await _libraryManager.Studios.Get(identifier.IsContainedIn(x => x.Shows!), fields); + return await _libraryManager + .Studios + .Get(identifier.IsContainedIn(x => x.Shows!), fields); } /// @@ -211,20 +235,27 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetCollections(Identifier identifier, + public async Task>> GetCollections( + Identifier identifier, [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include fields) + [FromQuery] Include fields + ) { - ICollection resources = await _libraryManager.Collections.GetAll( - Filter.And(filter, identifier.IsContainedIn(x => x.Shows!)), - sortBy, - fields, - pagination - ); + ICollection resources = await _libraryManager + .Collections + .GetAll( + Filter.And(filter, identifier.IsContainedIn(x => x.Shows!)), + sortBy, + fields, + pagination + ); - if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null) + if ( + !resources.Any() + && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null + ) return NotFound(); return Page(resources, pagination.Limit); } @@ -271,17 +302,16 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task SetWatchStatus(Identifier identifier, WatchStatus status) + public async Task SetWatchStatus( + Identifier identifier, + WatchStatus status + ) { Guid id = await identifier.Match( id => Task.FromResult(id), async slug => (await _libraryManager.Shows.Get(slug)).Id ); - return await _libraryManager.WatchStatus.SetShowStatus( - id, - User.GetIdOrThrow(), - status - ); + return await _libraryManager.WatchStatus.SetShowStatus(id, User.GetIdOrThrow(), status); } /// diff --git a/back/src/Kyoo.Core/Views/Resources/WatchlistApi.cs b/back/src/Kyoo.Core/Views/Resources/WatchlistApi.cs index 041ed467..566d2f4c 100644 --- a/back/src/Kyoo.Core/Views/Resources/WatchlistApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/WatchlistApi.cs @@ -63,7 +63,8 @@ namespace Kyoo.Core.Api public async Task>> GetAll( [FromQuery] Filter? filter, [FromQuery] Pagination pagination, - [FromQuery] Include? fields) + [FromQuery] Include? fields + ) { ICollection resources = await _repository.GetAll( filter, diff --git a/back/src/Kyoo.Host/Application.cs b/back/src/Kyoo.Host/Application.cs index 9263fa6f..4a5059d3 100644 --- a/back/src/Kyoo.Host/Application.cs +++ b/back/src/Kyoo.Host/Application.cs @@ -96,12 +96,10 @@ namespace Kyoo.Host _logger = Log.Logger.ForContext(); AppDomain.CurrentDomain.ProcessExit += (_, _) => Log.CloseAndFlush(); - AppDomain.CurrentDomain.UnhandledException += (_, ex) - => Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception"); + AppDomain.CurrentDomain.UnhandledException += (_, ex) => + Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception"); - IHost host = _CreateWebHostBuilder(args) - .ConfigureContainer(configure) - .Build(); + IHost host = _CreateWebHostBuilder(args).ConfigureContainer(configure).Build(); await using (AsyncServiceScope scope = host.Services.CreateAsyncScope()) { @@ -122,8 +120,14 @@ namespace Kyoo.Host try { CoreModule.Services = host.Services; - _logger.Information("Version: {Version}", Assembly.GetExecutingAssembly().GetName().Version.ToString(3)); - _logger.Information("Data directory: {DataDirectory}", Environment.CurrentDirectory); + _logger.Information( + "Version: {Version}", + Assembly.GetExecutingAssembly().GetName().Version.ToString(3) + ); + _logger.Information( + "Data directory: {DataDirectory}", + Environment.CurrentDirectory + ); await host.RunAsync(cancellationToken); } catch (Exception ex) @@ -146,12 +150,25 @@ namespace Kyoo.Host .ConfigureAppConfiguration(x => _SetupConfig(x, args)) .UseSerilog((host, services, builder) => _ConfigureLogging(builder)) .ConfigureServices(x => x.AddRouting()) - .ConfigureWebHost(x => x - .UseKestrel(options => { options.AddServerHeader = false; }) - .UseIIS() - .UseIISIntegration() - .UseUrls(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000") - .UseStartup(host => PluginsStartup.FromWebHost(host, new LoggerFactory().AddSerilog())) + .ConfigureWebHost( + x => + x.UseKestrel(options => + { + options.AddServerHeader = false; + }) + .UseIIS() + .UseIISIntegration() + .UseUrls( + 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} " + "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}"; builder - .MinimumLevel.Warning() - .MinimumLevel.Override("Kyoo", LogEventLevel.Verbose) - .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Verbose) - .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Fatal) - .WriteTo.Console(new ExpressionTemplate(template, theme: TemplateTheme.Code)) - .Enrich.WithThreadId() - .Enrich.FromLogContext(); + .MinimumLevel + .Warning() + .MinimumLevel + .Override("Kyoo", LogEventLevel.Verbose) + .MinimumLevel + .Override("Microsoft.Hosting.Lifetime", LogEventLevel.Verbose) + .MinimumLevel + .Override("Microsoft.EntityFrameworkCore", LogEventLevel.Fatal) + .WriteTo + .Console(new ExpressionTemplate(template, theme: TemplateTheme.Code)) + .Enrich + .WithThreadId() + .Enrich + .FromLogContext(); } } } diff --git a/back/src/Kyoo.Host/Contollers/PluginManager.cs b/back/src/Kyoo.Host/Contollers/PluginManager.cs index 6305c1b1..181dc6a1 100644 --- a/back/src/Kyoo.Host/Contollers/PluginManager.cs +++ b/back/src/Kyoo.Host/Contollers/PluginManager.cs @@ -51,8 +51,7 @@ namespace Kyoo.Host.Controllers /// /// A service container to allow initialization of plugins /// The logger used by this class. - public PluginManager(IServiceProvider provider, - ILogger logger) + public PluginManager(IServiceProvider provider, ILogger logger) { _provider = provider; _logger = logger; @@ -86,9 +85,10 @@ namespace Kyoo.Host.Controllers /// public void LoadPlugins(params Type[] plugins) { - LoadPlugins(plugins - .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)) - .ToArray() + LoadPlugins( + plugins + .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)) + .ToArray() ); } } diff --git a/back/src/Kyoo.Host/HostModule.cs b/back/src/Kyoo.Host/HostModule.cs index 3b9b4c55..46997857 100644 --- a/back/src/Kyoo.Host/HostModule.cs +++ b/back/src/Kyoo.Host/HostModule.cs @@ -55,9 +55,7 @@ namespace Kyoo.Host } /// - public IEnumerable ConfigureSteps => new[] - { - SA.New(app => app.UseSerilogRequestLogging(), SA.Before) - }; + public IEnumerable ConfigureSteps => + new[] { SA.New(app => app.UseSerilogRequestLogging(), SA.Before) }; } } diff --git a/back/src/Kyoo.Host/PluginsStartup.cs b/back/src/Kyoo.Host/PluginsStartup.cs index b89db6b2..6fcd4393 100644 --- a/back/src/Kyoo.Host/PluginsStartup.cs +++ b/back/src/Kyoo.Host/PluginsStartup.cs @@ -79,14 +79,11 @@ namespace Kyoo.Host /// The logger factory used to log while the application is setting itself up. /// /// A new . - public static PluginsStartup FromWebHost(WebHostBuilderContext host, - ILoggerFactory logger) + public static PluginsStartup FromWebHost(WebHostBuilderContext host, ILoggerFactory logger) { - HostServiceProvider hostProvider = new(host.HostingEnvironment, host.Configuration, logger); - PluginManager plugins = new( - hostProvider, - logger.CreateLogger() - ); + HostServiceProvider hostProvider = + new(host.HostingEnvironment, host.Configuration, logger); + PluginManager plugins = new(hostProvider, logger.CreateLogger()); return new PluginsStartup(plugins); } @@ -96,7 +93,9 @@ namespace Kyoo.Host /// The service collection to fill. 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); _hostModule.Configure(services); @@ -122,13 +121,14 @@ namespace Kyoo.Host /// An autofac container used to create a new scope to configure asp-net. public void Configure(IApplicationBuilder app, ILifetimeScope container) { - IEnumerable steps = _plugins.GetAllPlugins() + IEnumerable steps = _plugins + .GetAllPlugins() .Append(_hostModule) .SelectMany(x => x.ConfigureSteps) .OrderByDescending(x => x.Priority); - using ILifetimeScope scope = container.BeginLifetimeScope(x => - x.RegisterInstance(app).SingleInstance().ExternallyOwned() + using ILifetimeScope scope = container.BeginLifetimeScope( + x => x.RegisterInstance(app).SingleInstance().ExternallyOwned() ); IServiceProvider provider = scope.Resolve(); foreach (IStartupAction step in steps) @@ -164,9 +164,11 @@ namespace Kyoo.Host /// /// The configuration context /// A logger factory used to create a logger for the plugin manager. - public HostServiceProvider(IWebHostEnvironment hostEnvironment, + public HostServiceProvider( + IWebHostEnvironment hostEnvironment, IConfiguration configuration, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory + ) { _hostEnvironment = hostEnvironment; _configuration = configuration; @@ -176,7 +178,10 @@ namespace Kyoo.Host /// public object GetService(Type serviceType) { - if (serviceType == typeof(IWebHostEnvironment) || serviceType == typeof(IHostEnvironment)) + if ( + serviceType == typeof(IWebHostEnvironment) + || serviceType == typeof(IHostEnvironment) + ) return _hostEnvironment; if (serviceType == typeof(IConfiguration)) return _configuration; diff --git a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs index 1a4a5042..04930d41 100644 --- a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs +++ b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs @@ -33,103 +33,98 @@ namespace Kyoo.Meiliseach private readonly IConfiguration _configuration; - public static Dictionary IndexSettings => new() - { + public static Dictionary IndexSettings => + new() { - "items", - new Settings() { - SearchableAttributes = new[] + "items", + new Settings() { - CamelCase.ConvertName(nameof(Movie.Name)), - CamelCase.ConvertName(nameof(Movie.Slug)), - CamelCase.ConvertName(nameof(Movie.Aliases)), - CamelCase.ConvertName(nameof(Movie.Path)), - CamelCase.ConvertName(nameof(Movie.Tags)), - CamelCase.ConvertName(nameof(Movie.Overview)), - }, - FilterableAttributes = new[] - { - CamelCase.ConvertName(nameof(Movie.Genres)), - CamelCase.ConvertName(nameof(Movie.Status)), - CamelCase.ConvertName(nameof(Movie.AirDate)), - CamelCase.ConvertName(nameof(Movie.StudioId)), - "kind" - }, - SortableAttributes = new[] - { - CamelCase.ConvertName(nameof(Movie.AirDate)), - CamelCase.ConvertName(nameof(Movie.AddedDate)), - CamelCase.ConvertName(nameof(Movie.Rating)), - CamelCase.ConvertName(nameof(Movie.Runtime)), - }, - DisplayedAttributes = new[] - { - CamelCase.ConvertName(nameof(Movie.Id)), - "kind" - }, - RankingRules = new[] - { - "words", - "typo", - "proximity", - "attribute", - "sort", - "exactness", - $"{CamelCase.ConvertName(nameof(Movie.Rating))}:desc", + SearchableAttributes = new[] + { + CamelCase.ConvertName(nameof(Movie.Name)), + CamelCase.ConvertName(nameof(Movie.Slug)), + CamelCase.ConvertName(nameof(Movie.Aliases)), + CamelCase.ConvertName(nameof(Movie.Path)), + CamelCase.ConvertName(nameof(Movie.Tags)), + CamelCase.ConvertName(nameof(Movie.Overview)), + }, + FilterableAttributes = new[] + { + CamelCase.ConvertName(nameof(Movie.Genres)), + CamelCase.ConvertName(nameof(Movie.Status)), + CamelCase.ConvertName(nameof(Movie.AirDate)), + CamelCase.ConvertName(nameof(Movie.StudioId)), + "kind" + }, + SortableAttributes = new[] + { + CamelCase.ConvertName(nameof(Movie.AirDate)), + CamelCase.ConvertName(nameof(Movie.AddedDate)), + CamelCase.ConvertName(nameof(Movie.Rating)), + CamelCase.ConvertName(nameof(Movie.Runtime)), + }, + DisplayedAttributes = new[] + { + CamelCase.ConvertName(nameof(Movie.Id)), + "kind" + }, + RankingRules = new[] + { + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + $"{CamelCase.ConvertName(nameof(Movie.Rating))}:desc", + } + // TODO: Add stopwords } - // TODO: Add stopwords - } - }, - { - nameof(Episode), - new Settings() + }, { - SearchableAttributes = new[] + nameof(Episode), + new Settings() { - CamelCase.ConvertName(nameof(Episode.Name)), - CamelCase.ConvertName(nameof(Episode.Overview)), - CamelCase.ConvertName(nameof(Episode.Slug)), - CamelCase.ConvertName(nameof(Episode.Path)), - }, - FilterableAttributes = new[] - { - CamelCase.ConvertName(nameof(Episode.SeasonNumber)), - }, - SortableAttributes = new[] - { - CamelCase.ConvertName(nameof(Episode.ReleaseDate)), - CamelCase.ConvertName(nameof(Episode.AddedDate)), - CamelCase.ConvertName(nameof(Episode.SeasonNumber)), - CamelCase.ConvertName(nameof(Episode.EpisodeNumber)), - CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)), - }, - DisplayedAttributes = new[] - { - CamelCase.ConvertName(nameof(Episode.Id)), - }, - // TODO: Add stopwords - } - }, - { - nameof(Studio), - new Settings() + SearchableAttributes = new[] + { + CamelCase.ConvertName(nameof(Episode.Name)), + CamelCase.ConvertName(nameof(Episode.Overview)), + CamelCase.ConvertName(nameof(Episode.Slug)), + CamelCase.ConvertName(nameof(Episode.Path)), + }, + FilterableAttributes = new[] + { + CamelCase.ConvertName(nameof(Episode.SeasonNumber)), + }, + SortableAttributes = new[] + { + CamelCase.ConvertName(nameof(Episode.ReleaseDate)), + CamelCase.ConvertName(nameof(Episode.AddedDate)), + CamelCase.ConvertName(nameof(Episode.SeasonNumber)), + CamelCase.ConvertName(nameof(Episode.EpisodeNumber)), + CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)), + }, + DisplayedAttributes = new[] { CamelCase.ConvertName(nameof(Episode.Id)), }, + // TODO: Add stopwords + } + }, { - SearchableAttributes = new[] + nameof(Studio), + new Settings() { - CamelCase.ConvertName(nameof(Studio.Name)), - CamelCase.ConvertName(nameof(Studio.Slug)), - }, - FilterableAttributes = Array.Empty(), - SortableAttributes = Array.Empty(), - DisplayedAttributes = new[] - { - CamelCase.ConvertName(nameof(Studio.Id)), - }, - // TODO: Add stopwords - } - }, - }; + SearchableAttributes = new[] + { + CamelCase.ConvertName(nameof(Studio.Name)), + CamelCase.ConvertName(nameof(Studio.Slug)), + }, + FilterableAttributes = Array.Empty(), + SortableAttributes = Array.Empty(), + DisplayedAttributes = new[] { CamelCase.ConvertName(nameof(Studio.Id)), }, + // TODO: Add stopwords + } + }, + }; public MeilisearchModule(IConfiguration configuration) { @@ -173,7 +168,10 @@ namespace Kyoo.Meiliseach 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.Index(index).UpdateSettingsAsync(IndexSettings[index]); } @@ -181,12 +179,15 @@ namespace Kyoo.Meiliseach /// public void Configure(ContainerBuilder builder) { - builder.RegisterInstance(new MeilisearchClient( - _configuration.GetValue("MEILI_HOST", "http://meilisearch:7700"), - _configuration.GetValue("MEILI_MASTER_KEY") - )).SingleInstance(); - builder.RegisterType().AsSelf().SingleInstance() - .AutoActivate(); + builder + .RegisterInstance( + new MeilisearchClient( + _configuration.GetValue("MEILI_HOST", "http://meilisearch:7700"), + _configuration.GetValue("MEILI_MASTER_KEY") + ) + ) + .SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance().AutoActivate(); builder.RegisterType().As().InstancePerLifetimeScope(); } } diff --git a/back/src/Kyoo.Meilisearch/SearchManager.cs b/back/src/Kyoo.Meilisearch/SearchManager.cs index 9897f4bf..f2ab5d1b 100644 --- a/back/src/Kyoo.Meilisearch/SearchManager.cs +++ b/back/src/Kyoo.Meilisearch/SearchManager.cs @@ -36,11 +36,21 @@ public class SearchManager : ISearchManager return sort switch { Sort.Default => Array.Empty(), - Sort.By @sortBy => 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}"), + Sort.By @sortBy + => 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}"), Sort.Conglomerate(var list) => list.SelectMany(x => _GetSortsBy(index, x)), - Sort.Random => throw new ValidationException("Random sorting is not supported while searching."), + Sort.Random + => throw new ValidationException( + "Random sorting is not supported while searching." + ), _ => Array.Empty(), }; } @@ -51,79 +61,100 @@ public class SearchManager : ISearchManager _libraryManager = libraryManager; } - private async Task.SearchResult> _Search(string index, string? query, + private async Task.SearchResult> _Search( + string index, + string? query, string? where = null, Sort? sortBy = default, SearchPagination? pagination = default, - Include? include = default) + Include? include = default + ) where T : class, IResource, IQuery { // TODO: add filters and facets - ISearchable res = await _client.Index(index).SearchAsync(query, new SearchQuery() - { - Filter = where, - Sort = _GetSortsBy(index, sortBy), - Limit = pagination?.Limit ?? 50, - Offset = pagination?.Skip ?? 0, - }); + ISearchable res = await _client + .Index(index) + .SearchAsync( + query, + new SearchQuery() + { + Filter = where, + Sort = _GetSortsBy(index, sortBy), + Limit = pagination?.Limit ?? 50, + Offset = pagination?.Skip ?? 0, + } + ); return new SearchPage.SearchResult { Query = query, - Items = await _libraryManager.Repository() + Items = await _libraryManager + .Repository() .FromIds(res.Hits.Select(x => x.Id).ToList(), include), }; } /// - public Task.SearchResult> SearchItems(string? query, + public Task.SearchResult> SearchItems( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default) + Include? include = default + ) { return _Search("items", query, null, sortBy, pagination, include); } /// - public Task.SearchResult> SearchMovies(string? query, + public Task.SearchResult> SearchMovies( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default) + Include? include = default + ) { return _Search("items", query, $"kind = {nameof(Movie)}", sortBy, pagination, include); } /// - public Task.SearchResult> SearchShows(string? query, + public Task.SearchResult> SearchShows( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default) + Include? include = default + ) { return _Search("items", query, $"kind = {nameof(Show)}", sortBy, pagination, include); } /// - public Task.SearchResult> SearchCollections(string? query, + public Task.SearchResult> SearchCollections( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default) + Include? include = default + ) { return _Search("items", query, $"kind = {nameof(Collection)}", sortBy, pagination, include); } /// - public Task.SearchResult> SearchEpisodes(string? query, + public Task.SearchResult> SearchEpisodes( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default) + Include? include = default + ) { return _Search(nameof(Episode), query, null, sortBy, pagination, include); } /// - public Task.SearchResult> SearchStudios(string? query, + public Task.SearchResult> SearchStudios( + string? query, Sort sortBy, SearchPagination pagination, - Include? include = default) + Include? include = default + ) { return _Search(nameof(Studio), query, null, sortBy, pagination, include); } diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs index 9c6c4240..29b69112 100644 --- a/back/src/Kyoo.Postgresql/DatabaseContext.cs +++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs @@ -117,11 +117,13 @@ namespace Kyoo.Postgresql where T2 : class, IResource { Set>(LinkName()) - .Add(new Dictionary - { - [LinkNameFk()] = first, - [LinkNameFk()] = second - }); + .Add( + new Dictionary + { + [LinkNameFk()] = first, + [LinkNameFk()] = second + } + ); } protected DatabaseContext(IHttpContextAccessor accessor) @@ -177,11 +179,16 @@ namespace Kyoo.Postgresql // { // x.ToJson(); // }); - modelBuilder.Entity() + modelBuilder + .Entity() .Property(x => x.ExternalId) .HasConversion( v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), - v => JsonSerializer.Deserialize>(v, (JsonSerializerOptions?)null)! + v => + JsonSerializer.Deserialize>( + v, + (JsonSerializerOptions?)null + )! ) .HasColumnType("json"); } @@ -189,18 +196,16 @@ namespace Kyoo.Postgresql private static void _HasImages(ModelBuilder modelBuilder) where T : class, IThumbnails { - modelBuilder.Entity() - .OwnsOne(x => x.Poster); - modelBuilder.Entity() - .OwnsOne(x => x.Thumbnail); - modelBuilder.Entity() - .OwnsOne(x => x.Logo); + modelBuilder.Entity().OwnsOne(x => x.Poster); + modelBuilder.Entity().OwnsOne(x => x.Thumbnail); + modelBuilder.Entity().OwnsOne(x => x.Logo); } private static void _HasAddedDate(ModelBuilder modelBuilder) where T : class, IAddedDate { - modelBuilder.Entity() + modelBuilder + .Entity() .Property(x => x.AddedDate) .HasDefaultValueSql("now() at time zone 'utc'") .ValueGeneratedOnAdd(); @@ -215,27 +220,30 @@ namespace Kyoo.Postgresql /// The second navigation expression from T2 to T /// The owning type of the relationship /// The owned type of the relationship - private void _HasManyToMany(ModelBuilder modelBuilder, + private void _HasManyToMany( + ModelBuilder modelBuilder, Expression?>> firstNavigation, - Expression?>> secondNavigation) + Expression?>> secondNavigation + ) where T : class, IResource where T2 : class, IResource { - modelBuilder.Entity() + modelBuilder + .Entity() .HasMany(secondNavigation) .WithMany(firstNavigation) .UsingEntity>( LinkName(), - x => x - .HasOne() - .WithMany() - .HasForeignKey(LinkNameFk()) - .OnDelete(DeleteBehavior.Cascade), - x => x - .HasOne() - .WithMany() - .HasForeignKey(LinkNameFk()) - .OnDelete(DeleteBehavior.Cascade) + x => + x.HasOne() + .WithMany() + .HasForeignKey(LinkNameFk()) + .OnDelete(DeleteBehavior.Cascade), + x => + x.HasOne() + .WithMany() + .HasForeignKey(LinkNameFk()) + .OnDelete(DeleteBehavior.Cascade) ); } @@ -247,33 +255,37 @@ namespace Kyoo.Postgresql { base.OnModelCreating(modelBuilder); - modelBuilder.Entity() - .Ignore(x => x.FirstEpisode) - .Ignore(x => x.AirDate); - modelBuilder.Entity() + modelBuilder.Entity().Ignore(x => x.FirstEpisode).Ignore(x => x.AirDate); + modelBuilder + .Entity() .Ignore(x => x.PreviousEpisode) .Ignore(x => x.NextEpisode); // modelBuilder.Entity() // .Ignore(x => x.ForPeople); - modelBuilder.Entity() + modelBuilder + .Entity() .HasMany(x => x.Seasons) .WithOne(x => x.Show) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() + modelBuilder + .Entity() .HasMany(x => x.Episodes) .WithOne(x => x.Show) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() + modelBuilder + .Entity() .HasMany(x => x.Episodes) .WithOne(x => x.Season) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() + modelBuilder + .Entity() .HasOne(x => x.Studio) .WithMany(x => x.Movies) .OnDelete(DeleteBehavior.SetNull); - modelBuilder.Entity() + modelBuilder + .Entity() .HasOne(x => x.Studio) .WithMany(x => x.Shows) .OnDelete(DeleteBehavior.SetNull); @@ -305,16 +317,21 @@ namespace Kyoo.Postgresql modelBuilder.Entity().OwnsOne(x => x.Logo); - modelBuilder.Entity() + modelBuilder + .Entity() .HasKey(x => new { User = x.UserId, Movie = x.MovieId }); - modelBuilder.Entity() + modelBuilder + .Entity() .HasKey(x => new { User = x.UserId, Show = x.ShowId }); - modelBuilder.Entity() + modelBuilder + .Entity() .HasKey(x => new { User = x.UserId, Episode = x.EpisodeId }); modelBuilder.Entity().HasQueryFilter(x => x.UserId == CurrentUserId); modelBuilder.Entity().HasQueryFilter(x => x.UserId == CurrentUserId); - modelBuilder.Entity().HasQueryFilter(x => x.UserId == CurrentUserId); + modelBuilder + .Entity() + .HasQueryFilter(x => x.UserId == CurrentUserId); modelBuilder.Entity().Navigation(x => x.NextEpisode).AutoInclude(); @@ -326,39 +343,35 @@ namespace Kyoo.Postgresql modelBuilder.Entity().Ignore(x => x.WatchStatus); modelBuilder.Entity().Ignore(x => x.WatchStatus); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); + modelBuilder.Entity().HasIndex(x => x.Slug).IsUnique(); // modelBuilder.Entity() // .HasIndex(x => x.Slug) // .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); - modelBuilder.Entity() + modelBuilder.Entity().HasIndex(x => x.Slug).IsUnique(); + modelBuilder.Entity().HasIndex(x => x.Slug).IsUnique(); + modelBuilder.Entity().HasIndex(x => x.Slug).IsUnique(); + modelBuilder + .Entity() .HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber }) .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber }) - .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) - .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => x.Slug) + modelBuilder.Entity().HasIndex(x => x.Slug).IsUnique(); + modelBuilder + .Entity() + .HasIndex( + x => + new + { + ShowID = x.ShowId, + x.SeasonNumber, + x.EpisodeNumber, + x.AbsoluteNumber + } + ) .IsUnique(); + modelBuilder.Entity().HasIndex(x => x.Slug).IsUnique(); + modelBuilder.Entity().HasIndex(x => x.Slug).IsUnique(); - modelBuilder.Entity() - .Ignore(x => x.Links); + modelBuilder.Entity().Ignore(x => x.Links); } /// @@ -428,8 +441,10 @@ namespace Kyoo.Postgresql /// A to observe while waiting for the task to complete /// A duplicated item has been found. /// The number of state entries written to the database. - public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, - CancellationToken cancellationToken = default) + public override async Task SaveChangesAsync( + bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = default + ) { try { @@ -450,7 +465,9 @@ namespace Kyoo.Postgresql /// A to observe while waiting for the task to complete /// A duplicated item has been found. /// The number of state entries written to the database. - public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + public override async Task SaveChangesAsync( + CancellationToken cancellationToken = default + ) { try { @@ -475,7 +492,8 @@ namespace Kyoo.Postgresql /// The number of state entries written to the database. public async Task SaveChangesAsync( Func> getExisting, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { try { @@ -523,9 +541,7 @@ namespace Kyoo.Postgresql public T? LocalEntity(string slug) where T : class, IResource { - return ChangeTracker.Entries() - .FirstOrDefault(x => x.Entity.Slug == slug) - ?.Entity; + return ChangeTracker.Entries().FirstOrDefault(x => x.Entity.Slug == slug)?.Entity; } /// @@ -540,7 +556,11 @@ namespace Kyoo.Postgresql /// 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; } diff --git a/back/src/Kyoo.Postgresql/Migrations/20231128171554_Initial.cs b/back/src/Kyoo.Postgresql/Migrations/20231128171554_Initial.cs index 0cac871c..20b62adc 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231128171554_Initial.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231128171554_Initial.cs @@ -31,93 +31,163 @@ namespace Kyoo.Postgresql.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western") + migrationBuilder + .AlterDatabase() + .Annotation( + "Npgsql:Enum:genre", + "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western" + ) .Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned"); migrationBuilder.CreateTable( name: "collections", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - name = table.Column(type: "text", nullable: false), - overview = table.Column(type: "text", nullable: true), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - poster_source = table.Column(type: "text", nullable: true), - poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - thumbnail_source = table.Column(type: "text", nullable: true), - thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - logo_source = table.Column(type: "text", nullable: true), - logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - external_id = table.Column(type: "json", nullable: false) - }, + columns: table => + new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: false + ), + name = table.Column(type: "text", nullable: false), + overview = table.Column(type: "text", nullable: true), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + external_id = table.Column(type: "json", nullable: false) + }, constraints: table => { table.PrimaryKey("pk_collections", x => x.id); - }); + } + ); migrationBuilder.CreateTable( name: "studios", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - name = table.Column(type: "text", nullable: false), - external_id = table.Column(type: "json", nullable: false) - }, + columns: table => + new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: false + ), + name = table.Column(type: "text", nullable: false), + external_id = table.Column(type: "json", nullable: false) + }, constraints: table => { table.PrimaryKey("pk_studios", x => x.id); - }); + } + ); migrationBuilder.CreateTable( name: "users", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - username = table.Column(type: "text", nullable: false), - email = table.Column(type: "text", nullable: false), - password = table.Column(type: "text", nullable: false), - permissions = table.Column(type: "text[]", nullable: false), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - logo_source = table.Column(type: "text", nullable: true), - logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true) - }, + columns: table => + new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: false + ), + username = table.Column(type: "text", nullable: false), + email = table.Column(type: "text", nullable: false), + password = table.Column(type: "text", nullable: false), + permissions = table.Column(type: "text[]", nullable: false), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ) + }, constraints: table => { table.PrimaryKey("pk_users", x => x.id); - }); + } + ); migrationBuilder.CreateTable( name: "movies", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - name = table.Column(type: "text", nullable: false), - tagline = table.Column(type: "text", nullable: true), - aliases = table.Column(type: "text[]", nullable: false), - path = table.Column(type: "text", nullable: false), - overview = table.Column(type: "text", nullable: true), - tags = table.Column(type: "text[]", nullable: false), - genres = table.Column(type: "genre[]", nullable: false), - status = table.Column(type: "status", nullable: false), - rating = table.Column(type: "integer", nullable: false), - runtime = table.Column(type: "integer", nullable: false), - air_date = table.Column(type: "timestamp with time zone", nullable: true), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - poster_source = table.Column(type: "text", nullable: true), - poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - thumbnail_source = table.Column(type: "text", nullable: true), - thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - logo_source = table.Column(type: "text", nullable: true), - logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - trailer = table.Column(type: "text", nullable: true), - external_id = table.Column(type: "json", nullable: false), - studio_id = table.Column(type: "uuid", nullable: true) - }, + columns: table => + new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: false + ), + name = table.Column(type: "text", nullable: false), + tagline = table.Column(type: "text", nullable: true), + aliases = table.Column(type: "text[]", nullable: false), + path = table.Column(type: "text", nullable: false), + overview = table.Column(type: "text", nullable: true), + tags = table.Column(type: "text[]", nullable: false), + genres = table.Column(type: "genre[]", nullable: false), + status = table.Column(type: "status", nullable: false), + rating = table.Column(type: "integer", nullable: false), + runtime = table.Column(type: "integer", nullable: false), + air_date = table.Column( + type: "timestamp with time zone", + nullable: true + ), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + trailer = table.Column(type: "text", nullable: true), + external_id = table.Column(type: "json", nullable: false), + studio_id = table.Column(type: "uuid", nullable: true) + }, constraints: table => { table.PrimaryKey("pk_movies", x => x.id); @@ -126,36 +196,65 @@ namespace Kyoo.Postgresql.Migrations column: x => x.studio_id, principalTable: "studios", principalColumn: "id", - onDelete: ReferentialAction.SetNull); - }); + onDelete: ReferentialAction.SetNull + ); + } + ); migrationBuilder.CreateTable( name: "shows", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - name = table.Column(type: "text", nullable: false), - tagline = table.Column(type: "text", nullable: true), - aliases = table.Column>(type: "text[]", nullable: false), - overview = table.Column(type: "text", nullable: true), - tags = table.Column>(type: "text[]", nullable: false), - genres = table.Column>(type: "genre[]", nullable: false), - status = table.Column(type: "status", nullable: false), - rating = table.Column(type: "integer", nullable: false), - start_air = table.Column(type: "timestamp with time zone", nullable: true), - end_air = table.Column(type: "timestamp with time zone", nullable: true), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - poster_source = table.Column(type: "text", nullable: true), - poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - thumbnail_source = table.Column(type: "text", nullable: true), - thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - logo_source = table.Column(type: "text", nullable: true), - logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - trailer = table.Column(type: "text", nullable: true), - external_id = table.Column(type: "json", nullable: false), - studio_id = table.Column(type: "uuid", nullable: true) - }, + columns: table => + new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: false + ), + name = table.Column(type: "text", nullable: false), + tagline = table.Column(type: "text", nullable: true), + aliases = table.Column>(type: "text[]", nullable: false), + overview = table.Column(type: "text", nullable: true), + tags = table.Column>(type: "text[]", nullable: false), + genres = table.Column>(type: "genre[]", nullable: false), + status = table.Column(type: "status", nullable: false), + rating = table.Column(type: "integer", nullable: false), + start_air = table.Column( + type: "timestamp with time zone", + nullable: true + ), + end_air = table.Column( + type: "timestamp with time zone", + nullable: true + ), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + trailer = table.Column(type: "text", nullable: true), + external_id = table.Column(type: "json", nullable: false), + studio_id = table.Column(type: "uuid", nullable: true) + }, constraints: table => { table.PrimaryKey("pk_shows", x => x.id); @@ -164,78 +263,121 @@ namespace Kyoo.Postgresql.Migrations column: x => x.studio_id, principalTable: "studios", principalColumn: "id", - onDelete: ReferentialAction.SetNull); - }); + onDelete: ReferentialAction.SetNull + ); + } + ); migrationBuilder.CreateTable( name: "link_collection_movie", - columns: table => new - { - collection_id = table.Column(type: "uuid", nullable: false), - movie_id = table.Column(type: "uuid", nullable: false) - }, + columns: table => + new + { + collection_id = table.Column(type: "uuid", nullable: false), + movie_id = table.Column(type: "uuid", nullable: false) + }, 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( name: "fk_link_collection_movie_collections_collection_id", column: x => x.collection_id, principalTable: "collections", principalColumn: "id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "fk_link_collection_movie_movies_movie_id", column: x => x.movie_id, principalTable: "movies", principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "link_collection_show", - columns: table => new - { - collection_id = table.Column(type: "uuid", nullable: false), - show_id = table.Column(type: "uuid", nullable: false) - }, + columns: table => + new + { + collection_id = table.Column(type: "uuid", nullable: false), + show_id = table.Column(type: "uuid", nullable: false) + }, 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( name: "fk_link_collection_show_collections_collection_id", column: x => x.collection_id, principalTable: "collections", principalColumn: "id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "fk_link_collection_show_shows_show_id", column: x => x.show_id, principalTable: "shows", principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "seasons", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - show_id = table.Column(type: "uuid", nullable: false), - season_number = table.Column(type: "integer", nullable: false), - name = table.Column(type: "text", nullable: true), - overview = table.Column(type: "text", nullable: true), - start_date = table.Column(type: "timestamp with time zone", nullable: true), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - end_date = table.Column(type: "timestamp with time zone", nullable: true), - poster_source = table.Column(type: "text", nullable: true), - poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - thumbnail_source = table.Column(type: "text", nullable: true), - thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - logo_source = table.Column(type: "text", nullable: true), - logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - external_id = table.Column(type: "json", nullable: false) - }, + columns: table => + new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: false + ), + show_id = table.Column(type: "uuid", nullable: false), + season_number = table.Column(type: "integer", nullable: false), + name = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + start_date = table.Column( + type: "timestamp with time zone", + nullable: true + ), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + end_date = table.Column( + type: "timestamp with time zone", + nullable: true + ), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + external_id = table.Column(type: "json", nullable: false) + }, constraints: table => { table.PrimaryKey("pk_seasons", x => x.id); @@ -244,34 +386,60 @@ namespace Kyoo.Postgresql.Migrations column: x => x.show_id, principalTable: "shows", principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "episodes", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - show_id = table.Column(type: "uuid", nullable: false), - season_id = table.Column(type: "uuid", nullable: true), - season_number = table.Column(type: "integer", nullable: true), - episode_number = table.Column(type: "integer", nullable: true), - absolute_number = table.Column(type: "integer", nullable: true), - path = table.Column(type: "text", nullable: false), - name = table.Column(type: "text", nullable: true), - overview = table.Column(type: "text", nullable: true), - runtime = table.Column(type: "integer", nullable: false), - release_date = table.Column(type: "timestamp with time zone", nullable: true), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - poster_source = table.Column(type: "text", nullable: true), - poster_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - thumbnail_source = table.Column(type: "text", nullable: true), - thumbnail_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - logo_source = table.Column(type: "text", nullable: true), - logo_blurhash = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - external_id = table.Column(type: "json", nullable: false) - }, + columns: table => + new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: false + ), + show_id = table.Column(type: "uuid", nullable: false), + season_id = table.Column(type: "uuid", nullable: true), + season_number = table.Column(type: "integer", nullable: true), + episode_number = table.Column(type: "integer", nullable: true), + absolute_number = table.Column(type: "integer", nullable: true), + path = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + runtime = table.Column(type: "integer", nullable: false), + release_date = table.Column( + type: "timestamp with time zone", + nullable: true + ), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + poster_source = table.Column(type: "text", nullable: true), + poster_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + thumbnail_source = table.Column(type: "text", nullable: true), + thumbnail_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + logo_source = table.Column(type: "text", nullable: true), + logo_blurhash = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: true + ), + external_id = table.Column(type: "json", nullable: false) + }, constraints: table => { table.PrimaryKey("pk_episodes", x => x.id); @@ -280,124 +448,132 @@ namespace Kyoo.Postgresql.Migrations column: x => x.season_id, principalTable: "seasons", principalColumn: "id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "fk_episodes_shows_show_id", column: x => x.show_id, principalTable: "shows", principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateIndex( name: "ix_collections_slug", table: "collections", column: "slug", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_episodes_season_id", table: "episodes", - column: "season_id"); + column: "season_id" + ); migrationBuilder.CreateIndex( name: "ix_episodes_show_id_season_number_episode_number_absolute_numb", table: "episodes", columns: new[] { "show_id", "season_number", "episode_number", "absolute_number" }, - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_episodes_slug", table: "episodes", column: "slug", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_link_collection_movie_movie_id", table: "link_collection_movie", - column: "movie_id"); + column: "movie_id" + ); migrationBuilder.CreateIndex( name: "ix_link_collection_show_show_id", table: "link_collection_show", - column: "show_id"); + column: "show_id" + ); migrationBuilder.CreateIndex( name: "ix_movies_slug", table: "movies", column: "slug", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_movies_studio_id", table: "movies", - column: "studio_id"); + column: "studio_id" + ); migrationBuilder.CreateIndex( name: "ix_seasons_show_id_season_number", table: "seasons", columns: new[] { "show_id", "season_number" }, - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_seasons_slug", table: "seasons", column: "slug", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_shows_slug", table: "shows", column: "slug", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_shows_studio_id", table: "shows", - column: "studio_id"); + column: "studio_id" + ); migrationBuilder.CreateIndex( name: "ix_studios_slug", table: "studios", column: "slug", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "ix_users_slug", table: "users", column: "slug", - unique: true); + unique: true + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "episodes"); + migrationBuilder.DropTable(name: "episodes"); - migrationBuilder.DropTable( - name: "link_collection_movie"); + migrationBuilder.DropTable(name: "link_collection_movie"); - migrationBuilder.DropTable( - name: "link_collection_show"); + migrationBuilder.DropTable(name: "link_collection_show"); - migrationBuilder.DropTable( - name: "users"); + migrationBuilder.DropTable(name: "users"); - migrationBuilder.DropTable( - name: "seasons"); + migrationBuilder.DropTable(name: "seasons"); - migrationBuilder.DropTable( - name: "movies"); + migrationBuilder.DropTable(name: "movies"); - migrationBuilder.DropTable( - name: "collections"); + migrationBuilder.DropTable(name: "collections"); - migrationBuilder.DropTable( - name: "shows"); + migrationBuilder.DropTable(name: "shows"); - migrationBuilder.DropTable( - name: "studios"); + migrationBuilder.DropTable(name: "studios"); } } } diff --git a/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.cs b/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.cs index cb699d9d..ec932d59 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.cs @@ -30,54 +30,83 @@ namespace Kyoo.Postgresql.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western") + migrationBuilder + .AlterDatabase() + .Annotation( + "Npgsql:Enum:genre", + "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western" + ) .Annotation("Npgsql:Enum:status", "unknown,finished,airing,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"); migrationBuilder.CreateTable( name: "episode_watch_status", - columns: table => new - { - user_id = table.Column(type: "uuid", nullable: false), - episode_id = table.Column(type: "uuid", nullable: false), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - played_date = table.Column(type: "timestamp with time zone", nullable: true), - status = table.Column(type: "watch_status", nullable: false), - watched_time = table.Column(type: "integer", nullable: true), - watched_percent = table.Column(type: "integer", nullable: true) - }, + columns: table => + new + { + user_id = table.Column(type: "uuid", nullable: false), + episode_id = table.Column(type: "uuid", nullable: false), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + played_date = table.Column( + type: "timestamp with time zone", + nullable: true + ), + status = table.Column(type: "watch_status", nullable: false), + watched_time = table.Column(type: "integer", nullable: true), + watched_percent = table.Column(type: "integer", nullable: true) + }, 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( name: "fk_episode_watch_status_episodes_episode_id", column: x => x.episode_id, principalTable: "episodes", principalColumn: "id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "fk_episode_watch_status_users_user_id", column: x => x.user_id, principalTable: "users", principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "movie_watch_status", - columns: table => new - { - user_id = table.Column(type: "uuid", nullable: false), - movie_id = table.Column(type: "uuid", nullable: false), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - played_date = table.Column(type: "timestamp with time zone", nullable: true), - status = table.Column(type: "watch_status", nullable: false), - watched_time = table.Column(type: "integer", nullable: true), - watched_percent = table.Column(type: "integer", nullable: true) - }, + columns: table => + new + { + user_id = table.Column(type: "uuid", nullable: false), + movie_id = table.Column(type: "uuid", nullable: false), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + played_date = table.Column( + type: "timestamp with time zone", + nullable: true + ), + status = table.Column(type: "watch_status", nullable: false), + watched_time = table.Column(type: "integer", nullable: true), + watched_percent = table.Column(type: "integer", nullable: true) + }, constraints: table => { table.PrimaryKey("pk_movie_watch_status", x => new { x.user_id, x.movie_id }); @@ -86,29 +115,40 @@ namespace Kyoo.Postgresql.Migrations column: x => x.movie_id, principalTable: "movies", principalColumn: "id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "fk_movie_watch_status_users_user_id", column: x => x.user_id, principalTable: "users", principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "show_watch_status", - columns: table => new - { - user_id = table.Column(type: "uuid", nullable: false), - show_id = table.Column(type: "uuid", nullable: false), - added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), - played_date = table.Column(type: "timestamp with time zone", nullable: true), - status = table.Column(type: "watch_status", nullable: false), - unseen_episodes_count = table.Column(type: "integer", nullable: false), - next_episode_id = table.Column(type: "uuid", nullable: true), - watched_time = table.Column(type: "integer", nullable: true), - watched_percent = table.Column(type: "integer", nullable: true) - }, + columns: table => + new + { + user_id = table.Column(type: "uuid", nullable: false), + show_id = table.Column(type: "uuid", nullable: false), + added_date = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now() at time zone 'utc'" + ), + played_date = table.Column( + type: "timestamp with time zone", + nullable: true + ), + status = table.Column(type: "watch_status", nullable: false), + unseen_episodes_count = table.Column(type: "integer", nullable: false), + next_episode_id = table.Column(type: "uuid", nullable: true), + watched_time = table.Column(type: "integer", nullable: true), + watched_percent = table.Column(type: "integer", nullable: true) + }, constraints: table => { table.PrimaryKey("pk_show_watch_status", x => new { x.user_id, x.show_id }); @@ -116,58 +156,70 @@ namespace Kyoo.Postgresql.Migrations name: "fk_show_watch_status_episodes_next_episode_id", column: x => x.next_episode_id, principalTable: "episodes", - principalColumn: "id"); + principalColumn: "id" + ); table.ForeignKey( name: "fk_show_watch_status_shows_show_id", column: x => x.show_id, principalTable: "shows", principalColumn: "id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "fk_show_watch_status_users_user_id", column: x => x.user_id, principalTable: "users", principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateIndex( name: "ix_episode_watch_status_episode_id", table: "episode_watch_status", - column: "episode_id"); + column: "episode_id" + ); migrationBuilder.CreateIndex( name: "ix_movie_watch_status_movie_id", table: "movie_watch_status", - column: "movie_id"); + column: "movie_id" + ); migrationBuilder.CreateIndex( name: "ix_show_watch_status_next_episode_id", table: "show_watch_status", - column: "next_episode_id"); + column: "next_episode_id" + ); migrationBuilder.CreateIndex( name: "ix_show_watch_status_show_id", table: "show_watch_status", - column: "show_id"); + column: "show_id" + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "episode_watch_status"); + migrationBuilder.DropTable(name: "episode_watch_status"); - migrationBuilder.DropTable( - name: "movie_watch_status"); + migrationBuilder.DropTable(name: "movie_watch_status"); - migrationBuilder.DropTable( - name: "show_watch_status"); + migrationBuilder.DropTable(name: "show_watch_status"); - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western") + migrationBuilder + .AlterDatabase() + .Annotation( + "Npgsql:Enum:genre", + "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western" + ) .Annotation("Npgsql:Enum: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:watch_status", "completed,watching,droped,planned"); } diff --git a/back/src/Kyoo.Postgresql/PostgresContext.cs b/back/src/Kyoo.Postgresql/PostgresContext.cs index 2ec33d15..895c7ff5 100644 --- a/back/src/Kyoo.Postgresql/PostgresContext.cs +++ b/back/src/Kyoo.Postgresql/PostgresContext.cs @@ -56,8 +56,7 @@ namespace Kyoo.Postgresql /// Design time constructor (dotnet ef migrations add). Do not use /// public PostgresContext() - : base(null!) - { } + : base(null!) { } public PostgresContext(DbContextOptions options, IHttpContextAccessor accessor) : base(options, accessor) @@ -98,16 +97,18 @@ namespace Kyoo.Postgresql modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); - modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!) - .HasTranslation(args => - new SqlFunctionExpression( - "md5", - args, - nullable: true, - argumentsPropagateNullability: new[] { false }, - type: args[0].Type, - typeMapping: args[0].TypeMapping - ) + modelBuilder + .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!) + .HasTranslation( + args => + new SqlFunctionExpression( + "md5", + args, + nullable: true, + argumentsPropagateNullability: new[] { false }, + type: args[0].Type, + typeMapping: args[0].TypeMapping + ) ); base.OnModelCreating(modelBuilder); @@ -130,7 +131,12 @@ namespace Kyoo.Postgresql /// 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 + }; } } } diff --git a/back/src/Kyoo.Postgresql/PostgresModule.cs b/back/src/Kyoo.Postgresql/PostgresModule.cs index 4c2e8779..888de7fa 100644 --- a/back/src/Kyoo.Postgresql/PostgresModule.cs +++ b/back/src/Kyoo.Postgresql/PostgresModule.cs @@ -79,14 +79,24 @@ namespace Kyoo.Postgresql SqlMapper.TypeMapProvider = (type) => { - return new CustomPropertyTypeMap(type, (type, name) => - { - 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 - return type.GetProperty(newName)!; - }); + return new CustomPropertyTypeMap( + type, + (type, name) => + { + 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 + return type.GetProperty(newName)!; + } + ); }; - SqlMapper.AddTypeHandler(typeof(Dictionary), new JsonTypeHandler>()); + SqlMapper.AddTypeHandler( + typeof(Dictionary), + new JsonTypeHandler>() + ); SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler()); SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler()); SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler()); @@ -97,26 +107,31 @@ namespace Kyoo.Postgresql /// public void Configure(IServiceCollection services) { - DbConnectionStringBuilder builder = new() - { - ["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"), - ["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"), - ["SERVER"] = _configuration.GetValue("POSTGRES_SERVER", "db"), - ["PORT"] = _configuration.GetValue("POSTGRES_PORT", "5432"), - ["DATABASE"] = _configuration.GetValue("POSTGRES_DB", "kyooDB"), - ["POOLING"] = "true", - ["MAXPOOLSIZE"] = "95", - ["TIMEOUT"] = "30" - }; + DbConnectionStringBuilder builder = + new() + { + ["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"), + ["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"), + ["SERVER"] = _configuration.GetValue("POSTGRES_SERVER", "db"), + ["PORT"] = _configuration.GetValue("POSTGRES_PORT", "5432"), + ["DATABASE"] = _configuration.GetValue("POSTGRES_DB", "kyooDB"), + ["POOLING"] = "true", + ["MAXPOOLSIZE"] = "95", + ["TIMEOUT"] = "30" + }; - services.AddDbContext(x => - { - x.UseNpgsql(builder.ConnectionString) - .UseProjectables(); - if (_environment.IsDevelopment()) - x.EnableDetailedErrors().EnableSensitiveDataLogging(); - }, ServiceLifetime.Transient); - services.AddTransient((_) => new NpgsqlConnection(builder.ConnectionString)); + services.AddDbContext( + x => + { + x.UseNpgsql(builder.ConnectionString).UseProjectables(); + if (_environment.IsDevelopment()) + x.EnableDetailedErrors().EnableSensitiveDataLogging(); + }, + ServiceLifetime.Transient + ); + services.AddTransient( + (_) => new NpgsqlConnection(builder.ConnectionString) + ); services.AddHealthChecks().AddDbContextCheck(); } diff --git a/back/src/Kyoo.Swagger/ApiSorter.cs b/back/src/Kyoo.Swagger/ApiSorter.cs index 233151a6..c924c61f 100644 --- a/back/src/Kyoo.Swagger/ApiSorter.cs +++ b/back/src/Kyoo.Swagger/ApiSorter.cs @@ -38,7 +38,8 @@ namespace Kyoo.Swagger options.PostProcess += postProcess => { // We can't reorder items by assigning the sorted value to the Paths variable since it has no setter. - List> sorted = postProcess.Paths + List> sorted = postProcess + .Paths .OrderBy(x => x.Key) .ToList(); postProcess.Paths.Clear(); @@ -56,9 +57,7 @@ namespace Kyoo.Swagger .Select(x => { x.Name = x.Name[(x.Name.IndexOf(':') + 1)..]; - x.Tags = x.Tags - .OrderBy(y => y) - .ToList(); + x.Tags = x.Tags.OrderBy(y => y).ToList(); return x; }) .ToList(); diff --git a/back/src/Kyoo.Swagger/ApiTagsFilter.cs b/back/src/Kyoo.Swagger/ApiTagsFilter.cs index f3537541..84c470c1 100644 --- a/back/src/Kyoo.Swagger/ApiTagsFilter.cs +++ b/back/src/Kyoo.Swagger/ApiTagsFilter.cs @@ -21,10 +21,10 @@ using System.Linq; using System.Reflection; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Swagger.Models; -using Namotion.Reflection; using NSwag; using NSwag.Generation.AspNetCore; using NSwag.Generation.Processors.Contexts; +using Namotion.Reflection; namespace Kyoo.Swagger { @@ -42,21 +42,30 @@ namespace Kyoo.Swagger /// This always return true since it should not remove operations. public static bool OperationFilter(OperationProcessorContext context) { - ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute(); + ApiDefinitionAttribute def = context + .ControllerType + .GetCustomAttribute(); string name = def?.Name ?? context.ControllerType.Name; - ApiDefinitionAttribute methodOverride = context.MethodInfo.GetCustomAttribute(); + ApiDefinitionAttribute methodOverride = context + .MethodInfo + .GetCustomAttribute(); if (methodOverride != null) name = methodOverride.Name; context.OperationDescription.Operation.Tags.Add(name); if (context.Document.Tags.All(x => x.Name != name)) { - context.Document.Tags.Add(new OpenApiTag - { - Name = name, - Description = context.ControllerType.GetXmlDocsSummary() - }); + context + .Document + .Tags + .Add( + new OpenApiTag + { + Name = name, + Description = context.ControllerType.GetXmlDocsSummary() + } + ); } if (def?.Group == null) @@ -73,11 +82,13 @@ namespace Kyoo.Swagger } else { - obj.Add(new TagGroups - { - Name = def.Group, - Tags = new List { def.Name } - }); + obj.Add( + new TagGroups + { + Name = def.Group, + Tags = new List { def.Name } + } + ); } return true; @@ -94,19 +105,14 @@ namespace Kyoo.Swagger public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess) { List tagGroups = (List)postProcess.ExtensionData["x-tagGroups"]; - List tagsWithoutGroup = postProcess.Tags + List tagsWithoutGroup = postProcess + .Tags .Select(x => x.Name) - .Where(x => tagGroups - .SelectMany(y => y.Tags) - .All(y => y != x)) + .Where(x => tagGroups.SelectMany(y => y.Tags).All(y => y != x)) .ToList(); if (tagsWithoutGroup.Any()) { - tagGroups.Add(new TagGroups - { - Name = "Others", - Tags = tagsWithoutGroup - }); + tagGroups.Add(new TagGroups { Name = "Others", Tags = tagsWithoutGroup }); } } diff --git a/back/src/Kyoo.Swagger/GenericResponseProvider.cs b/back/src/Kyoo.Swagger/GenericResponseProvider.cs index f9ebd37c..6490da24 100644 --- a/back/src/Kyoo.Swagger/GenericResponseProvider.cs +++ b/back/src/Kyoo.Swagger/GenericResponseProvider.cs @@ -41,22 +41,27 @@ namespace Kyoo.Swagger public int Order => -1; /// - public void OnProvidersExecuted(ApplicationModelProviderContext context) - { } + public void OnProvidersExecuted(ApplicationModelProviderContext context) { } /// public void OnProvidersExecuting(ApplicationModelProviderContext context) { foreach (ActionModel action in context.Result.Controllers.SelectMany(x => x.Actions)) { - IEnumerable responses = action.Filters + IEnumerable responses = action + .Filters .OfType() .Where(x => x.Type == typeof(ActionResult<>)); foreach (ProducesResponseTypeAttribute response in responses) { Type type = action.ActionMethod.ReturnType; - type = Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0] ?? type; - type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type; + type = + Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0] + ?? type; + type = + Utility + .GetGenericDefinition(type, typeof(ActionResult<>)) + ?.GetGenericArguments()[0] ?? type; response.Type = type; } } diff --git a/back/src/Kyoo.Swagger/Models/TagGroups.cs b/back/src/Kyoo.Swagger/Models/TagGroups.cs index d04df266..5ff7a0de 100644 --- a/back/src/Kyoo.Swagger/Models/TagGroups.cs +++ b/back/src/Kyoo.Swagger/Models/TagGroups.cs @@ -17,8 +17,8 @@ // along with Kyoo. If not, see . using System.Collections.Generic; -using Newtonsoft.Json; using NSwag; +using Newtonsoft.Json; namespace Kyoo.Swagger.Models { diff --git a/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs b/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs index 25781efb..d5ff8c13 100644 --- a/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs +++ b/back/src/Kyoo.Swagger/OperationPermissionProcessor.cs @@ -36,49 +36,69 @@ namespace Kyoo.Swagger /// public bool Process(OperationProcessorContext context) { - context.OperationDescription.Operation.Security ??= new List(); - OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes() - .Aggregate(new OpenApiSecurityRequirement(), (agg, _) => - { - agg[nameof(Kyoo)] = Array.Empty(); - return agg; - }); + context.OperationDescription.Operation.Security ??= + new List(); + OpenApiSecurityRequirement perms = context + .MethodInfo + .GetCustomAttributes() + .Aggregate( + new OpenApiSecurityRequirement(), + (agg, _) => + { + agg[nameof(Kyoo)] = Array.Empty(); + return agg; + } + ); - perms = context.MethodInfo.GetCustomAttributes() - .Aggregate(perms, (agg, cur) => - { - ICollection permissions = _GetPermissionsList(agg, cur.Group); - permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}"); - agg[nameof(Kyoo)] = permissions; - return agg; - }); + perms = context + .MethodInfo + .GetCustomAttributes() + .Aggregate( + perms, + (agg, cur) => + { + ICollection permissions = _GetPermissionsList(agg, cur.Group); + permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}"); + agg[nameof(Kyoo)] = permissions; + return agg; + } + ); - PartialPermissionAttribute controller = context.ControllerType + PartialPermissionAttribute controller = context + .ControllerType .GetCustomAttribute(); if (controller != null) { - perms = context.MethodInfo.GetCustomAttributes() - .Aggregate(perms, (agg, cur) => - { - Group? group = controller.Group != Group.Overall - ? controller.Group - : cur.Group; - string type = controller.Type ?? cur.Type; - Kind? kind = controller.Type == null - ? controller.Kind - : cur.Kind; - ICollection permissions = _GetPermissionsList(agg, group ?? Group.Overall); - permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}"); - agg[nameof(Kyoo)] = permissions; - return agg; - }); + perms = context + .MethodInfo + .GetCustomAttributes() + .Aggregate( + perms, + (agg, cur) => + { + Group? group = + controller.Group != Group.Overall ? controller.Group : cur.Group; + string type = controller.Type ?? cur.Type; + Kind? kind = controller.Type == null ? controller.Kind : cur.Kind; + ICollection permissions = _GetPermissionsList( + agg, + group ?? Group.Overall + ); + permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}"); + agg[nameof(Kyoo)] = permissions; + return agg; + } + ); } context.OperationDescription.Operation.Security.Add(perms); return true; } - private static ICollection _GetPermissionsList(OpenApiSecurityRequirement security, Group group) + private static ICollection _GetPermissionsList( + OpenApiSecurityRequirement security, + Group group + ) { return security.TryGetValue(group.ToString(), out IEnumerable perms) ? perms.ToList() diff --git a/back/src/Kyoo.Swagger/SwaggerModule.cs b/back/src/Kyoo.Swagger/SwaggerModule.cs index 84dfc5e5..b11c8d6c 100644 --- a/back/src/Kyoo.Swagger/SwaggerModule.cs +++ b/back/src/Kyoo.Swagger/SwaggerModule.cs @@ -78,39 +78,58 @@ namespace Kyoo.Swagger document.AddOperationFilter(x => { if (x is AspNetCoreOperationProcessorContext ctx) - return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute; + return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order + != AlternativeRoute; return true; }); - document.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x => - { - x.IsNullableRaw = false; - x.Type = JsonObjectType.String | JsonObjectType.Integer; - })); + document + .SchemaGenerator + .Settings + .TypeMappers + .Add( + new PrimitiveTypeMapper( + typeof(Identifier), + x => + { + x.IsNullableRaw = false; + x.Type = JsonObjectType.String | JsonObjectType.Integer; + } + ) + ); - document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.Http, - Scheme = "Bearer", - BearerFormat = "JWT", - Description = "The user's bearer" - }); + document.AddSecurity( + nameof(Kyoo), + new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.Http, + Scheme = "Bearer", + BearerFormat = "JWT", + Description = "The user's bearer" + } + ); document.OperationProcessors.Add(new OperationPermissionProcessor()); }); } /// - public IEnumerable ConfigureSteps => new IStartupAction[] - { - SA.New(app => app.UseOpenApi(), SA.Before + 1), - SA.New(app => app.UseReDoc(x => + public IEnumerable ConfigureSteps => + new IStartupAction[] { - x.Path = "/doc"; - x.TransformToExternalPath = (internalUiRoute, _) => "/api" + internalUiRoute; - x.AdditionalSettings["theme"] = new - { - colors = new { primary = new { main = "#e13e13" } } - }; - }), SA.Before) - }; + SA.New(app => app.UseOpenApi(), SA.Before + 1), + SA.New( + app => + app.UseReDoc(x => + { + x.Path = "/doc"; + x.TransformToExternalPath = (internalUiRoute, _) => + "/api" + internalUiRoute; + x.AdditionalSettings["theme"] = new + { + colors = new { primary = new { main = "#e13e13" } } + }; + }), + SA.Before + ) + }; } } diff --git a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs index 3912a4bf..1d741234 100644 --- a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs +++ b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs @@ -48,9 +48,12 @@ namespace Kyoo.Tests.Database Mock thumbs = new(); CollectionRepository collection = new(_NewContext(), thumbs.Object); StudioRepository studio = new(_NewContext(), thumbs.Object); - PeopleRepository people = new(_NewContext(), - new Lazy>(() => LibraryManager.Shows), - thumbs.Object); + PeopleRepository people = + new( + _NewContext(), + new Lazy>(() => LibraryManager.Shows), + thumbs.Object + ); MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object); ShowRepository show = new(_NewContext(), studio, people, thumbs.Object); SeasonRepository season = new(_NewContext(), thumbs.Object); diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs index 711b2a89..616807b3 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs @@ -72,16 +72,8 @@ namespace Kyoo.Tests.Database Collection collection = TestSample.GetNew(); collection.ExternalId = new Dictionary { - ["1"] = new() - { - Link = "link", - DataId = "id" - }, - ["2"] = new() - { - Link = "new-provider-link", - DataId = "new-id" - } + ["1"] = new() { Link = "link", DataId = "id" }, + ["2"] = new() { Link = "new-provider-link", DataId = "new-id" } }; await _repository.Create(collection); @@ -111,11 +103,7 @@ namespace Kyoo.Tests.Database Collection value = await _repository.Get(TestSample.Get().Slug); value.ExternalId = new Dictionary { - ["test"] = new() - { - Link = "link", - DataId = "id" - }, + ["test"] = new() { Link = "link", DataId = "id" }, }; await _repository.Edit(value); @@ -131,11 +119,7 @@ namespace Kyoo.Tests.Database Collection value = await _repository.Get(TestSample.Get().Slug); value.ExternalId = new Dictionary { - ["toto"] = new() - { - Link = "link", - DataId = "id" - }, + ["toto"] = new() { Link = "link", DataId = "id" }, }; await _repository.Edit(value); @@ -146,11 +130,7 @@ namespace Kyoo.Tests.Database KAssert.DeepEqual(value, retrieved); } - value.ExternalId.Add("test", new MetadataId - { - Link = "link", - DataId = "id" - }); + value.ExternalId.Add("test", new MetadataId { Link = "link", DataId = "id" }); await _repository.Edit(value); { @@ -169,11 +149,7 @@ namespace Kyoo.Tests.Database [InlineData("SuPeR")] public async Task SearchTest(string query) { - Collection value = new() - { - Slug = "super-test", - Name = "This is a test title", - }; + Collection value = new() { Slug = "super-test", Name = "This is a test title", }; await _repository.Create(value); ICollection ret = await _repository.Search(query); KAssert.DeepEqual(value, ret.First()); diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs index 32e8ec0a..b28748a9 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs @@ -46,7 +46,6 @@ namespace Kyoo.Tests.Database protected AEpisodeTests(RepositoryActivator repositories) : base(repositories) { - _repository = repositories.LibraryManager.Episodes; } @@ -55,11 +54,17 @@ namespace Kyoo.Tests.Database { Episode episode = await _repository.Get(1.AsGuid()); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - await Repositories.LibraryManager.Shows.Patch(episode.ShowId, (x) => - { - x.Slug = "new-slug"; - return Task.FromResult(true); - }); + await Repositories + .LibraryManager + .Shows + .Patch( + episode.ShowId, + (x) => + { + x.Slug = "new-slug"; + return Task.FromResult(true); + } + ); episode = await _repository.Get(1.AsGuid()); Assert.Equal("new-slug-s1e1", episode.Slug); } @@ -69,11 +74,14 @@ namespace Kyoo.Tests.Database { Episode episode = await _repository.Get(1.AsGuid()); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - episode = await _repository.Patch(1.AsGuid(), (x) => - { - x.SeasonNumber = 2; - return Task.FromResult(true); - }); + episode = await _repository.Patch( + 1.AsGuid(), + (x) => + { + x.SeasonNumber = 2; + return Task.FromResult(true); + } + ); Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); episode = await _repository.Get(1.AsGuid()); Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); @@ -84,11 +92,17 @@ namespace Kyoo.Tests.Database { Episode episode = await _repository.Get(1.AsGuid()); Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); - episode = await Repositories.LibraryManager.Episodes.Patch(episode.Id, (x) => - { - x.EpisodeNumber = 2; - return Task.FromResult(true); - }); + episode = await Repositories + .LibraryManager + .Episodes + .Patch( + episode.Id, + (x) => + { + x.EpisodeNumber = 2; + return Task.FromResult(true); + } + ); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); episode = await _repository.Get(1.AsGuid()); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); @@ -109,26 +123,37 @@ namespace Kyoo.Tests.Database [Fact] public void AbsoluteSlugTest() { - Assert.Equal($"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", - TestSample.GetAbsoluteEpisode().Slug); + Assert.Equal( + $"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", + TestSample.GetAbsoluteEpisode().Slug + ); } [Fact] public async Task EpisodeCreationAbsoluteSlugTest() { Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); - Assert.Equal($"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", episode.Slug); + Assert.Equal( + $"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", + episode.Slug + ); } [Fact] public async Task SlugEditAbsoluteTest() { Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); - await Repositories.LibraryManager.Shows.Patch(episode.ShowId, (x) => - { - x.Slug = "new-slug"; - return Task.FromResult(true); - }); + await Repositories + .LibraryManager + .Shows + .Patch( + episode.ShowId, + (x) => + { + x.Slug = "new-slug"; + return Task.FromResult(true); + } + ); episode = await _repository.Get(2.AsGuid()); Assert.Equal($"new-slug-3", episode.Slug); } @@ -137,11 +162,14 @@ namespace Kyoo.Tests.Database public async Task AbsoluteNumberEditTest() { await _repository.Create(TestSample.GetAbsoluteEpisode()); - Episode episode = await _repository.Patch(2.AsGuid(), (x) => - { - x.AbsoluteNumber = 56; - return Task.FromResult(true); - }); + Episode episode = await _repository.Patch( + 2.AsGuid(), + (x) => + { + x.AbsoluteNumber = 56; + return Task.FromResult(true); + } + ); Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); episode = await _repository.Get(2.AsGuid()); Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); @@ -151,12 +179,15 @@ namespace Kyoo.Tests.Database public async Task AbsoluteToNormalEditTest() { await _repository.Create(TestSample.GetAbsoluteEpisode()); - Episode episode = await _repository.Patch(2.AsGuid(), (x) => - { - x.SeasonNumber = 1; - x.EpisodeNumber = 2; - return Task.FromResult(true); - }); + Episode episode = await _repository.Patch( + 2.AsGuid(), + (x) => + { + x.SeasonNumber = 1; + x.EpisodeNumber = 2; + return Task.FromResult(true); + } + ); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); episode = await _repository.Get(2.AsGuid()); Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); @@ -180,16 +211,8 @@ namespace Kyoo.Tests.Database Episode value = TestSample.GetNew(); value.ExternalId = new Dictionary { - ["2"] = new() - { - Link = "link", - DataId = "id" - }, - ["3"] = new() - { - Link = "new-provider-link", - DataId = "new-id" - } + ["2"] = new() { Link = "link", DataId = "id" }, + ["3"] = new() { Link = "new-provider-link", DataId = "new-id" } }; await _repository.Create(value); @@ -219,11 +242,7 @@ namespace Kyoo.Tests.Database Episode value = await _repository.Get(TestSample.Get().Slug); value.ExternalId = new Dictionary { - ["1"] = new() - { - Link = "link", - DataId = "id" - }, + ["1"] = new() { Link = "link", DataId = "id" }, }; await _repository.Edit(value); @@ -239,11 +258,7 @@ namespace Kyoo.Tests.Database Episode value = await _repository.Get(TestSample.Get().Slug); value.ExternalId = new Dictionary { - ["toto"] = new() - { - Link = "link", - DataId = "id" - }, + ["toto"] = new() { Link = "link", DataId = "id" }, }; await _repository.Edit(value); @@ -254,11 +269,7 @@ namespace Kyoo.Tests.Database KAssert.DeepEqual(value, retrieved); } - value.ExternalId.Add("test", new MetadataId - { - Link = "link", - DataId = "id" - }); + value.ExternalId.Add("test", new MetadataId { Link = "link", DataId = "id" }); await _repository.Edit(value); { @@ -289,13 +300,19 @@ namespace Kyoo.Tests.Database [Fact] public async Task CreateTest() { - await Assert.ThrowsAsync(() => _repository.Create(TestSample.Get())); + await Assert.ThrowsAsync( + () => _repository.Create(TestSample.Get()) + ); await _repository.Delete(TestSample.Get()); Episode expected = TestSample.Get(); expected.Id = 0.AsGuid(); - expected.ShowId = (await Repositories.LibraryManager.Shows.Create(TestSample.Get())).Id; - expected.SeasonId = (await Repositories.LibraryManager.Seasons.Create(TestSample.Get())).Id; + expected.ShowId = ( + await Repositories.LibraryManager.Shows.Create(TestSample.Get()) + ).Id; + expected.SeasonId = ( + await Repositories.LibraryManager.Seasons.Create(TestSample.Get()) + ).Id; await _repository.Create(expected); KAssert.DeepEqual(expected, await _repository.Get(expected.Slug)); } @@ -304,10 +321,17 @@ namespace Kyoo.Tests.Database public override async Task CreateIfNotExistTest() { Episode expected = TestSample.Get(); - KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get())); + KAssert.DeepEqual( + expected, + await _repository.CreateIfNotExists(TestSample.Get()) + ); await _repository.Delete(TestSample.Get()); - expected.ShowId = (await Repositories.LibraryManager.Shows.Create(TestSample.Get())).Id; - expected.SeasonId = (await Repositories.LibraryManager.Seasons.Create(TestSample.Get())).Id; + expected.ShowId = ( + await Repositories.LibraryManager.Shows.Create(TestSample.Get()) + ).Id; + expected.SeasonId = ( + await Repositories.LibraryManager.Seasons.Create(TestSample.Get()) + ).Id; KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected)); } } diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs index d3cf272d..06b02a3d 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs @@ -53,11 +53,17 @@ namespace Kyoo.Tests.Database { Season season = await _repository.Get(1.AsGuid()); Assert.Equal("anohana-s1", season.Slug); - await Repositories.LibraryManager.Shows.Patch(season.ShowId, (x) => - { - x.Slug = "new-slug"; - return Task.FromResult(true); - }); + await Repositories + .LibraryManager + .Shows + .Patch( + season.ShowId, + (x) => + { + x.Slug = "new-slug"; + return Task.FromResult(true); + } + ); season = await _repository.Get(1.AsGuid()); Assert.Equal("new-slug-s1", season.Slug); } @@ -67,11 +73,13 @@ namespace Kyoo.Tests.Database { Season season = await _repository.Get(1.AsGuid()); Assert.Equal("anohana-s1", season.Slug); - await _repository.Patch(season.Id, (x) => - { - x.SeasonNumber = 2; - return Task.FromResult(true); - } + await _repository.Patch( + season.Id, + (x) => + { + x.SeasonNumber = 2; + return Task.FromResult(true); + } ); season = await _repository.Get(1.AsGuid()); Assert.Equal("anohana-s2", season.Slug); @@ -80,11 +88,9 @@ namespace Kyoo.Tests.Database [Fact] public async Task SeasonCreationSlugTest() { - Season season = await _repository.Create(new Season - { - ShowId = TestSample.Get().Id, - SeasonNumber = 2 - }); + Season season = await _repository.Create( + new Season { ShowId = TestSample.Get().Id, SeasonNumber = 2 } + ); Assert.Equal($"{TestSample.Get().Slug}-s2", season.Slug); } @@ -94,16 +100,8 @@ namespace Kyoo.Tests.Database Season season = TestSample.GetNew(); season.ExternalId = new Dictionary { - ["2"] = new() - { - Link = "link", - DataId = "id" - }, - ["1"] = new() - { - Link = "new-provider-link", - DataId = "new-id" - } + ["2"] = new() { Link = "link", DataId = "id" }, + ["1"] = new() { Link = "new-provider-link", DataId = "new-id" } }; await _repository.Create(season); @@ -133,11 +131,7 @@ namespace Kyoo.Tests.Database Season value = await _repository.Get(TestSample.Get().Slug); value.ExternalId = new Dictionary { - ["toto"] = new() - { - Link = "link", - DataId = "id" - }, + ["toto"] = new() { Link = "link", DataId = "id" }, }; await _repository.Edit(value); @@ -153,11 +147,7 @@ namespace Kyoo.Tests.Database Season value = await _repository.Get(TestSample.Get().Slug); value.ExternalId = new Dictionary { - ["1"] = new() - { - Link = "link", - DataId = "id" - }, + ["1"] = new() { Link = "link", DataId = "id" }, }; await _repository.Edit(value); @@ -168,11 +158,7 @@ namespace Kyoo.Tests.Database KAssert.DeepEqual(value, retrieved); } - value.ExternalId.Add("toto", new MetadataId - { - Link = "link", - DataId = "id" - }); + value.ExternalId.Add("toto", new MetadataId { Link = "link", DataId = "id" }); await _repository.Edit(value); { @@ -191,11 +177,7 @@ namespace Kyoo.Tests.Database [InlineData("SuPeR")] public async Task SearchTest(string query) { - Season value = new() - { - Name = "This is a test super title", - ShowId = 1.AsGuid() - }; + Season value = new() { Name = "This is a test super title", ShowId = 1.AsGuid() }; await _repository.Create(value); ICollection ret = await _repository.Search(query); KAssert.DeepEqual(value, ret.First()); diff --git a/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs b/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs index 2584b105..c9050174 100644 --- a/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs +++ b/back/tests/Kyoo.Tests/Database/SpecificTests/ShowTests.cs @@ -172,10 +172,7 @@ namespace Kyoo.Tests.Database Show value = await _repository.Get(TestSample.Get().Slug); value.ExternalId = new Dictionary() { - ["test"] = new() - { - DataId = "1234" - } + ["test"] = new() { DataId = "1234" } }; Show edited = await _repository.Edit(value); @@ -197,10 +194,7 @@ namespace Kyoo.Tests.Database expected.Slug = "created-relation-test"; expected.ExternalId = new Dictionary { - ["test"] = new() - { - DataId = "ID" - } + ["test"] = new() { DataId = "ID" } }; expected.Genres = new List() { Genre.Action }; // expected.People = new[] @@ -219,7 +213,8 @@ namespace Kyoo.Tests.Database KAssert.DeepEqual(expected, created); await using DatabaseContext context = Repositories.Context.New(); - Show retrieved = await context.Shows + Show retrieved = await context + .Shows // .Include(x => x.People) // .ThenInclude(x => x.People) .Include(x => x.Studio) @@ -253,10 +248,7 @@ namespace Kyoo.Tests.Database expected.Slug = "created-relation-test"; expected.ExternalId = new Dictionary { - ["test"] = new() - { - DataId = "ID" - } + ["test"] = new() { DataId = "ID" } }; Show created = await _repository.Create(expected); KAssert.DeepEqual(expected, created); @@ -285,11 +277,7 @@ namespace Kyoo.Tests.Database [InlineData("SuPeR")] public async Task SearchTest(string query) { - Show value = new() - { - Slug = "super-test", - Name = "This is a test title?" - }; + Show value = new() { Slug = "super-test", Name = "This is a test title?" }; await _repository.Create(value); ICollection ret = await _repository.Search(query); KAssert.DeepEqual(value, ret.First()); diff --git a/back/tests/Kyoo.Tests/Database/TestContext.cs b/back/tests/Kyoo.Tests/Database/TestContext.cs index baab6954..0e750b26 100644 --- a/back/tests/Kyoo.Tests/Database/TestContext.cs +++ b/back/tests/Kyoo.Tests/Database/TestContext.cs @@ -29,8 +29,7 @@ using Xunit.Abstractions; namespace Kyoo.Tests { [CollectionDefinition(nameof(Postgresql))] - public class PostgresCollection : ICollectionFixture - { } + public class PostgresCollection : ICollectionFixture { } public sealed class PostgresFixture : IDisposable { @@ -45,9 +44,7 @@ namespace Kyoo.Tests string id = Guid.NewGuid().ToString().Replace('-', '_'); Template = $"kyoo_template_{id}"; - _options = new DbContextOptionsBuilder() - .UseNpgsql(Connection) - .Options; + _options = new DbContextOptionsBuilder().UseNpgsql(Connection).Options; using PostgresContext context = new(_options, null); context.Database.Migrate(); @@ -80,17 +77,23 @@ namespace Kyoo.Tests using (NpgsqlConnection connection = new(template.Connection)) { 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(); } _context = new DbContextOptionsBuilder() .UseNpgsql(GetConnectionString(_database)) - .UseLoggerFactory(LoggerFactory.Create(x => - { - x.ClearProviders(); - x.AddXunit(output); - })) + .UseLoggerFactory( + LoggerFactory.Create(x => + { + x.ClearProviders(); + x.AddXunit(output); + }) + ) .EnableSensitiveDataLogging() .EnableDetailedErrors() .Options; @@ -101,7 +104,8 @@ namespace Kyoo.Tests string server = Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "127.0.0.1"; string port = Environment.GetEnvironmentVariable("POSTGRES_PORT") ?? "5432"; string username = Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "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"; } diff --git a/back/tests/Kyoo.Tests/Database/TestSample.cs b/back/tests/Kyoo.Tests/Database/TestSample.cs index 7272fbd7..e4728b46 100644 --- a/back/tests/Kyoo.Tests/Database/TestSample.cs +++ b/back/tests/Kyoo.Tests/Database/TestSample.cs @@ -25,192 +25,207 @@ namespace Kyoo.Tests { public static class TestSample { - private static readonly Dictionary> NewSamples = new() - { + private static readonly Dictionary> NewSamples = + new() { - typeof(Collection), - () => new Collection { - Id = 2.AsGuid(), - Slug = "new-collection", - Name = "New Collection", - Overview = "A collection created by new sample", - Thumbnail = new Image("thumbnail") - } - }, - { - typeof(Show), - () => new Show + typeof(Collection), + () => + new Collection + { + Id = 2.AsGuid(), + Slug = "new-collection", + Name = "New Collection", + Overview = "A collection created by new sample", + Thumbnail = new Image("thumbnail") + } + }, { - Id = 2.AsGuid(), - Slug = "new-show", - Name = "New Show", - Overview = "overview", - Status = Status.Planned, - StartAir = new DateTime(2011, 1, 1).ToUniversalTime(), - EndAir = new DateTime(2011, 1, 1).ToUniversalTime(), - Poster = new Image("Poster"), - Logo = new Image("Logo"), - Thumbnail = new Image("Thumbnail"), - Studio = null - } - }, - { - typeof(Season), - () => new Season + typeof(Show), + () => + new Show + { + Id = 2.AsGuid(), + Slug = "new-show", + Name = "New Show", + Overview = "overview", + Status = Status.Planned, + StartAir = new DateTime(2011, 1, 1).ToUniversalTime(), + EndAir = new DateTime(2011, 1, 1).ToUniversalTime(), + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail"), + Studio = null + } + }, { - Id = 2.AsGuid(), - ShowId = 1.AsGuid(), - ShowSlug = Get().Slug, - Name = "New season", - Overview = "New overview", - EndDate = new DateTime(2000, 10, 10).ToUniversalTime(), - SeasonNumber = 2, - StartDate = new DateTime(2010, 10, 10).ToUniversalTime(), - Logo = new Image("logo") - } - }, - { - typeof(Episode), - () => new Episode + typeof(Season), + () => + new Season + { + Id = 2.AsGuid(), + ShowId = 1.AsGuid(), + ShowSlug = Get().Slug, + Name = "New season", + Overview = "New overview", + EndDate = new DateTime(2000, 10, 10).ToUniversalTime(), + SeasonNumber = 2, + StartDate = new DateTime(2010, 10, 10).ToUniversalTime(), + Logo = new Image("logo") + } + }, { - Id = 2.AsGuid(), - ShowId = 1.AsGuid(), - ShowSlug = Get().Slug, - SeasonId = 1.AsGuid(), - SeasonNumber = Get().SeasonNumber, - EpisodeNumber = 3, - AbsoluteNumber = 4, - Path = "/episode-path", - Name = "New Episode Title", - ReleaseDate = new DateTime(2000, 10, 10).ToUniversalTime(), - Overview = "new episode overview", - Logo = new Image("new episode logo") - } - }, - { - typeof(People), - () => new People + typeof(Episode), + () => + new Episode + { + Id = 2.AsGuid(), + ShowId = 1.AsGuid(), + ShowSlug = Get().Slug, + SeasonId = 1.AsGuid(), + SeasonNumber = Get().SeasonNumber, + EpisodeNumber = 3, + AbsoluteNumber = 4, + Path = "/episode-path", + Name = "New Episode Title", + ReleaseDate = new DateTime(2000, 10, 10).ToUniversalTime(), + Overview = "new episode overview", + Logo = new Image("new episode logo") + } + }, { - Id = 2.AsGuid(), - Slug = "new-person-name", - Name = "New person name", - Logo = new Image("Old Logo"), - Poster = new Image("Old poster") + typeof(People), + () => + new People + { + Id = 2.AsGuid(), + Slug = "new-person-name", + Name = "New person name", + Logo = new Image("Old Logo"), + Poster = new Image("Old poster") + } } - } - }; + }; - private static readonly Dictionary> Samples = new() - { + private static readonly Dictionary> Samples = + new() { - typeof(Collection), - () => new Collection { - Id = 1.AsGuid(), - Slug = "collection", - Name = "Collection", - Overview = "A nice collection for tests", - Poster = new Image("Poster") - } - }, - { - typeof(Show), - () => new Show + typeof(Collection), + () => + new Collection + { + Id = 1.AsGuid(), + Slug = "collection", + Name = "Collection", + Overview = "A nice collection for tests", + Poster = new Image("Poster") + } + }, { - Id = 1.AsGuid(), - Slug = "anohana", - Name = "Anohana: The Flower We Saw That Day", - Aliases = new List - { - "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", - "AnoHana", - "We Still Don't Know the Name of the Flower We Saw That Day." - }, - Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + - "In time, however, these childhood friends drifted apart, and when they became high " + - "school students, they had long ceased to think of each other as friends.", - Status = Status.Finished, - StudioId = 1.AsGuid(), - StartAir = new DateTime(2011, 1, 1).ToUniversalTime(), - EndAir = new DateTime(2011, 1, 1).ToUniversalTime(), - Poster = new Image("Poster"), - Logo = new Image("Logo"), - Thumbnail = new Image("Thumbnail"), - Studio = null - } - }, - { - typeof(Season), - () => new Season + typeof(Show), + () => + new Show + { + Id = 1.AsGuid(), + Slug = "anohana", + Name = "Anohana: The Flower We Saw That Day", + Aliases = new List + { + "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", + "AnoHana", + "We Still Don't Know the Name of the Flower We Saw That Day." + }, + Overview = + "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + + "In time, however, these childhood friends drifted apart, and when they became high " + + "school students, they had long ceased to think of each other as friends.", + Status = Status.Finished, + StudioId = 1.AsGuid(), + StartAir = new DateTime(2011, 1, 1).ToUniversalTime(), + EndAir = new DateTime(2011, 1, 1).ToUniversalTime(), + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail"), + Studio = null + } + }, { - Id = 1.AsGuid(), - ShowSlug = "anohana", - ShowId = 1.AsGuid(), - SeasonNumber = 1, - Name = "Season 1", - Overview = "The first season", - StartDate = new DateTime(2020, 06, 05).ToUniversalTime(), - EndDate = new DateTime(2020, 07, 05).ToUniversalTime(), - Poster = new Image("Poster"), - Logo = new Image("Logo"), - Thumbnail = new Image("Thumbnail") - } - }, - { - typeof(Episode), - () => new Episode + typeof(Season), + () => + new Season + { + Id = 1.AsGuid(), + ShowSlug = "anohana", + ShowId = 1.AsGuid(), + SeasonNumber = 1, + Name = "Season 1", + Overview = "The first season", + StartDate = new DateTime(2020, 06, 05).ToUniversalTime(), + EndDate = new DateTime(2020, 07, 05).ToUniversalTime(), + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail") + } + }, { - Id = 1.AsGuid(), - ShowSlug = "anohana", - ShowId = 1.AsGuid(), - SeasonId = 1.AsGuid(), - SeasonNumber = 1, - EpisodeNumber = 1, - AbsoluteNumber = 1, - Path = "/home/kyoo/anohana-s1e1", - Poster = new Image("Poster"), - Logo = new Image("Logo"), - Thumbnail = new Image("Thumbnail"), - Name = "Episode 1", - Overview = "Summary of the first episode", - ReleaseDate = new DateTime(2020, 06, 05).ToUniversalTime() - } - }, - { - typeof(People), - () => new People + typeof(Episode), + () => + new Episode + { + Id = 1.AsGuid(), + ShowSlug = "anohana", + ShowId = 1.AsGuid(), + SeasonId = 1.AsGuid(), + SeasonNumber = 1, + EpisodeNumber = 1, + AbsoluteNumber = 1, + Path = "/home/kyoo/anohana-s1e1", + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail"), + Name = "Episode 1", + Overview = "Summary of the first episode", + ReleaseDate = new DateTime(2020, 06, 05).ToUniversalTime() + } + }, { - Id = 1.AsGuid(), - Slug = "the-actor", - Name = "The Actor", - Poster = new Image("Poster"), - Logo = new Image("Logo"), - Thumbnail = new Image("Thumbnail") - } - }, - { - typeof(Studio), - () => new Studio + typeof(People), + () => + new People + { + Id = 1.AsGuid(), + Slug = "the-actor", + Name = "The Actor", + Poster = new Image("Poster"), + Logo = new Image("Logo"), + Thumbnail = new Image("Thumbnail") + } + }, { - Id = 1.AsGuid(), - Slug = "hyper-studio", - Name = "Hyper studio", - } - }, - { - typeof(User), - () => new User + typeof(Studio), + () => + new Studio + { + Id = 1.AsGuid(), + Slug = "hyper-studio", + Name = "Hyper studio", + } + }, { - Id = 1.AsGuid(), - Slug = "user", - Username = "User", - Email = "user@im-a-user.com", - Password = "MD5-encoded", - Permissions = new[] { "overall.read" } + typeof(User), + () => + new User + { + Id = 1.AsGuid(), + Slug = "user", + Username = "User", + Email = "user@im-a-user.com", + Password = "MD5-encoded", + Permissions = new[] { "overall.read" } + } } - } - }; + }; public static T Get() { diff --git a/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs b/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs index 80a55a6f..69aeb3ae 100644 --- a/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs +++ b/back/tests/Kyoo.Tests/Utility/EnumerableTests.cs @@ -29,9 +29,12 @@ namespace Kyoo.Tests.Utility public void IfEmptyTest() { 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(); - Assert.Throws(() => list.IfEmpty(() => throw new ArgumentException()).ToList()); + Assert.Throws( + () => list.IfEmpty(() => throw new ArgumentException()).ToList() + ); Assert.Empty(list.IfEmpty(() => { })); } } diff --git a/back/tests/Kyoo.Tests/Utility/MergerTests.cs b/back/tests/Kyoo.Tests/Utility/MergerTests.cs index ab6a4202..2e214e5d 100644 --- a/back/tests/Kyoo.Tests/Utility/MergerTests.cs +++ b/back/tests/Kyoo.Tests/Utility/MergerTests.cs @@ -29,15 +29,8 @@ namespace Kyoo.Tests.Utility [Fact] public void CompleteTest() { - Studio genre = new() - { - Name = "merged" - }; - Studio genre2 = new() - { - Name = "test", - Id = 5.AsGuid(), - }; + Studio genre = new() { Name = "merged" }; + Studio genre2 = new() { Name = "test", Id = 5.AsGuid(), }; Studio ret = Merger.Complete(genre, genre2); Assert.True(ReferenceEquals(genre, ret)); Assert.Equal(5.AsGuid(), ret.Id); @@ -48,15 +41,8 @@ namespace Kyoo.Tests.Utility [Fact] public void CompleteDictionaryTest() { - Collection collection = new() - { - Name = "merged", - }; - Collection collection2 = new() - { - Id = 5.AsGuid(), - Name = "test", - }; + Collection collection = new() { Name = "merged", }; + Collection collection2 = new() { Id = 5.AsGuid(), Name = "test", }; Collection ret = Merger.Complete(collection, collection2); Assert.True(ReferenceEquals(collection, ret)); Assert.Equal(5.AsGuid(), ret.Id); @@ -67,17 +53,14 @@ namespace Kyoo.Tests.Utility [Fact] public void CompleteDictionaryOutParam() { - Dictionary first = new() - { - ["logo"] = "logo", - ["poster"] = "poster" - }; - Dictionary second = new() - { - ["poster"] = "new-poster", - ["thumbnail"] = "thumbnails" - }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + Dictionary first = new() { ["logo"] = "logo", ["poster"] = "poster" }; + Dictionary second = + new() { ["poster"] = "new-poster", ["thumbnail"] = "thumbnails" }; + IDictionary ret = Merger.CompleteDictionaries( + first, + second, + out bool changed + ); Assert.True(changed); Assert.Equal(3, ret.Count); Assert.Equal("new-poster", ret["poster"]); @@ -88,15 +71,13 @@ namespace Kyoo.Tests.Utility [Fact] public void CompleteDictionaryEqualTest() { - Dictionary first = new() - { - ["poster"] = "poster" - }; - Dictionary second = new() - { - ["poster"] = "new-poster", - }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + Dictionary first = new() { ["poster"] = "poster" }; + Dictionary second = new() { ["poster"] = "new-poster", }; + IDictionary ret = Merger.CompleteDictionaries( + first, + second, + out bool changed + ); Assert.True(changed); Assert.Single(ret); Assert.Equal("new-poster", ret["poster"]); @@ -121,17 +102,8 @@ namespace Kyoo.Tests.Utility [Fact] public void CompleteDictionaryNoChangeNoSetTest() { - TestMergeSetter first = new() - { - Backing = new Dictionary - { - [2] = 3 - } - }; - TestMergeSetter second = new() - { - Backing = new Dictionary() - }; + TestMergeSetter first = new() { Backing = new Dictionary { [2] = 3 } }; + TestMergeSetter second = new() { Backing = new Dictionary() }; Merger.Complete(first, second); // This should no call the setter of first so the test should pass. } @@ -139,17 +111,14 @@ namespace Kyoo.Tests.Utility [Fact] public void CompleteDictionaryNullValue() { - Dictionary first = new() - { - ["logo"] = "logo", - ["poster"] = null - }; - Dictionary second = new() - { - ["poster"] = "new-poster", - ["thumbnail"] = "thumbnails" - }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + Dictionary first = new() { ["logo"] = "logo", ["poster"] = null }; + Dictionary second = + new() { ["poster"] = "new-poster", ["thumbnail"] = "thumbnails" }; + IDictionary ret = Merger.CompleteDictionaries( + first, + second, + out bool changed + ); Assert.True(changed); Assert.Equal(3, ret.Count); Assert.Equal("new-poster", ret["poster"]); @@ -160,16 +129,13 @@ namespace Kyoo.Tests.Utility [Fact] public void CompleteDictionaryNullValueNoChange() { - Dictionary first = new() - { - ["logo"] = "logo", - ["poster"] = null - }; - Dictionary second = new() - { - ["poster"] = null, - }; - IDictionary ret = Merger.CompleteDictionaries(first, second, out bool changed); + Dictionary first = new() { ["logo"] = "logo", ["poster"] = null }; + Dictionary second = new() { ["poster"] = null, }; + IDictionary ret = Merger.CompleteDictionaries( + first, + second, + out bool changed + ); Assert.False(changed); Assert.Equal(2, ret.Count); Assert.Null(ret["poster"]); diff --git a/back/tests/Kyoo.Tests/Utility/UtilityTests.cs b/back/tests/Kyoo.Tests/Utility/UtilityTests.cs index fb7ac30e..137591e5 100644 --- a/back/tests/Kyoo.Tests/Utility/UtilityTests.cs +++ b/back/tests/Kyoo.Tests/Utility/UtilityTests.cs @@ -21,7 +21,6 @@ using System.Linq.Expressions; using System.Reflection; using Kyoo.Abstractions.Models; using Xunit; - using KUtility = Kyoo.Utils.Utility; namespace Kyoo.Tests.Utility @@ -54,32 +53,44 @@ namespace Kyoo.Tests.Utility [Fact] public void GetMethodTest() { - MethodInfo method = KUtility.GetMethod(typeof(UtilityTests), + MethodInfo method = KUtility.GetMethod( + typeof(UtilityTests), BindingFlags.Instance | BindingFlags.Public, nameof(GetMethodTest), Array.Empty(), - Array.Empty()); + Array.Empty() + ); Assert.Equal(MethodBase.GetCurrentMethod(), method); } [Fact] public void GetMethodInvalidGenericsTest() { - Assert.Throws(() => KUtility.GetMethod(typeof(UtilityTests), - BindingFlags.Instance | BindingFlags.Public, - nameof(GetMethodTest), - new[] { typeof(KUtility) }, - Array.Empty())); + Assert.Throws( + () => + KUtility.GetMethod( + typeof(UtilityTests), + BindingFlags.Instance | BindingFlags.Public, + nameof(GetMethodTest), + new[] { typeof(KUtility) }, + Array.Empty() + ) + ); } [Fact] public void GetMethodInvalidParamsTest() { - Assert.Throws(() => KUtility.GetMethod(typeof(UtilityTests), - BindingFlags.Instance | BindingFlags.Public, - nameof(GetMethodTest), - Array.Empty(), - new object[] { this })); + Assert.Throws( + () => + KUtility.GetMethod( + typeof(UtilityTests), + BindingFlags.Instance | BindingFlags.Public, + nameof(GetMethodTest), + Array.Empty(), + new object[] { this } + ) + ); } } }