API: Creating a common thumbs api

This commit is contained in:
Zoe Roux 2021-09-21 21:13:03 +02:00
parent 41cbc50940
commit 9b3eb7fede
7 changed files with 224 additions and 179 deletions

View File

@ -19,6 +19,7 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq.Expressions;
using JetBrains.Annotations;
namespace Kyoo.Abstractions.Models.Utils
@ -86,6 +87,29 @@ namespace Kyoo.Abstractions.Models.Utils
: slugFunc(_slug);
}
/// <summary>
/// Return true if this <see cref="Identifier"/> match a resource.
/// </summary>
/// <param name="resource">The resource to match</param>
/// <returns>
/// <c>true</c> if the <paramref name="resource"/> match this identifier, <c>false</c> otherwise.
/// </returns>
public bool IsSame(IResource resource)
{
return Match(
id => resource.ID == id,
slug => resource.Slug == slug
);
}
public Expression<Func<T, bool>> IsSame<T>()
where T : IResource
{
return _id.HasValue
? x => x.ID == _id
: x => x.Slug == _slug;
}
public class IdentifierConvertor : TypeConverter
{
/// <inheritdoc />

View File

@ -31,14 +31,14 @@ namespace Kyoo.Abstractions.Controllers
/// <summary>
/// Where to start? Using the given sort.
/// </summary>
public int AfterID { get; }
public int? AfterID { get; }
/// <summary>
/// Create a new <see cref="Pagination"/> instance.
/// </summary>
/// <param name="count">Set the <see cref="Count"/> value</param>
/// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param>
public Pagination(int count, int afterID = 0)
public Pagination(int count, int? afterID = null)
{
Count = count;
AfterID = afterID;

View File

@ -179,9 +179,9 @@ namespace Kyoo.Core.Controllers
query = sort.Descendant ? query.OrderByDescending(sortKey) : query.OrderBy(sortKey);
if (limit.AfterID != 0)
if (limit.AfterID != null)
{
TValue after = await get(limit.AfterID);
TValue after = await get(limit.AfterID.Value);
Expression key = Expression.Constant(sortKey.Compile()(after), sortExpression.Type);
query = query.Where(Expression.Lambda<Func<TValue, bool>>(
ApiHelper.StringCompatibleExpression(Expression.GreaterThan, sortExpression, key),

View File

@ -22,7 +22,6 @@ using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Models.Options;
@ -40,111 +39,56 @@ namespace Kyoo.Core.Api
[Route("api/collection", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(CollectionApi))]
public class CollectionApi : CrudApi<Collection>
public class CollectionApi : CrudThumbsApi<Collection>
{
/// <summary>
/// The library manager used to modify or retrieve information about the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used to send images.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// The thumbnail manager used to retrieve images paths.
/// </summary>
private readonly IThumbnailsManager _thumbs;
public CollectionApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs,
IOptions<BasicOptions> options)
: base(libraryManager.CollectionRepository, options.Value.PublicUrl)
: base(libraryManager.CollectionRepository, files, thumbs, options.Value.PublicUrl)
{
_libraryManager = libraryManager;
_files = files;
_thumbs = thumbs;
}
/// <summary>
/// Get shows in collection (via id)
/// Get shows in collection
/// </summary>
/// <remarks>
/// Lists the shows that are contained in the collection with the given id.
/// Lists the shows that are contained in the collection with the given id or slug.
/// </remarks>
/// <param name="id">The ID of the <see cref="Collection"/>.</param>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given ID could be found.</response>
[HttpGet("{id:int}/shows")]
[HttpGet("{id:int}/show", Order = AlternativeRoute)]
[HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(int id,
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.ID == id)),
ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(identifier.IsSame)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(id) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get shows in collection (via slug)
/// </summary>
/// <remarks>
/// Lists the shows that are contained in the collection with the given slug.
/// </remarks>
/// <param name="slug">The slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given slug could be found.</response>
[HttpGet("{slug}/shows")]
[HttpGet("{slug}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(slug) == null)
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound();
return Page(resources, limit);
}
@ -158,108 +102,39 @@ namespace Kyoo.Core.Api
/// Get libraries containing this collection
/// </summary>
/// <remarks>
/// Lists the libraries that contain the collection with the given id.
/// Lists the libraries that contain the collection with the given id or slug.
/// </remarks>
/// <param name="slug">The slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort libraries by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <returns>A page of shows.</returns>
/// <param name="limit">The number of libraries to return.</param>
/// <param name="afterID">An optional library's ID to start the query from this specific item.</param>
/// <returns>A page of libraries.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given slug could be found.</response>
[HttpGet("{id:int}/libraries")]
[HttpGet("{id:int}/library", Order = AlternativeRoute)]
/// <response code="404">No collection with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/libraries")]
[HttpGet("{identifier:id}/library", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Library>>> GetLibraries(int id,
public async Task<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.ID == id)),
ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(identifier.IsSame)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(id) == null)
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/libraries")]
[HttpGet("{slug}/library", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Library>>> GetLibraries(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.Slug == slug)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/poster")]
public async Task<IActionResult> GetPoster(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Poster));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/logo")]
public async Task<IActionResult> GetLogo(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Logo));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/backdrop")]
[HttpGet("{slug}/thumbnail")]
public async Task<IActionResult> GetBackdrop(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Thumbnail));
}
catch (ItemNotFoundException)
{
return NotFound();
return BadRequest(new RequestError(ex.Message));
}
}
}

View File

@ -42,7 +42,7 @@ namespace Kyoo.Core.Api
/// <summary>
/// The repository of the resource, used to retrieve, save and do operations on the baking store.
/// </summary>
private readonly IRepository<T> _repository;
protected IRepository<T> Repository { get; }
/// <summary>
/// The base URL of Kyoo. This will be used to create links for images and
@ -61,7 +61,7 @@ namespace Kyoo.Core.Api
/// </param>
public CrudApi(IRepository<T> repository, Uri baseURL)
{
_repository = repository;
Repository = repository;
BaseURL = baseURL;
}
@ -100,8 +100,8 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<T>> Get(Identifier identifier)
{
T ret = await identifier.Match(
id => _repository.GetOrDefault(id),
slug => _repository.GetOrDefault(slug)
id => Repository.GetOrDefault(id),
slug => Repository.GetOrDefault(slug)
);
if (ret == null)
return NotFound();
@ -125,7 +125,7 @@ namespace Kyoo.Core.Api
{
try
{
return await _repository.GetCount(ApiHelper.ParseWhere<T>(where));
return await Repository.GetCount(ApiHelper.ParseWhere<T>(where));
}
catch (ArgumentException ex)
{
@ -140,9 +140,9 @@ namespace Kyoo.Core.Api
/// Get all resources that match the given filter.
/// </remarks>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="afterID">Where the pagination should start.</param>
/// <param name="where">Filter the returned items.</param>
/// <param name="limit">How many items per page should be returned.</param>
/// <param name="afterID">Where the pagination should start.</param>
/// <returns>A list of resources that match every filters.</returns>
/// <response code="400">Invalid filters or sort information.</response>
[HttpGet]
@ -151,13 +151,13 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<ActionResult<Page<T>>> GetAll(
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
[FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{
try
{
ICollection<T> resources = await _repository.GetAll(ApiHelper.ParseWhere<T>(where),
ICollection<T> resources = await Repository.GetAll(ApiHelper.ParseWhere<T>(where),
new Sort<T>(sortBy),
new Pagination(limit, afterID));
@ -188,7 +188,7 @@ namespace Kyoo.Core.Api
{
try
{
return await _repository.Create(resource);
return await Repository.Create(resource);
}
catch (ArgumentException ex)
{
@ -196,7 +196,7 @@ namespace Kyoo.Core.Api
}
catch (DuplicatedItemException)
{
T existing = await _repository.GetOrDefault(resource.Slug);
T existing = await Repository.GetOrDefault(resource.Slug);
return Conflict(existing);
}
}
@ -225,11 +225,11 @@ namespace Kyoo.Core.Api
try
{
if (resource.ID > 0)
return await _repository.Edit(resource, resetOld);
return await Repository.Edit(resource, resetOld);
T old = await _repository.Get(resource.Slug);
T old = await Repository.Get(resource.Slug);
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld);
return await Repository.Edit(resource, resetOld);
}
catch (ItemNotFoundException)
{
@ -255,8 +255,8 @@ namespace Kyoo.Core.Api
try
{
await identifier.Match(
id => _repository.Delete(id),
slug => _repository.Delete(slug)
id => Repository.Delete(id),
slug => Repository.Delete(slug)
);
}
catch (ItemNotFoundException)
@ -284,7 +284,7 @@ namespace Kyoo.Core.Api
{
try
{
await _repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
await Repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
}
catch (ArgumentException ex)
{

View File

@ -0,0 +1,146 @@
// 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.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
public class CrudThumbsApi<T> : CrudApi<T>
where T : class, IResource, IThumbnails
{
private readonly IFileSystem _files;
private readonly IThumbnailsManager _thumbs;
public CrudThumbsApi(IRepository<T> repository,
IFileSystem files,
IThumbnailsManager thumbs,
Uri baseURL)
: base(repository, baseURL)
{
_files = files;
_thumbs = thumbs;
}
/// <summary>
/// Get Image
/// </summary>
/// <remarks>
/// Get an image for the specified item.
/// List of commonly available images:
/// <list type="bullet">
/// <item>
/// <description>Poster: Image 0, also available at /poster</description>
/// </item>
/// <item>
/// <description>Thumbnail: Image 1, also available at /thumbnail</description>
/// </item>
/// <item>
/// <description>Logo: Image 3, also available at /logo</description>
/// </item>
/// </list>
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="image">The number of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/image-{image:int}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetImage(Identifier identifier, int image)
{
T resource = await identifier.Match(
id => Repository.GetOrDefault(id),
slug => Repository.GetOrDefault(slug)
);
if (resource == null)
return NotFound();
string path = await _thumbs.GetImagePath(resource, Images.Poster);
return _files.FileResult(path);
}
/// <summary>
/// Get Poster
/// </summary>
/// <remarks>
/// Get the poster for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/poster", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetPoster(Identifier identifier)
{
return GetImage(identifier, Images.Poster);
}
/// <summary>
/// Get Logo
/// </summary>
/// <remarks>
/// Get the logo for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/logo", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetLogo(Identifier identifier)
{
return GetImage(identifier, Images.Logo);
}
/// <summary>
/// Get Thumbnail
/// </summary>
/// <remarks>
/// Get the thumbnail for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
[HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)]
public Task<IActionResult> GetBackdrop(Identifier identifier)
{
return GetImage(identifier, Images.Thumbnail);
}
}
}

View File

@ -80,11 +80,11 @@ namespace Kyoo.Swagger
return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute;
return true;
});
options.SchemaGenerator.Settings.TypeMappers
.Add(new PrimitiveTypeMapper(
typeof(Identifier),
x => x.Type = JsonObjectType.String | JsonObjectType.Integer)
);
options.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x =>
{
x.IsNullableRaw = false;
x.Type = JsonObjectType.String | JsonObjectType.Integer;
}));
});
}