From 177391a74ccb3e77d836353c85822f556d276106 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 19 Nov 2023 21:18:44 +0100 Subject: [PATCH] Rework default sort and make it work with dapper --- .../Controllers/ILibraryManager.cs | 2 +- .../Controllers/IRepository.cs | 2 +- .../{OneOf.cs => OneOfAttribute.cs} | 0 .../Kyoo.Abstractions/Models/ILibraryItem.cs | 6 +- back/src/Kyoo.Abstractions/Models/News.cs | 2 +- .../Models/Resources/Collection.cs | 5 +- .../Models/Resources/Episode.cs | 10 ++- .../Models/Resources/Interfaces/IQuery.cs | 30 +++++++++ .../Models/Resources/Movie.cs | 5 +- .../Models/Resources/People.cs | 5 +- .../Models/Resources/Season.cs | 5 +- .../Models/Resources/Show.cs | 8 ++- .../Models/Resources/Studio.cs | 5 +- .../Models/Resources/User.cs | 5 +- .../Kyoo.Abstractions/Models/Utils/Include.cs | 9 ++- .../Kyoo.Abstractions/Models/Utils/Sort.cs | 21 ++++++- back/src/Kyoo.Abstractions/Utility/Utility.cs | 63 +++++++++++++++++++ .../Kyoo.Core/Controllers/LibraryManager.cs | 2 +- .../Repositories/CollectionRepository.cs | 13 ++-- .../Repositories/EpisodeRepository.cs | 14 +---- .../Repositories/LibraryItemRepository.cs | 38 ++++++++--- .../Repositories/LocalRepository.cs | 19 +++--- .../Repositories/MovieRepository.cs | 9 +-- .../Repositories/NewsRepository.cs | 3 - .../Repositories/PeopleRepository.cs | 9 +-- .../Repositories/SeasonRepository.cs | 9 +-- .../Repositories/ShowRepository.cs | 9 +-- .../Repositories/StudioRepository.cs | 9 +-- .../Repositories/UserRepository.cs | 9 +-- back/src/Kyoo.Core/Views/Helper/CrudApi.cs | 2 +- .../Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 2 +- .../Serializers/JsonSerializerContract.cs | 2 +- back/src/Kyoo.Core/Views/Helper/SortBinder.cs | 3 +- back/src/Kyoo.Meilisearch/SearchManager.cs | 3 +- .../20230907201814_added_date.Designer.cs | 4 -- .../20231029233109_news.Designer.cs | 4 -- .../20231031212819_rating.Designer.cs | 4 -- .../PostgresContextModelSnapshot.cs | 4 -- back/src/Kyoo.Postgresql/PostgresContext.cs | 2 - back/src/Kyoo.Postgresql/PostgresModule.cs | 1 - 40 files changed, 226 insertions(+), 131 deletions(-) rename back/src/Kyoo.Abstractions/Models/Attributes/{OneOf.cs => OneOfAttribute.cs} (100%) create mode 100644 back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IQuery.cs diff --git a/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs b/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs index 59cd116d..cc3e2f5b 100644 --- a/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs @@ -26,7 +26,7 @@ namespace Kyoo.Abstractions.Controllers public interface ILibraryManager { IRepository Repository() - where T : class, IResource; + where T : class, IResource, IQuery; /// /// The repository that handle libraries items (a wrapper around shows and collections). diff --git a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs index e52afafd..c4303b99 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs @@ -31,7 +31,7 @@ namespace Kyoo.Abstractions.Controllers /// /// The resource's type that this repository manage. public interface IRepository : IBaseRepository - where T : class, IResource + where T : class, IResource, IQuery { /// /// The event handler type for all events of this repository. diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/OneOf.cs b/back/src/Kyoo.Abstractions/Models/Attributes/OneOfAttribute.cs similarity index 100% rename from back/src/Kyoo.Abstractions/Models/Attributes/OneOf.cs rename to back/src/Kyoo.Abstractions/Models/Attributes/OneOfAttribute.cs diff --git a/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs b/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs index bdeb61d5..00847eec 100644 --- a/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs +++ b/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models; @@ -24,4 +25,7 @@ namespace Kyoo.Abstractions.Models; /// A show, a movie or a collection. /// [OneOf(Types = new[] { typeof(Show), typeof(Movie), typeof(Collection) })] -public interface ILibraryItem : IResource, IThumbnails, IMetadata, IAddedDate { } +public interface ILibraryItem : IResource, IThumbnails, IMetadata, IAddedDate, IQuery +{ + static Sort IQuery.DefaultSort => new Sort.By(nameof(Movie.AirDate)); +} diff --git a/back/src/Kyoo.Abstractions/Models/News.cs b/back/src/Kyoo.Abstractions/Models/News.cs index 9ecb8f99..6001aa84 100644 --- a/back/src/Kyoo.Abstractions/Models/News.cs +++ b/back/src/Kyoo.Abstractions/Models/News.cs @@ -41,7 +41,7 @@ namespace Kyoo.Abstractions.Models /// /// A new item /// - public class News : IResource, IMetadata, IThumbnails, IAddedDate + public class News : IResource, IMetadata, IThumbnails, IAddedDate, IQuery { /// public int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs index f0f149aa..771050ef 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Newtonsoft.Json; @@ -28,8 +29,10 @@ namespace Kyoo.Abstractions.Models /// /// A class representing collections of . /// - public class Collection : IResource, IMetadata, IThumbnails, IAddedDate, ILibraryItem + public class Collection : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, ILibraryItem { + public static Sort DefaultSort => new Sort.By(nameof(Collection.Name)); + /// public int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs index 54495bcb..1bf3fafc 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs @@ -23,6 +23,7 @@ using System.Linq; using System.Text.RegularExpressions; using EntityFrameworkCore.Projectables; using JetBrains.Annotations; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models @@ -30,8 +31,15 @@ namespace Kyoo.Abstractions.Models /// /// A class to represent a single show's episode. /// - public class Episode : IResource, IMetadata, IThumbnails, IAddedDate + public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate { + // 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 int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IQuery.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IQuery.cs new file mode 100644 index 00000000..95634fa7 --- /dev/null +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IQuery.cs @@ -0,0 +1,30 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using Kyoo.Abstractions.Controllers; + +namespace Kyoo.Abstractions.Models; + +public interface IQuery +{ + /// + /// The sorting that will be used when no user defined one is present. + /// + public static virtual Sort DefaultSort => throw new NotImplementedException(); +} diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs index 92babc84..0304a475 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Newtonsoft.Json; @@ -28,8 +29,10 @@ namespace Kyoo.Abstractions.Models /// /// A series or a movie. /// - public class Movie : IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem + public class Movie : IQuery, IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem { + public static Sort DefaultSort => new Sort.By(x => x.Name); + /// public int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/People.cs b/back/src/Kyoo.Abstractions/Models/Resources/People.cs index fee3fa99..f468840e 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/People.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/People.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Newtonsoft.Json; @@ -27,8 +28,10 @@ namespace Kyoo.Abstractions.Models /// /// An actor, voice actor, writer, animator, somebody who worked on a . /// - public class People : IResource, IMetadata, IThumbnails + public class People : IQuery, IResource, IMetadata, IThumbnails { + public static Sort DefaultSort => new Sort.By(x => x.Name); + /// public int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs index 9951b9e1..ae2c199c 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs @@ -23,6 +23,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.RegularExpressions; using EntityFrameworkCore.Projectables; using JetBrains.Annotations; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models @@ -30,8 +31,10 @@ namespace Kyoo.Abstractions.Models /// /// A season of a . /// - public class Season : IResource, IMetadata, IThumbnails, IAddedDate + public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate { + public static Sort DefaultSort => new Sort.By(x => x.SeasonNumber); + /// public int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs index ea9127ef..07e8935f 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using EntityFrameworkCore.Projectables; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Newtonsoft.Json; @@ -30,8 +31,10 @@ namespace Kyoo.Abstractions.Models /// /// A series or a movie. /// - public class Show : IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem + public class Show : IQuery, IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem { + public static Sort DefaultSort => new Sort.By(x => x.Name); + /// public int Id { get; set; } @@ -107,7 +110,8 @@ namespace Kyoo.Abstractions.Models /// public string? Trailer { get; set; } - [SerializeIgnore] public DateTime? AirDate => StartAir; + [SerializeIgnore] + public DateTime? AirDate => StartAir; /// public Dictionary ExternalId { get; set; } = new(); diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs b/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs index 77b10a90..a1ab6847 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Studio.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Newtonsoft.Json; @@ -27,8 +28,10 @@ namespace Kyoo.Abstractions.Models /// /// A studio that make shows. /// - public class Studio : IResource, IMetadata + public class Studio : IQuery, IResource, IMetadata { + public static Sort DefaultSort => new Sort.By(x => x.Name); + /// public int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/User.cs b/back/src/Kyoo.Abstractions/Models/Resources/User.cs index 05f7241d..460680c1 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/User.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/User.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Newtonsoft.Json; @@ -28,8 +29,10 @@ namespace Kyoo.Abstractions.Models /// /// A single user of the app. /// - public class User : IResource, IAddedDate + public class User : IQuery, IResource, IAddedDate { + public static Sort DefaultSort => new Sort.By(x => x.Username); + /// public int Id { get; set; } diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs index 045ac2cc..e94244e9 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs @@ -43,11 +43,14 @@ public class Include return new Include { - Fields = fields.Split(',').Select(x => + Fields = fields.Split(',').Select(key => { - PropertyInfo? prop = typeof(T).GetProperty(x, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; + PropertyInfo? prop = types + .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)) + .FirstOrDefault(); if (prop?.GetCustomAttribute() == null) - throw new ValidationException($"No loadable relation with the name {x}."); + throw new ValidationException($"No loadable relation with the name {key}."); return prop.Name; }).ToArray() }; diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs index 89e1f4fe..482be649 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs @@ -21,15 +21,20 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; namespace Kyoo.Abstractions.Controllers { + public record Sort; + /// /// Information about how a query should be sorted. What factor should decide the sort and in which order. /// /// For witch type this sort applies - public record Sort + public record Sort : Sort + where T : IQuery { /// /// Sort by a specific key @@ -61,7 +66,13 @@ namespace Kyoo.Abstractions.Controllers public record Random(uint seed) : Sort; /// The default sort method for the given type. - public record Default : Sort; + public record Default : Sort + { + public void Deconstruct(out Sort value) + { + value = (Sort)T.DefaultSort; + } + } /// /// Create a new instance from a key's name (case insensitive). @@ -91,7 +102,11 @@ namespace Kyoo.Abstractions.Controllers null => false, _ => throw new ValidationException($"The sort order, if set, should be :asc or :desc but it was :{order}.") }; - PropertyInfo? property = typeof(T).GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + + Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; + PropertyInfo? property = types + .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)) + .FirstOrDefault(); if (property == null) throw new ValidationException("The given sort key is not valid."); return new By(property.Name, desendant); diff --git a/back/src/Kyoo.Abstractions/Utility/Utility.cs b/back/src/Kyoo.Abstractions/Utility/Utility.cs index e3e91694..9f3a95bf 100644 --- a/back/src/Kyoo.Abstractions/Utility/Utility.cs +++ b/back/src/Kyoo.Abstractions/Utility/Utility.cs @@ -34,6 +34,69 @@ namespace Kyoo.Utils /// public static class Utility { + /// + /// Convert a string to snake case. Stollen from + /// https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs + /// + /// The string to convert. + /// The string in snake case + public static string ToSnakeCase(this string name) + { + StringBuilder builder = new(name.Length + Math.Min(2, name.Length / 5)); + UnicodeCategory? previousCategory = default; + + for (int currentIndex = 0; currentIndex < name.Length; currentIndex++) + { + char currentChar = name[currentIndex]; + if (currentChar == '_') + { + builder.Append('_'); + previousCategory = null; + continue; + } + + UnicodeCategory currentCategory = char.GetUnicodeCategory(currentChar); + switch (currentCategory) + { + 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]))) + { + builder.Append('_'); + } + + currentChar = char.ToLowerInvariant(currentChar); + break; + + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.DecimalDigitNumber: + if (previousCategory == UnicodeCategory.SpaceSeparator) + { + builder.Append('_'); + } + break; + + default: + if (previousCategory != null) + { + previousCategory = UnicodeCategory.SpaceSeparator; + } + continue; + } + + builder.Append(currentChar); + previousCategory = currentCategory; + } + + return builder.ToString(); + } + /// /// Is the lambda expression a member (like x => x.Body). /// diff --git a/back/src/Kyoo.Core/Controllers/LibraryManager.cs b/back/src/Kyoo.Core/Controllers/LibraryManager.cs index 17cfb63d..c9bc1b48 100644 --- a/back/src/Kyoo.Core/Controllers/LibraryManager.cs +++ b/back/src/Kyoo.Core/Controllers/LibraryManager.cs @@ -92,7 +92,7 @@ namespace Kyoo.Core.Controllers public IRepository Users { get; } public IRepository Repository() - where T : class, IResource + where T : class, IResource, IQuery { return (IRepository)_repositories.First(x => x.RepositoryType == typeof(T)); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index 6d219148..f4fb0a03 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; @@ -39,9 +38,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - protected override Sort DefaultSort => new Sort.By(nameof(Collection.Name)); - /// /// Create a new . /// @@ -56,11 +52,10 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.Collections, include) - .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) - .Take(20) - ).ToListAsync(); + return await AddIncludes(_database.Collections, include) + .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) + .Take(20) + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index b52f8c3e..f9eb0d27 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -41,14 +41,6 @@ namespace Kyoo.Core.Controllers private readonly IRepository _shows; - /// - // Use absolute numbers by default and fallback to season/episodes if it does not exists. - protected override Sort DefaultSort => new Sort.Conglomerate( - new Sort.By(x => x.AbsoluteNumber), - new Sort.By(x => x.SeasonNumber), - new Sort.By(x => x.EpisodeNumber) - ); - static EpisodeRepository() { // Edit episode slugs when the show's slug changes. @@ -86,10 +78,8 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.Episodes, include) - .Where(_database.Like(x => x.Name!, $"%{query}%")) - ) + return await AddIncludes(_database.Episodes, include) + .Where(_database.Like(x => x.Name!, $"%{query}%")) .Take(20) .ToListAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index 0eac4667..13ef6d90 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -30,6 +30,7 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; +using Kyoo.Utils; namespace Kyoo.Core.Controllers { @@ -91,17 +92,30 @@ namespace Kyoo.Core.Controllers throw new NotImplementedException(); } - public string ProcessSort(Sort sort, string[] tables) + public string ProcessSort(Sort sort, Dictionary config) + where T : IQuery { - return sort switch + string Property(string key) { - // TODO: Implement default sort by - Sort.Default => $"coalesce({string.Join(", ", tables.Select(x => $"{x}.name"))})", - Sort.By(string key, bool desc) => $"coalesce({string.Join(", ", tables.Select(x => $"{x}.{key}"))}) {(desc ? "desc" : "asc")}", - Sort.Random(var seed) => $"md5('{seed}' || coalesce({string.Join(", ", tables.Select(x => $"{x}.id"))}))", - Sort.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, tables))), + if (config.Count == 1) + return $"{config.First()}.{key.ToSnakeCase()}"; + + IEnumerable keys = config + .Where(x => x.Value.GetProperty(key) != null) + .Select(x => $"{x.Key}.{key.ToSnakeCase()}"); + return $"coalesce({string.Join(", ", keys)})"; + } + + string ret = sort switch + { + Sort.Default(var value) => ProcessSort(value, config), + Sort.By(string key, bool desc) => $"{Property(key)} {(desc ? "desc nulls last" : "asc")}", + Sort.Random(var seed) => $"md5('{seed}' || {Property("id")})", + Sort.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, config))), _ => throw new SwitchExpressionException(), }; + // always end query by an id sort. + return $"{ret}, {Property("id")} asc"; } public async Task> GetAll( @@ -110,6 +124,12 @@ namespace Kyoo.Core.Controllers Pagination? limit = null, Include? include = null) { + Dictionary config = new() + { + { "s", typeof(Show) }, + { "m", typeof(Movie) }, + { "c", typeof(Collection) } + }; // language=PostgreSQL IDapperSqlCommand query = _database.SqlBuilder($""" select @@ -130,11 +150,11 @@ namespace Kyoo.Core.Controllers from collections) as c on false left join studios as st on st.id = coalesce(s.studio_id, m.studio_id) - order by {ProcessSort(sort, new[] { "s", "m", "c" }):raw} + order by {ProcessSort(sort, config):raw} limit {limit.Limit} """).Build(); - Type[] types = new[] { typeof(Show), typeof(Movie), typeof(Collection), typeof(Studio) }; + Type[] types = config.Select(x => x.Value).Concat(new[] { typeof(Studio) }).ToArray(); IEnumerable data = await query.QueryAsync(types, items => { var studio = items[3] as Studio; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index 0796dca3..6fead483 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -40,7 +40,7 @@ namespace Kyoo.Core.Controllers /// /// The type of this repository public abstract class LocalRepository : IRepository - where T : class, IResource + where T : class, IResource, IQuery { /// /// The Entity Framework's Database handle. @@ -52,11 +52,6 @@ namespace Kyoo.Core.Controllers /// private readonly IThumbnailsManager _thumbs; - /// - /// The default sort order that will be used for this resource's type. - /// - protected abstract Sort DefaultSort { get; } - /// /// Create a new base with the given database handle. /// @@ -77,9 +72,9 @@ namespace Kyoo.Core.Controllers /// The query to sort. /// How to sort the query. /// The newly sorted query. - protected IOrderedQueryable Sort(IQueryable query, Sort? sortBy = null) + protected IOrderedQueryable Sort(IQueryable query, Sort? sortBy) { - sortBy ??= DefaultSort; + sortBy ??= new Sort.Default(); IOrderedQueryable _SortBy(IQueryable qr, Expression> sort, bool desc, bool then) { @@ -98,8 +93,8 @@ namespace Kyoo.Core.Controllers { switch (sortBy) { - case Sort.Default: - return _Sort(query, DefaultSort, then); + case Sort.Default(var value): + return _Sort(query, value, then); case Sort.By(var key, var desc): return _SortBy(query, x => EF.Property(x, key), desc, then); case Sort.Random(var seed): @@ -154,7 +149,7 @@ namespace Kyoo.Core.Controllers T reference, bool next = true) { - sort ??= DefaultSort; + sort ??= new Sort.Default(); // x => ParameterExpression x = Expression.Parameter(typeof(T), "x"); @@ -173,7 +168,7 @@ namespace Kyoo.Core.Controllers { return sort switch { - Sort.Default => GetSortsBy(DefaultSort), + Sort.Default(var value) => GetSortsBy(value), 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()) }, diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index 4101b7e9..ae055f4e 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -47,9 +47,6 @@ namespace Kyoo.Core.Controllers /// private readonly IRepository _people; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Name); - /// /// Create a new . /// @@ -71,10 +68,8 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.Movies, include) - .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) - ) + return await AddIncludes(_database.Movies, include) + .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) .Take(20) .ToListAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs index c66c6f24..ba813113 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs @@ -31,9 +31,6 @@ namespace Kyoo.Core.Controllers /// public class NewsRepository : LocalRepository { - /// - protected override Sort DefaultSort => new Sort.By(x => x.AddedDate, true); - public NewsRepository(DatabaseContext database, IThumbnailsManager thumbs) : base(database, thumbs) { } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs index 1a004ff9..0c63c83b 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs @@ -44,9 +44,6 @@ namespace Kyoo.Core.Controllers /// private readonly Lazy> _shows; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Name); - /// /// Create a new /// @@ -65,10 +62,8 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.People, include) - .Where(_database.Like(x => x.Name, $"%{query}%")) - ) + return await AddIncludes(_database.People, include) + .Where(_database.Like(x => x.Name, $"%{query}%")) .Take(20) .ToListAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 2b285b76..95067a93 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -39,9 +39,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - protected override Sort DefaultSort => new Sort.By(x => x.SeasonNumber); - static SeasonRepository() { // Edit seasons slugs when the show's slug changes. @@ -76,10 +73,8 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.Seasons, include) - .Where(_database.Like(x => x.Name!, $"%{query}%")) - ) + return await AddIncludes(_database.Seasons, include) + .Where(_database.Like(x => x.Name!, $"%{query}%")) .Take(20) .ToListAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index 4d234454..139fea05 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -48,9 +48,6 @@ namespace Kyoo.Core.Controllers /// private readonly IRepository _people; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Name); - /// /// Create a new . /// @@ -72,10 +69,8 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.Shows, include) - .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) - ) + return await AddIncludes(_database.Shows, include) + .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) .Take(20) .ToListAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs index 31a8d920..6dda27d2 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs @@ -38,9 +38,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Name); - /// /// Create a new . /// @@ -55,10 +52,8 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.Studios, include) - .Where(_database.Like(x => x.Name, $"%{query}%")) - ) + return await AddIncludes(_database.Studios, include) + .Where(_database.Like(x => x.Name, $"%{query}%")) .Take(20) .ToListAsync(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs index daf38738..5e87a31c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs @@ -37,9 +37,6 @@ namespace Kyoo.Core.Controllers /// private readonly DatabaseContext _database; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Username); - /// /// Create a new /// @@ -54,10 +51,8 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query, Include? include = default) { - return await Sort( - AddIncludes(_database.Users, include) - .Where(_database.Like(x => x.Username, $"%{query}%")) - ) + return await AddIncludes(_database.Users, include) + .Where(_database.Like(x => x.Username, $"%{query}%")) .Take(20) .ToListAsync(); } diff --git a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs index d3cc5fca..a58862bd 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -37,7 +37,7 @@ namespace Kyoo.Core.Api [ApiController] [ResourceView] public class CrudApi : BaseApi - where T : class, IResource + where T : class, IResource, IQuery { /// /// The repository of the resource, used to retrieve, save and do operations on the baking store. diff --git a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index a8143ebc..8168a09d 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -36,7 +36,7 @@ namespace Kyoo.Core.Api [ApiController] [ResourceView] public class CrudThumbsApi : CrudApi - where T : class, IResource, IThumbnails + where T : class, IResource, IThumbnails, IQuery { /// /// The thumbnail manager used to retrieve images paths. diff --git a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index a2e086e5..eca83dc1 100644 --- a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -77,7 +77,7 @@ namespace Kyoo.Core.Api { IList properties = base.CreateProperties(type, memberSerialization); - if (properties.All(x => x.PropertyName != "kind")) + if (properties.All(x => x.PropertyName != "kind") && type.IsAssignableTo(typeof(IResource))) { properties.Add(new JsonProperty() { diff --git a/back/src/Kyoo.Core/Views/Helper/SortBinder.cs b/back/src/Kyoo.Core/Views/Helper/SortBinder.cs index 3200201f..59b70884 100644 --- a/back/src/Kyoo.Core/Views/Helper/SortBinder.cs +++ b/back/src/Kyoo.Core/Views/Helper/SortBinder.cs @@ -20,6 +20,7 @@ using System; using System.Reflection; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; @@ -38,7 +39,7 @@ public class SortBinder : IModelBinder ); 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.Meilisearch/SearchManager.cs b/back/src/Kyoo.Meilisearch/SearchManager.cs index 9ebed39c..f88f369d 100644 --- a/back/src/Kyoo.Meilisearch/SearchManager.cs +++ b/back/src/Kyoo.Meilisearch/SearchManager.cs @@ -31,6 +31,7 @@ public class SearchManager : ISearchManager private readonly ILibraryManager _libraryManager; private static IEnumerable _GetSortsBy(string index, Sort? sort) + where T : IQuery { return sort switch { @@ -55,7 +56,7 @@ public class SearchManager : ISearchManager Sort? sortBy = default, SearchPagination? pagination = default, Include? include = default) - where T : class, IResource + where T : class, IResource, IQuery { // TODO: add filters and facets ISearchable res = await _client.Index(index).SearchAsync(query, new SearchQuery() diff --git a/back/src/Kyoo.Postgresql/Migrations/20230907201814_added_date.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20230907201814_added_date.Designer.cs index 2f012a1a..484baa3b 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20230907201814_added_date.Designer.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20230907201814_added_date.Designer.cs @@ -193,10 +193,6 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("genre[]") .HasColumnName("genres"); - b.Property("Kind") - .HasColumnType("item_kind") - .HasColumnName("kind"); - b.Property("Name") .IsRequired() .HasColumnType("text") diff --git a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs index 318bd082..95fff21a 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs @@ -194,10 +194,6 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("genre[]") .HasColumnName("genres"); - b.Property("Kind") - .HasColumnType("item_kind") - .HasColumnName("kind"); - b.Property("Name") .IsRequired() .HasColumnType("text") diff --git a/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs index fe21bd2c..68a97c4a 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs @@ -198,10 +198,6 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("genre[]") .HasColumnName("genres"); - b.Property("Kind") - .HasColumnType("item_kind") - .HasColumnName("kind"); - b.Property("Name") .IsRequired() .HasColumnType("text") diff --git a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 17fe6ec8..3973198b 100644 --- a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -195,10 +195,6 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("genre[]") .HasColumnName("genres"); - b.Property("Kind") - .HasColumnType("item_kind") - .HasColumnName("kind"); - b.Property("Name") .IsRequired() .HasColumnType("text") diff --git a/back/src/Kyoo.Postgresql/PostgresContext.cs b/back/src/Kyoo.Postgresql/PostgresContext.cs index 520758e7..0f6b25bb 100644 --- a/back/src/Kyoo.Postgresql/PostgresContext.cs +++ b/back/src/Kyoo.Postgresql/PostgresContext.cs @@ -50,7 +50,6 @@ namespace Kyoo.Postgresql { NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); } @@ -104,7 +103,6 @@ namespace Kyoo.Postgresql { modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); - modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!) diff --git a/back/src/Kyoo.Postgresql/PostgresModule.cs b/back/src/Kyoo.Postgresql/PostgresModule.cs index 3a4440e5..7f8785d2 100644 --- a/back/src/Kyoo.Postgresql/PostgresModule.cs +++ b/back/src/Kyoo.Postgresql/PostgresModule.cs @@ -21,7 +21,6 @@ using System.Collections.Generic; using System.Data.Common; using System.Text.RegularExpressions; using Dapper; -using EFCore.NamingConventions.Internal; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Postgresql.Utils;