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
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using Kyoo.Utils;
@ -40,6 +39,11 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public string First { get; }
/// <summary>
/// The link of the previous page.
/// </summary>
public string Previous { get; }
/// <summary>
/// The link of the next page.
/// </summary>
@ -60,12 +64,14 @@ namespace Kyoo.Abstractions.Models
/// </summary>
/// <param name="items">The list of items in the 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="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;
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();

View File

@ -21,32 +21,42 @@ namespace Kyoo.Abstractions.Controllers
/// <summary>
/// Information about the pagination. How many items should be displayed and where to start.
/// </summary>
public readonly struct Pagination
public class Pagination
{
/// <summary>
/// The count of items to return.
/// </summary>
public int Count { get; }
public int Limit { get; set; }
/// <summary>
/// Where to start? Using the given sort.
/// </summary>
public int? AfterID { get; }
public int? AfterID { get; set; }
/// <summary>
/// Should the previous page be returned instead of the next?
/// </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>
/// Create a new <see cref="Pagination"/> instance.
/// </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="reverse">Should the previous page be returned instead of the next?</param>
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
/// <summary>
/// Implicitly create a new pagination from a limit number.
/// </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>
public static implicit operator Pagination(int limit) => new(limit);
}

View File

@ -48,36 +48,13 @@ namespace Kyoo.Abstractions.Controllers
/// </param>
public By(Expression<Func<T, object>> key, bool desendant = false)
: 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>
/// Sort by multiple keys.
/// </summary>
/// <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>
public record Default : Sort<T>;
@ -90,11 +67,24 @@ namespace Kyoo.Abstractions.Controllers
/// <returns>A <see cref="Sort{T}"/> for the given string</returns>
public static Sort<T> 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);
}
}
}

View File

@ -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<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
{
EpisodeID = ep.ID,

View File

@ -72,31 +72,38 @@ namespace Kyoo.Core.Controllers
{
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)
{
case Sort<T>.Default:
return Sort(query, DefaultSort);
return _Sort(query, DefaultSort, then);
case Sort<T>.By(var key, var desc):
return desc
? query.OrderByDescending(x => EF.Property<object>(x, key))
: query.OrderBy(x => EF.Property<T>(x, key));
case Sort<T>.Conglomerate(var keys):
IOrderedQueryable<T> nQuery = _Sort(query, keys[0]);
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 _SortBy(query, x => EF.Property<T>(x, key), desc, then);
case Sort<T>.Conglomerate(var sorts):
IOrderedQueryable<T> nQuery = _Sort(query, sorts.First(), false);
foreach (Sort<T> 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<Expression, Expression, BinaryExpression> _GetComparisonExpression(
@ -133,22 +140,24 @@ namespace Kyoo.Core.Controllers
T reference,
bool next = true)
{
if (sort is Sort<T>.Default)
sort = DefaultSort;
// x =>
ParameterExpression x = Expression.Parameter(typeof(T), "x");
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).
Sort<T>.By id = new(x => x.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);
IEnumerable<Sort<T>.By> sorts = _GetSortsBy(sort).Append(id);
BinaryExpression filter = null;
List<Sort<T>.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();
}

View File

@ -120,7 +120,7 @@ namespace Kyoo.Core.Api
pagination
);
return Page(resources, pagination.Count);
return Page(resources, pagination.Limit);
}
/// <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)
{
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)
where.Remove(key);
nWhere.Remove(key);
context.ActionArguments["where"] = nWhere;
}
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)
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)
);
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)
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)
return NotFound();
return Page(resources, pagination.Count);
return Page(resources, pagination.Limit);
}
/// <summary>
@ -128,7 +128,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
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)
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)
return NotFound();
return Page(resources, pagination.Count);
return Page(resources, pagination.Limit);
}
/// <summary>
@ -127,7 +127,7 @@ namespace Kyoo.Core.Api
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound();
return Page(resources, pagination.Count);
return Page(resources, pagination.Limit);
}
/// <summary>
@ -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);
}
}
}

View File

@ -89,7 +89,7 @@ namespace Kyoo.Core.Api
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)
return NotFound();
return Page(resources, pagination.Count);
return Page(resources, pagination.Limit);
}
/// <summary>

View File

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