From 9b3eb7fede561e6bbb015b55efad2fbc8acb5891 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 21 Sep 2021 21:13:03 +0200 Subject: [PATCH] API: Creating a common thumbs api --- .../Models/Utils/Identifier.cs | 24 +++ .../Models/Utils/Pagination.cs | 4 +- .../Repositories/LocalRepository.cs | 4 +- src/Kyoo.Core/Views/CollectionApi.cs | 181 +++--------------- src/Kyoo.Core/Views/Helper/CrudApi.cs | 34 ++-- src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 146 ++++++++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 10 +- 7 files changed, 224 insertions(+), 179 deletions(-) create mode 100644 src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs diff --git a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs index 356ff3d8..416bfabf 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -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); } + /// + /// Return true if this match a resource. + /// + /// The resource to match + /// + /// true if the match this identifier, false otherwise. + /// + public bool IsSame(IResource resource) + { + return Match( + id => resource.ID == id, + slug => resource.Slug == slug + ); + } + + public Expression> IsSame() + where T : IResource + { + return _id.HasValue + ? x => x.ID == _id + : x => x.Slug == _slug; + } + public class IdentifierConvertor : TypeConverter { /// diff --git a/src/Kyoo.Abstractions/Models/Utils/Pagination.cs b/src/Kyoo.Abstractions/Models/Utils/Pagination.cs index 652991a1..e52bbf63 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Pagination.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Pagination.cs @@ -31,14 +31,14 @@ namespace Kyoo.Abstractions.Controllers /// /// Where to start? Using the given sort. /// - public int AfterID { get; } + public int? AfterID { get; } /// /// Create a new instance. /// /// Set the value /// Set the value. If not specified, it will start from the start - public Pagination(int count, int afterID = 0) + public Pagination(int count, int? afterID = null) { Count = count; AfterID = afterID; diff --git a/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index efc017d7..3cc80036 100644 --- a/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -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>( ApiHelper.StringCompatibleExpression(Expression.GreaterThan, sortExpression, key), diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index e920f52c..b9a627cf 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -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 + public class CollectionApi : CrudThumbsApi { /// /// The library manager used to modify or retrieve information about the data store. /// private readonly ILibraryManager _libraryManager; - /// - /// The file manager used to send images. - /// - private readonly IFileSystem _files; - - /// - /// The thumbnail manager used to retrieve images paths. - /// - private readonly IThumbnailsManager _thumbs; - public CollectionApi(ILibraryManager libraryManager, IFileSystem files, IThumbnailsManager thumbs, IOptions options) - : base(libraryManager.CollectionRepository, options.Value.PublicUrl) + : base(libraryManager.CollectionRepository, files, thumbs, options.Value.PublicUrl) { _libraryManager = libraryManager; - _files = files; - _thumbs = thumbs; } /// - /// Get shows in collection (via id) + /// Get shows in collection /// /// - /// 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. /// - /// The ID of the . + /// The ID or slug of the . /// A key to sort shows by. - /// An optional show's ID to start the query from this specific item. /// An optional list of filters. /// The number of shows to return. + /// An optional show's ID to start the query from this specific item. /// A page of shows. /// The filters or the sort parameters are invalid. /// No collection with the given ID could be found. - [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>> GetShows(int id, + public async Task>> GetShows(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 30) + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.ID == id)), + ApiHelper.ParseWhere(where, x => x.Collections.Any(identifier.IsSame)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new RequestError(ex.Message)); - } - } - - /// - /// Get shows in collection (via slug) - /// - /// - /// Lists the shows that are contained in the collection with the given slug. - /// - /// The slug of the . - /// A key to sort shows by. - /// An optional show's ID to start the query from this specific item. - /// An optional list of filters. - /// The number of shows to return. - /// A page of shows. - /// The filters or the sort parameters are invalid. - /// No collection with the given slug could be found. - [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>> GetShows(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.Slug == slug)), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } @@ -158,108 +102,39 @@ namespace Kyoo.Core.Api /// Get libraries containing this collection /// /// - /// Lists the libraries that contain the collection with the given id. + /// Lists the libraries that contain the collection with the given id or slug. /// - /// The slug of the . - /// A key to sort shows by. - /// An optional show's ID to start the query from this specific item. + /// The ID or slug of the . + /// A key to sort libraries by. /// An optional list of filters. - /// The number of shows to return. - /// A page of shows. + /// The number of libraries to return. + /// An optional library's ID to start the query from this specific item. + /// A page of libraries. /// The filters or the sort parameters are invalid. - /// No collection with the given slug could be found. - [HttpGet("{id:int}/libraries")] - [HttpGet("{id:int}/library", Order = AlternativeRoute)] + /// No collection with the given ID or slug could be found. + [HttpGet("{identifier:id}/libraries")] + [HttpGet("{identifier:id}/library", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetLibraries(int id, + public async Task>> GetLibraries(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 30) + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.ID == id)), + ApiHelper.ParseWhere(where, x => x.Collections.Any(identifier.IsSame)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == 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>> GetLibraries(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.Slug == slug)), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{slug}/poster")] - public async Task GetPoster(string slug) - { - try - { - Collection collection = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Poster)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{slug}/logo")] - public async Task GetLogo(string slug) - { - try - { - Collection collection = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Logo)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{slug}/backdrop")] - [HttpGet("{slug}/thumbnail")] - public async Task GetBackdrop(string slug) - { - try - { - Collection collection = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Thumbnail)); - } - catch (ItemNotFoundException) - { - return NotFound(); + return BadRequest(new RequestError(ex.Message)); } } } diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index a90cfce1..76d5acb3 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -42,7 +42,7 @@ namespace Kyoo.Core.Api /// /// The repository of the resource, used to retrieve, save and do operations on the baking store. /// - private readonly IRepository _repository; + protected IRepository Repository { get; } /// /// The base URL of Kyoo. This will be used to create links for images and @@ -61,7 +61,7 @@ namespace Kyoo.Core.Api /// public CrudApi(IRepository repository, Uri baseURL) { - _repository = repository; + Repository = repository; BaseURL = baseURL; } @@ -100,8 +100,8 @@ namespace Kyoo.Core.Api public async Task> 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(where)); + return await Repository.GetCount(ApiHelper.ParseWhere(where)); } catch (ArgumentException ex) { @@ -140,9 +140,9 @@ namespace Kyoo.Core.Api /// Get all resources that match the given filter. /// /// Sort information about the query (sort by, sort order). - /// Where the pagination should start. /// Filter the returned items. /// How many items per page should be returned. + /// Where the pagination should start. /// A list of resources that match every filters. /// Invalid filters or sort information. [HttpGet] @@ -151,13 +151,13 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] public async Task>> GetAll( [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 20) + [FromQuery] int limit = 20, + [FromQuery] int? afterID = null) { try { - ICollection resources = await _repository.GetAll(ApiHelper.ParseWhere(where), + ICollection resources = await Repository.GetAll(ApiHelper.ParseWhere(where), new Sort(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(where)); + await Repository.DeleteAll(ApiHelper.ParseWhere(where)); } catch (ArgumentException ex) { diff --git a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs new file mode 100644 index 00000000..7d3ccc42 --- /dev/null +++ b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -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 . + +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 : CrudApi + where T : class, IResource, IThumbnails + { + private readonly IFileSystem _files; + private readonly IThumbnailsManager _thumbs; + + public CrudThumbsApi(IRepository repository, + IFileSystem files, + IThumbnailsManager thumbs, + Uri baseURL) + : base(repository, baseURL) + { + _files = files; + _thumbs = thumbs; + } + + /// + /// Get Image + /// + /// + /// Get an image for the specified item. + /// List of commonly available images: + /// + /// + /// Poster: Image 0, also available at /poster + /// + /// + /// Thumbnail: Image 1, also available at /thumbnail + /// + /// + /// Logo: Image 3, also available at /logo + /// + /// + /// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint. + /// + /// The ID or slug of the resource to get the image for. + /// The number of the image to retrieve. + /// The image asked. + /// + /// No item exist with the specific identifier or the image does not exists on kyoo. + /// + [HttpGet("{identifier:id}/image-{image:int}")] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task 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); + } + + /// + /// Get Poster + /// + /// + /// Get the poster for the specified item. + /// + /// The ID or slug of the resource to get the image for. + /// The image asked. + /// + /// No item exist with the specific identifier or the image does not exists on kyoo. + /// + [HttpGet("{identifier:id}/poster", Order = AlternativeRoute)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public Task GetPoster(Identifier identifier) + { + return GetImage(identifier, Images.Poster); + } + + /// + /// Get Logo + /// + /// + /// Get the logo for the specified item. + /// + /// The ID or slug of the resource to get the image for. + /// The image asked. + /// + /// No item exist with the specific identifier or the image does not exists on kyoo. + /// + [HttpGet("{identifier:id}/logo", Order = AlternativeRoute)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public Task GetLogo(Identifier identifier) + { + return GetImage(identifier, Images.Logo); + } + + /// + /// Get Thumbnail + /// + /// + /// Get the thumbnail for the specified item. + /// + /// The ID or slug of the resource to get the image for. + /// The image asked. + /// + /// No item exist with the specific identifier or the image does not exists on kyoo. + /// + [HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)] + [HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)] + public Task GetBackdrop(Identifier identifier) + { + return GetImage(identifier, Images.Thumbnail); + } + } +} diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index dfc4135f..4d743b3a 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -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; + })); }); }