Add seed in random queries next url

This commit is contained in:
Zoe Roux 2023-10-26 23:47:06 +02:00
parent 76d0c53cc1
commit 97de98b89a
15 changed files with 119 additions and 40 deletions

View File

@ -42,12 +42,12 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The link of the previous page.
/// </summary>
public string Previous { get; }
public string? Previous { get; }
/// <summary>
/// The link of the next page.
/// </summary>
public string Next { get; }
public string? Next { get; }
/// <summary>
/// The number of items in the current page.

View File

@ -17,6 +17,7 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@ -57,7 +58,7 @@ namespace Kyoo.Abstractions.Controllers
public record Conglomerate(params Sort<T>[] List) : Sort<T>;
/// <summary>Sort randomly items</summary>
public record Random(int seed) : Sort<T>;
public record Random(uint seed) : Sort<T>;
/// <summary>The default sort method for the given type.</summary>
public record Default : Sort<T>;
@ -66,19 +67,20 @@ namespace Kyoo.Abstractions.Controllers
/// 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>
/// <param name="seed">The random seed.</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 Sort<T> From(string sortBy)
public static Sort<T> From(string? sortBy, uint seed)
{
if (string.IsNullOrEmpty(sortBy) || sortBy == "default")
return new Default();
if (sortBy == "random")
return new Random(new System.Random().Next(int.MinValue, int.MaxValue));
return new Random(seed);
if (sortBy.Contains(','))
return new Conglomerate(sortBy.Split(',').Select(From).ToArray());
return new Conglomerate(sortBy.Split(',').Select(x => From(x, seed)).ToArray());
if (sortBy.StartsWith("random:"))
return new Random(int.Parse(sortBy["random:".Length..]));
return new Random(uint.Parse(sortBy["random:".Length..]));
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
@ -87,11 +89,11 @@ namespace Kyoo.Abstractions.Controllers
"desc" => true,
"asc" => false,
null => false,
_ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.")
_ => throw new ValidationException($"The sort order, if set, should be :asc or :desc but it was :{order}.")
};
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.");
throw new ValidationException("The given sort key is not valid.");
return new By(property.Name, desendant);
}
}

View File

@ -23,6 +23,7 @@ using Autofac;
using Kyoo.Abstractions;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Api;
using Kyoo.Core.Controllers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
@ -69,6 +70,7 @@ namespace Kyoo.Core
services.AddMvcCore(options =>
{
options.Filters.Add<ExceptionFilter>();
options.ModelBinderProviders.Insert(0, new SortBinder.Provider());
})
.AddNewtonsoftJson(x =>
{

View File

@ -17,6 +17,7 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
@ -50,6 +51,9 @@ namespace Kyoo.Core
case ArgumentException ex:
context.Result = new BadRequestObjectResult(new RequestError(ex.Message));
break;
case ValidationException ex:
context.Result = new BadRequestObjectResult(new RequestError(ex.Message));
break;
case ItemNotFoundException ex:
context.Result = new NotFoundObjectResult(new RequestError(ex.Message));
break;

View File

@ -19,6 +19,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Kyoo.Abstractions.Models;
using Microsoft.AspNetCore.Mvc;
@ -42,14 +43,23 @@ namespace Kyoo.Core.Api
protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit)
where TResult : IResource
{
Dictionary<string, string> query = Request.Query.ToDictionary(
x => x.Key,
x => x.Value.ToString(),
StringComparer.InvariantCultureIgnoreCase
);
// If the query was sorted randomly, add the seed to the url to get reproducible links (next,prev,first...)
if (query.ContainsKey("sortBy"))
{
object seed = HttpContext.Items["seed"]!;
query["sortBy"] = Regex.Replace(query["sortBy"], "random(?!:)", $"random:{seed}");
}
return new Page<TResult>(
resources,
Request.Path,
Request.Query.ToDictionary(
x => x.Key,
x => x.Value.ToString(),
StringComparer.InvariantCultureIgnoreCase
),
query,
limit
);
}

View File

@ -113,13 +113,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<ActionResult<Page<T>>> GetAll(
[FromQuery] string sortBy,
[FromQuery] Sort<T> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<T> resources = await Repository.GetAll(
ApiHelper.ParseWhere<T>(where),
Sort<T>.From(sortBy),
sortBy,
pagination
);

View File

@ -0,0 +1,65 @@
// 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 <https://www.gnu.org/licenses/>.
using System;
using System.Reflection;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
namespace Kyoo.Core.Api;
public class SortBinder : IModelBinder
{
private readonly Random _rng = new();
public Task BindModelAsync(ModelBindingContext bindingContext)
{
ValueProviderResult sortBy = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
uint seed = BitConverter.ToUInt32(
BitConverter.GetBytes(_rng.Next(int.MinValue, int.MaxValue)),
0
);
try
{
object sort = bindingContext.ModelType.GetMethod(nameof(Sort<object>.From))!
.Invoke(null, new object?[] { sortBy.FirstValue, seed })!;
bindingContext.Result = ModelBindingResult.Success(sort);
bindingContext.HttpContext.Items["seed"] = seed;
return Task.CompletedTask;
}
catch (TargetInvocationException ex)
{
throw ex.InnerException!;
}
}
public class Provider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType.Name == "Sort`1")
{
return new BinderTypeModelBinder(typeof(SortBinder));
}
return null!;
}
}
}

View File

@ -81,16 +81,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<PeopleRole>>> GetRoles(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<PeopleRole> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
Expression<Func<PeopleRole, bool>>? whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
Sort<PeopleRole> sort = Sort<PeopleRole>.From(sortBy);
ICollection<PeopleRole> resources = await identifier.Match(
id => _libraryManager.GetRolesFromPeople(id, whereQuery, sort, pagination),
slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sort, pagination)
id => _libraryManager.GetRolesFromPeople(id, whereQuery, sortBy, pagination),
slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sortBy, pagination)
);
return Page(resources, pagination.Limit);

View File

@ -78,13 +78,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<Show> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug)),
Sort<Show>.From(sortBy),
sortBy,
pagination
);

View File

@ -79,13 +79,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<Show> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Collection>(x => x.Collections!)),
Sort<Show>.From(sortBy),
sortBy,
pagination
);

View File

@ -131,13 +131,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<Collection> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Movie>(x => x.Movies!)),
Sort<Collection>.From(sortBy),
sortBy,
pagination
);

View File

@ -79,13 +79,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Episode>>> GetEpisode(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<Episode> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)),
Sort<Episode>.From(sortBy),
sortBy,
pagination
);

View File

@ -81,13 +81,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Season>>> GetSeasons(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<Season> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)),
Sort<Season>.From(sortBy),
sortBy,
pagination
);
@ -116,13 +116,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Episode>>> GetEpisodes(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<Episode> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)),
Sort<Episode>.From(sortBy),
sortBy,
pagination
);
@ -151,16 +151,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<PeopleRole>>> GetPeople(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<PeopleRole> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
Expression<Func<PeopleRole, bool>>? whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
Sort<PeopleRole> sort = Sort<PeopleRole>.From(sortBy);
ICollection<PeopleRole> resources = await identifier.Match(
id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination),
slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination)
id => _libraryManager.GetPeopleFromShow(id, whereQuery, sortBy, pagination),
slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sortBy, pagination)
);
return Page(resources, pagination.Limit);
}
@ -203,13 +202,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Sort<Collection> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
Sort<Collection>.From(sortBy),
sortBy,
pagination
);

View File

@ -23,7 +23,6 @@ using System.Linq.Expressions;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;

View File

@ -18,7 +18,6 @@
using System;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using EFCore.NamingConventions.Internal;