From fbe624ca6d83942980e07586c6665a9acd452dc0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 13 Mar 2023 18:40:07 +0900 Subject: [PATCH] Add previous page support --- back/src/Kyoo.Abstractions/Models/Page.cs | 17 ++++- .../Models/Utils/Pagination.cs | 24 ++++--- .../Kyoo.Abstractions/Models/Utils/Sort.cs | 44 +++++-------- .../src/Kyoo.Abstractions/Models/WatchItem.cs | 41 ------------ .../Repositories/LocalRepository.cs | 63 +++++++++++-------- back/src/Kyoo.Core/Views/Helper/CrudApi.cs | 2 +- .../Views/Helper/ResourceViewAttribute.cs | 9 ++- back/src/Kyoo.Core/Views/Metadata/GenreApi.cs | 2 +- back/src/Kyoo.Core/Views/Metadata/StaffApi.cs | 2 +- .../src/Kyoo.Core/Views/Metadata/StudioApi.cs | 2 +- .../Views/Resources/CollectionApi.cs | 4 +- .../Kyoo.Core/Views/Resources/EpisodeApi.cs | 2 +- .../Kyoo.Core/Views/Resources/LibraryApi.cs | 6 +- .../Views/Resources/LibraryItemApi.cs | 2 +- .../Kyoo.Core/Views/Resources/SeasonApi.cs | 2 +- back/src/Kyoo.Core/Views/Resources/ShowApi.cs | 12 ++-- 16 files changed, 110 insertions(+), 124 deletions(-) diff --git a/back/src/Kyoo.Abstractions/Models/Page.cs b/back/src/Kyoo.Abstractions/Models/Page.cs index 0acdb4a9..6c09e54a 100644 --- a/back/src/Kyoo.Abstractions/Models/Page.cs +++ b/back/src/Kyoo.Abstractions/Models/Page.cs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; using System.Linq; using Kyoo.Utils; @@ -40,6 +39,11 @@ namespace Kyoo.Abstractions.Models /// public string First { get; } + /// + /// The link of the previous page. + /// + public string Previous { get; } + /// /// The link of the next page. /// @@ -60,12 +64,14 @@ namespace Kyoo.Abstractions.Models /// /// The list of items in the page. /// The link of the current page. + /// 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 next, string first) + public Page(ICollection items, string @this, string previous, string next, string first) { Items = items; This = @this; + Previous = previous; Next = next; First = first; } @@ -85,6 +91,13 @@ namespace Kyoo.Abstractions.Models Items = items; This = url + query.ToQueryString(); + if (items.Count > 0 && query.ContainsKey("afterID")) + { + query["afterID"] = items.First().ID.ToString(); + query["reverse"] = "true"; + Previous = url + query.ToQueryString(); + } + query.Remove("reverse"); if (items.Count == limit && limit > 0) { query["afterID"] = items.Last().ID.ToString(); diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Pagination.cs b/back/src/Kyoo.Abstractions/Models/Utils/Pagination.cs index 935393b3..8dee9b00 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Pagination.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Pagination.cs @@ -21,32 +21,42 @@ namespace Kyoo.Abstractions.Controllers /// /// Information about the pagination. How many items should be displayed and where to start. /// - public readonly struct Pagination + public class Pagination { /// /// The count of items to return. /// - public int Count { get; } + public int Limit { get; set; } /// /// Where to start? Using the given sort. /// - public int? AfterID { get; } + public int? AfterID { get; set; } /// /// Should the previous page be returned instead of the next? /// - public bool Reverse { get; } + public bool Reverse { get; set; } + + /// + /// Create a new with default values. + /// + public Pagination() + { + Limit = 20; + AfterID = null; + Reverse = false; + } /// /// Create a new instance. /// - /// Set the value + /// Set the value /// Set the value. If not specified, it will start from the start /// Should the previous page be returned instead of the next? public Pagination(int count, int? afterID = null, bool reverse = false) { - Count = count; + Limit = count; AfterID = afterID; Reverse = reverse; } @@ -54,7 +64,7 @@ namespace Kyoo.Abstractions.Controllers /// /// Implicitly create a new pagination from a limit number. /// - /// Set the value + /// Set the value /// A new instance public static implicit operator Pagination(int limit) => new(limit); } diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs index 4cafb788..5e8191b0 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Sort.cs @@ -48,36 +48,13 @@ namespace Kyoo.Abstractions.Controllers /// public By(Expression> key, bool desendant = false) : this(Utility.GetPropertyName(key), desendant) { } - - /// - /// Create a new instance from a key's name (case insensitive). - /// - /// A key name with an optional order specifier. Format: "key:asc", "key:desc" or "key". - /// An invalid key or sort specifier as been given. - /// A for the given string - public static new By From(string sortBy) - { - string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy; - string order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null; - bool desendant = order switch - { - "desc" => true, - "asc" => false, - null => false, - _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") - }; - PropertyInfo property = typeof(T).GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - if (property == null) - throw new ArgumentException("The given sort key is not valid."); - return new By(property.Name, desendant); - } } /// /// Sort by multiple keys. /// /// The list of keys to sort by. - public record Conglomerate(params By[] list) : Sort; + public record Conglomerate(params Sort[] list) : Sort; /// The default sort method for the given type. public record Default : Sort; @@ -90,11 +67,24 @@ namespace Kyoo.Abstractions.Controllers /// A for the given string public static Sort From(string sortBy) { - if (string.IsNullOrEmpty(sortBy)) + if (string.IsNullOrEmpty(sortBy) || sortBy == "default") return new Default(); if (sortBy.Contains(',')) - return new Conglomerate(sortBy.Split(',').Select(By.From).ToArray()); - return By.From(sortBy); + return new Conglomerate(sortBy.Split(',').Select(From).ToArray()); + + string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy; + string order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null; + bool desendant = order switch + { + "desc" => true, + "asc" => false, + null => false, + _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") + }; + PropertyInfo property = typeof(T).GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + if (property == null) + throw new ArgumentException("The given sort key is not valid."); + return new By(property.Name, desendant); } } } diff --git a/back/src/Kyoo.Abstractions/Models/WatchItem.cs b/back/src/Kyoo.Abstractions/Models/WatchItem.cs index 10bdfae4..c737839c 100644 --- a/back/src/Kyoo.Abstractions/Models/WatchItem.cs +++ b/back/src/Kyoo.Abstractions/Models/WatchItem.cs @@ -163,47 +163,6 @@ namespace Kyoo.Abstractions.Models await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Tracks); - // if (!ep.Show.IsMovie) - // { - // if (ep.AbsoluteNumber != null) - // { - // previous = await library.GetOrDefault( - // x => x.ShowID == ep.ShowID && x.AbsoluteNumber < ep.AbsoluteNumber, - // new Sort(x => x.AbsoluteNumber, true) - // ); - // next = await library.GetOrDefault( - // x => x.ShowID == ep.ShowID && x.AbsoluteNumber > ep.AbsoluteNumber, - // new Sort(x => x.AbsoluteNumber) - // ); - // } - // else if (ep.SeasonNumber != null && ep.EpisodeNumber != null) - // { - // previous = await library.GetOrDefault( - // x => x.ShowID == ep.ShowID - // && x.SeasonNumber == ep.SeasonNumber - // && x.EpisodeNumber < ep.EpisodeNumber, - // new Sort(x => x.EpisodeNumber, true) - // ); - // previous ??= await library.GetOrDefault( - // x => x.ShowID == ep.ShowID - // && x.SeasonNumber == ep.SeasonNumber - 1, - // new Sort(x => x.EpisodeNumber, true) - // ); - // - // next = await library.GetOrDefault( - // x => x.ShowID == ep.ShowID - // && x.SeasonNumber == ep.SeasonNumber - // && x.EpisodeNumber > ep.EpisodeNumber, - // new Sort(x => x.EpisodeNumber) - // ); - // next ??= await library.GetOrDefault( - // x => x.ShowID == ep.ShowID - // && x.SeasonNumber == ep.SeasonNumber + 1, - // new Sort(x => x.EpisodeNumber) - // ); - // } - // } - return new WatchItem { EpisodeID = ep.ID, diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index 515f9ad0..835218bd 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -72,31 +72,38 @@ namespace Kyoo.Core.Controllers { sortBy ??= DefaultSort; - IOrderedQueryable _Sort(IQueryable query, Sort sortBy) + 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 + ? qr.OrderByDescending(sort) + : qr.OrderBy(sort); + } + + IOrderedQueryable _Sort(IQueryable query, Sort sortBy, bool then) { switch (sortBy) { case Sort.Default: - return Sort(query, DefaultSort); + return _Sort(query, DefaultSort, then); case Sort.By(var key, var desc): - return desc - ? query.OrderByDescending(x => EF.Property(x, key)) - : query.OrderBy(x => EF.Property(x, key)); - case Sort.Conglomerate(var keys): - IOrderedQueryable nQuery = _Sort(query, keys[0]); - foreach ((string key, bool desc) in keys.Skip(1)) - { - nQuery = desc - ? nQuery.ThenByDescending(x => EF.Property(x, key)) - : nQuery.ThenBy(x => EF.Property(x, key)); - } + return _SortBy(query, x => EF.Property(x, key), desc, then); + case Sort.Conglomerate(var sorts): + IOrderedQueryable nQuery = _Sort(query, sorts.First(), false); + foreach (Sort sort in sorts.Skip(1)) + nQuery = _Sort(nQuery, sort, true); return nQuery; default: // The language should not require me to do this... throw new SwitchExpressionException(); } } - return _Sort(query, sortBy).ThenBy(x => x.ID); + return _Sort(query, sortBy, false).ThenBy(x => x.ID); } private static Func _GetComparisonExpression( @@ -133,22 +140,24 @@ namespace Kyoo.Core.Controllers T reference, bool next = true) { - if (sort is Sort.Default) - sort = DefaultSort; - // x => ParameterExpression x = Expression.Parameter(typeof(T), "x"); ConstantExpression referenceC = Expression.Constant(reference, typeof(T)); + IEnumerable.By> _GetSortsBy(Sort sort) + { + return sort switch + { + Sort.Default => _GetSortsBy(DefaultSort), + Sort.By @sortBy => new[] { sortBy }, + Sort.Conglomerate(var list) => list.SelectMany(_GetSortsBy), + _ => Array.Empty.By>(), + }; + } + // Don't forget that every sorts must end with a ID sort (to differenciate equalities). Sort.By id = new(x => x.ID); - - IEnumerable.By> sorts = (sort switch - { - Sort.By @sortBy => new[] { sortBy }, - Sort.Conglomerate(var list) => list, - _ => Array.Empty.By>(), - }).Append(id); + IEnumerable.By> sorts = _GetSortsBy(sort).Append(id); BinaryExpression filter = null; List.By> previousSteps = new(); @@ -278,10 +287,10 @@ namespace Kyoo.Core.Controllers if (limit.AfterID != null) { T reference = await Get(limit.AfterID.Value); - query = query.Where(KeysetPaginatate(sort, reference)); + query = query.Where(KeysetPaginatate(sort, reference, !limit.Reverse)); } - if (limit.Count > 0) - query = query.Take(limit.Count); + if (limit.Limit > 0) + query = query.Take(limit.Limit); return await query.ToListAsync(); } diff --git a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs index 76ef01eb..dbb258b1 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -120,7 +120,7 @@ namespace Kyoo.Core.Api pagination ); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// diff --git a/back/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs b/back/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs index a9c8ae4a..b28dc802 100644 --- a/back/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs +++ b/back/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs @@ -44,9 +44,14 @@ namespace Kyoo.Core.Api { if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary where) { - where.Remove("fields"); + Dictionary nWhere = new(where, StringComparer.InvariantCultureIgnoreCase); + nWhere.Remove("fields"); + nWhere.Remove("afterID"); + nWhere.Remove("limit"); + nWhere.Remove("reverse"); foreach ((string key, _) in context.ActionArguments) - where.Remove(key); + nWhere.Remove(key); + context.ActionArguments["where"] = nWhere; } List fields = context.HttpContext.Request.Query["fields"] diff --git a/back/src/Kyoo.Core/Views/Metadata/GenreApi.cs b/back/src/Kyoo.Core/Views/Metadata/GenreApi.cs index 48df7490..eb72b284 100644 --- a/back/src/Kyoo.Core/Views/Metadata/GenreApi.cs +++ b/back/src/Kyoo.Core/Views/Metadata/GenreApi.cs @@ -89,7 +89,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } } } diff --git a/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs b/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs index 040e7eca..682b027a 100644 --- a/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs +++ b/back/src/Kyoo.Core/Views/Metadata/StaffApi.cs @@ -95,7 +95,7 @@ namespace Kyoo.Core.Api slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sort, pagination) ); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } } } diff --git a/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs b/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs index 48da7042..873c398f 100644 --- a/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs +++ b/back/src/Kyoo.Core/Views/Metadata/StudioApi.cs @@ -90,7 +90,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + 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 24ff4356..e09ba2e5 100644 --- a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -93,7 +93,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -128,7 +128,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + 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 d8cc7633..21c5e952 100644 --- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -158,7 +158,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } } } diff --git a/back/src/Kyoo.Core/Views/Resources/LibraryApi.cs b/back/src/Kyoo.Core/Views/Resources/LibraryApi.cs index 4700de8b..6551a99a 100644 --- a/back/src/Kyoo.Core/Views/Resources/LibraryApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/LibraryApi.cs @@ -92,7 +92,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -127,7 +127,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -165,7 +165,7 @@ namespace Kyoo.Core.Api slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination) ); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } } } diff --git a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs index ba582f7e..f1b6747d 100644 --- a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs @@ -89,7 +89,7 @@ namespace Kyoo.Core.Api pagination ); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } } } diff --git a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs index 74b92318..ca0acff5 100644 --- a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs @@ -93,7 +93,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// diff --git a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs index e77de8ff..6db8323b 100644 --- a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -97,7 +97,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -132,7 +132,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -166,7 +166,7 @@ namespace Kyoo.Core.Api id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination), slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination) ); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -201,7 +201,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -255,7 +255,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } /// @@ -290,7 +290,7 @@ namespace Kyoo.Core.Api if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); - return Page(resources, pagination.Count); + return Page(resources, pagination.Limit); } } }