Add previous page support

This commit is contained in:
Zoe Roux 2023-03-13 18:40:07 +09:00
parent 67112a37da
commit fbe624ca6d
16 changed files with 110 additions and 124 deletions

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Kyoo.Utils; using Kyoo.Utils;
@ -40,6 +39,11 @@ namespace Kyoo.Abstractions.Models
/// </summary> /// </summary>
public string First { get; } public string First { get; }
/// <summary>
/// The link of the previous page.
/// </summary>
public string Previous { get; }
/// <summary> /// <summary>
/// The link of the next page. /// The link of the next page.
/// </summary> /// </summary>
@ -60,12 +64,14 @@ namespace Kyoo.Abstractions.Models
/// </summary> /// </summary>
/// <param name="items">The list of items in the page.</param> /// <param name="items">The list of items in the page.</param>
/// <param name="this">The link of the current page.</param> /// <param name="this">The link of the current page.</param>
/// <param name="previous">The link of the previous page.</param>
/// <param name="next">The link of the next page.</param> /// <param name="next">The link of the next page.</param>
/// <param name="first">The link of the first page.</param> /// <param name="first">The link of the first page.</param>
public Page(ICollection<T> items, string @this, string next, string first) public Page(ICollection<T> items, string @this, string previous, string next, string first)
{ {
Items = items; Items = items;
This = @this; This = @this;
Previous = previous;
Next = next; Next = next;
First = first; First = first;
} }
@ -85,6 +91,13 @@ namespace Kyoo.Abstractions.Models
Items = items; Items = items;
This = url + query.ToQueryString(); 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) if (items.Count == limit && limit > 0)
{ {
query["afterID"] = items.Last().ID.ToString(); query["afterID"] = items.Last().ID.ToString();

View File

@ -21,32 +21,42 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Information about the pagination. How many items should be displayed and where to start. /// Information about the pagination. How many items should be displayed and where to start.
/// </summary> /// </summary>
public readonly struct Pagination public class Pagination
{ {
/// <summary> /// <summary>
/// The count of items to return. /// The count of items to return.
/// </summary> /// </summary>
public int Count { get; } public int Limit { get; set; }
/// <summary> /// <summary>
/// Where to start? Using the given sort. /// Where to start? Using the given sort.
/// </summary> /// </summary>
public int? AfterID { get; } public int? AfterID { get; set; }
/// <summary> /// <summary>
/// Should the previous page be returned instead of the next? /// Should the previous page be returned instead of the next?
/// </summary> /// </summary>
public bool Reverse { get; } public bool Reverse { get; set; }
/// <summary>
/// Create a new <see cref="Pagination"/> with default values.
/// </summary>
public Pagination()
{
Limit = 20;
AfterID = null;
Reverse = false;
}
/// <summary> /// <summary>
/// Create a new <see cref="Pagination"/> instance. /// Create a new <see cref="Pagination"/> instance.
/// </summary> /// </summary>
/// <param name="count">Set the <see cref="Count"/> value</param> /// <param name="count">Set the <see cref="Limit"/> value</param>
/// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param> /// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param>
/// <param name="reverse">Should the previous page be returned instead of the next?</param> /// <param name="reverse">Should the previous page be returned instead of the next?</param>
public Pagination(int count, int? afterID = null, bool reverse = false) public Pagination(int count, int? afterID = null, bool reverse = false)
{ {
Count = count; Limit = count;
AfterID = afterID; AfterID = afterID;
Reverse = reverse; Reverse = reverse;
} }
@ -54,7 +64,7 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Implicitly create a new pagination from a limit number. /// Implicitly create a new pagination from a limit number.
/// </summary> /// </summary>
/// <param name="limit">Set the <see cref="Count"/> value</param> /// <param name="limit">Set the <see cref="Limit"/> value</param>
/// <returns>A new <see cref="Pagination"/> instance</returns> /// <returns>A new <see cref="Pagination"/> instance</returns>
public static implicit operator Pagination(int limit) => new(limit); public static implicit operator Pagination(int limit) => new(limit);
} }

View File

@ -48,36 +48,13 @@ namespace Kyoo.Abstractions.Controllers
/// </param> /// </param>
public By(Expression<Func<T, object>> key, bool desendant = false) public By(Expression<Func<T, object>> key, bool desendant = false)
: this(Utility.GetPropertyName(key), desendant) { } : this(Utility.GetPropertyName(key), desendant) { }
/// <summary>
/// Create a new <see cref="Sort{T}"/> instance from a key's name (case insensitive).
/// </summary>
/// <param name="sortBy">A key name with an optional order specifier. Format: "key:asc", "key:desc" or "key".</param>
/// <exception cref="ArgumentException">An invalid key or sort specifier as been given.</exception>
/// <returns>A <see cref="Sort{T}"/> for the given string</returns>
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);
}
} }
/// <summary> /// <summary>
/// Sort by multiple keys. /// Sort by multiple keys.
/// </summary> /// </summary>
/// <param name="list">The list of keys to sort by.</param> /// <param name="list">The list of keys to sort by.</param>
public record Conglomerate(params By[] list) : Sort<T>; public record Conglomerate(params Sort<T>[] list) : Sort<T>;
/// <summary>The default sort method for the given type.</summary> /// <summary>The default sort method for the given type.</summary>
public record Default : Sort<T>; public record Default : Sort<T>;
@ -90,11 +67,24 @@ namespace Kyoo.Abstractions.Controllers
/// <returns>A <see cref="Sort{T}"/> for the given string</returns> /// <returns>A <see cref="Sort{T}"/> for the given string</returns>
public static Sort<T> From(string sortBy) public static Sort<T> From(string sortBy)
{ {
if (string.IsNullOrEmpty(sortBy)) if (string.IsNullOrEmpty(sortBy) || sortBy == "default")
return new Default(); return new Default();
if (sortBy.Contains(',')) if (sortBy.Contains(','))
return new Conglomerate(sortBy.Split(',').Select(By.From).ToArray()); return new Conglomerate(sortBy.Split(',').Select(From).ToArray());
return By.From(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);
} }
} }
} }

View File

@ -163,47 +163,6 @@ namespace Kyoo.Abstractions.Models
await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Show);
await library.Load(ep, x => x.Tracks); 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<Episode>(x => x.AbsoluteNumber, true)
// );
// next = await library.GetOrDefault(
// x => x.ShowID == ep.ShowID && x.AbsoluteNumber > ep.AbsoluteNumber,
// new Sort<Episode>(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<Episode>(x => x.EpisodeNumber, true)
// );
// previous ??= await library.GetOrDefault(
// x => x.ShowID == ep.ShowID
// && x.SeasonNumber == ep.SeasonNumber - 1,
// new Sort<Episode>(x => x.EpisodeNumber, true)
// );
//
// next = await library.GetOrDefault(
// x => x.ShowID == ep.ShowID
// && x.SeasonNumber == ep.SeasonNumber
// && x.EpisodeNumber > ep.EpisodeNumber,
// new Sort<Episode>(x => x.EpisodeNumber)
// );
// next ??= await library.GetOrDefault(
// x => x.ShowID == ep.ShowID
// && x.SeasonNumber == ep.SeasonNumber + 1,
// new Sort<Episode>(x => x.EpisodeNumber)
// );
// }
// }
return new WatchItem return new WatchItem
{ {
EpisodeID = ep.ID, EpisodeID = ep.ID,

View File

@ -72,31 +72,38 @@ namespace Kyoo.Core.Controllers
{ {
sortBy ??= DefaultSort; sortBy ??= DefaultSort;
IOrderedQueryable<T> _Sort(IQueryable<T> query, Sort<T> sortBy) IOrderedQueryable<T> _SortBy(IQueryable<T> qr, Expression<Func<T, object>> sort, bool desc, bool then)
{
if (then && qr is IOrderedQueryable<T> qro)
{
return desc
? qro.ThenByDescending(sort)
: qro.ThenBy(sort);
}
return desc
? qr.OrderByDescending(sort)
: qr.OrderBy(sort);
}
IOrderedQueryable<T> _Sort(IQueryable<T> query, Sort<T> sortBy, bool then)
{ {
switch (sortBy) switch (sortBy)
{ {
case Sort<T>.Default: case Sort<T>.Default:
return Sort(query, DefaultSort); return _Sort(query, DefaultSort, then);
case Sort<T>.By(var key, var desc): case Sort<T>.By(var key, var desc):
return desc return _SortBy(query, x => EF.Property<T>(x, key), desc, then);
? query.OrderByDescending(x => EF.Property<object>(x, key)) case Sort<T>.Conglomerate(var sorts):
: query.OrderBy(x => EF.Property<T>(x, key)); IOrderedQueryable<T> nQuery = _Sort(query, sorts.First(), false);
case Sort<T>.Conglomerate(var keys): foreach (Sort<T> sort in sorts.Skip(1))
IOrderedQueryable<T> nQuery = _Sort(query, keys[0]); nQuery = _Sort(nQuery, sort, true);
foreach ((string key, bool desc) in keys.Skip(1))
{
nQuery = desc
? nQuery.ThenByDescending(x => EF.Property<object>(x, key))
: nQuery.ThenBy(x => EF.Property<object>(x, key));
}
return nQuery; return nQuery;
default: default:
// The language should not require me to do this... // The language should not require me to do this...
throw new SwitchExpressionException(); throw new SwitchExpressionException();
} }
} }
return _Sort(query, sortBy).ThenBy(x => x.ID); return _Sort(query, sortBy, false).ThenBy(x => x.ID);
} }
private static Func<Expression, Expression, BinaryExpression> _GetComparisonExpression( private static Func<Expression, Expression, BinaryExpression> _GetComparisonExpression(
@ -133,22 +140,24 @@ namespace Kyoo.Core.Controllers
T reference, T reference,
bool next = true) bool next = true)
{ {
if (sort is Sort<T>.Default)
sort = DefaultSort;
// x => // x =>
ParameterExpression x = Expression.Parameter(typeof(T), "x"); ParameterExpression x = Expression.Parameter(typeof(T), "x");
ConstantExpression referenceC = Expression.Constant(reference, typeof(T)); ConstantExpression referenceC = Expression.Constant(reference, typeof(T));
IEnumerable<Sort<T>.By> _GetSortsBy(Sort<T> sort)
{
return sort switch
{
Sort<T>.Default => _GetSortsBy(DefaultSort),
Sort<T>.By @sortBy => new[] { sortBy },
Sort<T>.Conglomerate(var list) => list.SelectMany(_GetSortsBy),
_ => Array.Empty<Sort<T>.By>(),
};
}
// Don't forget that every sorts must end with a ID sort (to differenciate equalities). // Don't forget that every sorts must end with a ID sort (to differenciate equalities).
Sort<T>.By id = new(x => x.ID); Sort<T>.By id = new(x => x.ID);
IEnumerable<Sort<T>.By> sorts = _GetSortsBy(sort).Append(id);
IEnumerable<Sort<T>.By> sorts = (sort switch
{
Sort<T>.By @sortBy => new[] { sortBy },
Sort<T>.Conglomerate(var list) => list,
_ => Array.Empty<Sort<T>.By>(),
}).Append(id);
BinaryExpression filter = null; BinaryExpression filter = null;
List<Sort<T>.By> previousSteps = new(); List<Sort<T>.By> previousSteps = new();
@ -278,10 +287,10 @@ namespace Kyoo.Core.Controllers
if (limit.AfterID != null) if (limit.AfterID != null)
{ {
T reference = await Get(limit.AfterID.Value); T reference = await Get(limit.AfterID.Value);
query = query.Where(KeysetPaginatate(sort, reference)); query = query.Where(KeysetPaginatate(sort, reference, !limit.Reverse));
} }
if (limit.Count > 0) if (limit.Limit > 0)
query = query.Take(limit.Count); query = query.Take(limit.Limit);
return await query.ToListAsync(); return await query.ToListAsync();
} }

View File

@ -120,7 +120,7 @@ namespace Kyoo.Core.Api
pagination pagination
); );
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>

View File

@ -44,9 +44,14 @@ namespace Kyoo.Core.Api
{ {
if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary<string, string> where) if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary<string, string> where)
{ {
where.Remove("fields"); Dictionary<string, string> nWhere = new(where, StringComparer.InvariantCultureIgnoreCase);
nWhere.Remove("fields");
nWhere.Remove("afterID");
nWhere.Remove("limit");
nWhere.Remove("reverse");
foreach ((string key, _) in context.ActionArguments) foreach ((string key, _) in context.ActionArguments)
where.Remove(key); nWhere.Remove(key);
context.ActionArguments["where"] = nWhere;
} }
List<string> fields = context.HttpContext.Request.Query["fields"] List<string> fields = context.HttpContext.Request.Query["fields"]

View File

@ -89,7 +89,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Genre>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Genre>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }

View File

@ -95,7 +95,7 @@ namespace Kyoo.Core.Api
slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sort, pagination) slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sort, pagination)
); );
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }

View File

@ -90,7 +90,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Studio>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Studio>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }

View File

@ -93,7 +93,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -128,7 +128,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }

View File

@ -158,7 +158,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Episode>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Episode>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }

View File

@ -92,7 +92,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -127,7 +127,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -165,7 +165,7 @@ namespace Kyoo.Core.Api
slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination) slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination)
); );
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }

View File

@ -89,7 +89,7 @@ namespace Kyoo.Core.Api
pagination pagination
); );
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }

View File

@ -93,7 +93,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Season>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Season>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>

View File

@ -97,7 +97,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -132,7 +132,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -166,7 +166,7 @@ namespace Kyoo.Core.Api
id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination), id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination),
slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination) slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination)
); );
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -201,7 +201,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -255,7 +255,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -290,7 +290,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Count); return Page(resources, pagination.Limit);
} }
} }
} }