From bd4fd848e16ba654f9ffd9bbdffcdbdbdef32d5a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 22 Sep 2021 20:54:48 +0200 Subject: [PATCH] API: Documenting the Show API --- .../Models/Utils/Identifier.cs | 62 +- .../Repositories/PeopleRepository.cs | 15 +- src/Kyoo.Core/Views/CollectionApi.cs | 9 +- src/Kyoo.Core/Views/Helper/CrudApi.cs | 6 +- src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 4 +- src/Kyoo.Core/Views/ShowApi.cs | 580 ++++++++---------- 6 files changed, 342 insertions(+), 334 deletions(-) diff --git a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs index 416bfabf..bd83e975 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -17,9 +17,12 @@ // along with Kyoo. If not, see . using System; +using System.Collections.Generic; using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; namespace Kyoo.Abstractions.Models.Utils @@ -87,6 +90,26 @@ namespace Kyoo.Abstractions.Models.Utils : slugFunc(_slug); } + /// + /// Match a custom type to an identifier. This can be used for wrapped resources (see example for more details). + /// + /// An expression to retrieve an ID from the type . + /// An expression to retrieve a slug from the type . + /// The type to match against this identifier. + /// An expression to match the type to this identifier. + /// + /// + /// identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug) + /// + /// + public Expression> Matcher(Expression> idGetter, + Expression> slugGetter) + { + ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug); + BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self); + return Expression.Lambda>(equal); + } + /// /// Return true if this match a resource. /// @@ -102,14 +125,51 @@ namespace Kyoo.Abstractions.Models.Utils ); } + /// + /// Return an expression that return true if this match a given resource. + /// + /// The type of resource to match against. + /// + /// true if the given resource match this identifier, false otherwise. + /// public Expression> IsSame() where T : IResource { return _id.HasValue - ? x => x.ID == _id + ? x => x.ID == _id.Value : x => x.Slug == _slug; } + /// + /// Return an expression that return true if this is containing in a collection. + /// + /// An expression to retrieve the list to check. + /// The type that contain the list to check. + /// The type of resource to check this identifier against. + /// An expression to check if this is contained. + public Expression> IsContainedIn(Expression>> listGetter) + where T2 : IResource + { + MethodInfo method = typeof(Enumerable) + .GetMethods() + .Where(x => x.Name == nameof(Enumerable.Any)) + .FirstOrDefault(x => x.GetParameters().Length == 2)! + .MakeGenericMethod(typeof(T2)); + MethodCallExpression call = Expression.Call(null, method!, listGetter.Body, IsSame()); + return Expression.Lambda>(call, listGetter.Parameters); + } + + /// + public override string ToString() + { + return _id.HasValue + ? _id.Value.ToString() + : _slug; + } + + /// + /// A custom used to convert int or strings to an . + /// public class IdentifierConvertor : TypeConverter { /// diff --git a/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs b/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs index 09809426..d599d6ee 100644 --- a/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs +++ b/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs @@ -23,7 +23,6 @@ using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Database; using Kyoo.Utils; using Microsoft.EntityFrameworkCore; @@ -160,8 +159,6 @@ namespace Kyoo.Core.Controllers where, sort, limit); - if (!people.Any() && await _shows.Value.GetOrDefault(showID) == null) - throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; return people; @@ -182,8 +179,6 @@ namespace Kyoo.Core.Controllers where, sort, limit); - if (!people.Any() && await _shows.Value.GetOrDefault(showSlug) == null) - throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; return people; @@ -195,7 +190,7 @@ namespace Kyoo.Core.Controllers Sort sort = default, Pagination limit = default) { - ICollection roles = await ApplyFilters(_database.PeopleRoles + return await ApplyFilters(_database.PeopleRoles .Where(x => x.PeopleID == id) .Include(x => x.Show), y => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == y), @@ -203,9 +198,6 @@ namespace Kyoo.Core.Controllers where, sort, limit); - if (!roles.Any() && await GetOrDefault(id) == null) - throw new ItemNotFoundException(); - return roles; } /// @@ -214,7 +206,7 @@ namespace Kyoo.Core.Controllers Sort sort = default, Pagination limit = default) { - ICollection roles = await ApplyFilters(_database.PeopleRoles + return await ApplyFilters(_database.PeopleRoles .Where(x => x.People.Slug == slug) .Include(x => x.Show), id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), @@ -222,9 +214,6 @@ namespace Kyoo.Core.Controllers where, sort, limit); - if (!roles.Any() && await GetOrDefault(slug) == null) - throw new ItemNotFoundException(); - return roles; } } } diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index 22b0608e..65f49a90 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -29,7 +29,6 @@ using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using NSwag.Annotations; using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api @@ -41,7 +40,7 @@ namespace Kyoo.Core.Api [Route("api/collection", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(CollectionApi))] - [ApiDefinition("Collection", Group = ResourceGroup)] + [ApiDefinition("Collections", Group = ResourceGroup)] public class CollectionApi : CrudThumbsApi { /// @@ -100,7 +99,8 @@ namespace Kyoo.Core.Api ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Collections.Any(identifier.IsSame)), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); @@ -143,7 +143,8 @@ namespace Kyoo.Core.Api ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Collections.Any(identifier.IsSame)), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index 76d5acb3..6e11ed6b 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -157,9 +157,11 @@ namespace Kyoo.Core.Api { try { - ICollection resources = await Repository.GetAll(ApiHelper.ParseWhere(where), + ICollection resources = await Repository.GetAll( + ApiHelper.ParseWhere(where), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); return Page(resources, limit); } diff --git a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 95a5782c..68acd112 100644 --- a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -70,7 +70,7 @@ namespace Kyoo.Core.Api } /// - /// Get Image + /// Get image /// /// /// Get an image for the specified item. @@ -97,7 +97,7 @@ namespace Kyoo.Core.Api ); if (resource == null) return NotFound(); - string path = await _thumbs.GetImagePath(resource, Images.Poster); + string path = await _thumbs.GetImagePath(resource, image); return _files.FileResult(path); } diff --git a/src/Kyoo.Core/Views/ShowApi.cs b/src/Kyoo.Core/Views/ShowApi.cs index 4ebe06da..f819e9ab 100644 --- a/src/Kyoo.Core/Views/ShowApi.cs +++ b/src/Kyoo.Core/Views/ShowApi.cs @@ -20,455 +20,411 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Permissions; +using Kyoo.Abstractions.Models.Utils; using Kyoo.Core.Models.Options; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { - [Route("api/show")] + /// + /// Information about one or multiple . + /// [Route("api/shows")] - [Route("api/movie")] - [Route("api/movies")] + [Route("api/show", Order = AlternativeRoute)] + [Route("api/movie", Order = AlternativeRoute)] + [Route("api/movies", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(ShowApi))] - public class ShowApi : CrudApi + [ApiDefinition("Shows", Group = ResourceGroup)] + public class ShowApi : CrudThumbsApi { + /// + /// The library manager used to modify or retrieve information about the data store. + /// private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _files; - private readonly IThumbnailsManager _thumbs; + /// + /// The file manager used to send images and fonts. + /// + private readonly IFileSystem _files; + + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information about the data store. + /// + /// The file manager used to send images and fonts. + /// The thumbnail manager used to retrieve images paths. + /// + /// Options used to retrieve the base URL of Kyoo. + /// public ShowApi(ILibraryManager libraryManager, IFileSystem files, IThumbnailsManager thumbs, IOptions options) - : base(libraryManager.ShowRepository, options.Value.PublicUrl) + : base(libraryManager.ShowRepository, files, thumbs, options.Value.PublicUrl) { _libraryManager = libraryManager; _files = files; - _thumbs = thumbs; } - [HttpGet("{showID:int}/season")] - [HttpGet("{showID:int}/seasons")] + /// + /// Get seasons of this show + /// + /// + /// List the seasons that are part of the specified show. + /// + /// The ID or slug of the . + /// A key to sort seasons by. + /// An optional list of filters. + /// The number of seasons to return. + /// An optional season's ID to start the query from this specific item. + /// A page of seasons. + /// The filters or the sort parameters are invalid. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/seasons")] + [HttpGet("{identifier:id}/season", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetSeasons(int showID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetSeasons(Identifier identifier, [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 _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.ShowID == showID), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowID, x => x.Show.Slug)), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); - if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == 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 }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{slug}/season")] - [HttpGet("{slug}/seasons")] + /// + /// Get episodes of this show + /// + /// + /// List the episodes that are part of the specified show. + /// + /// The ID or slug of the . + /// A key to sort episodes by. + /// An optional list of filters. + /// The number of episodes to return. + /// An optional episode's ID to start the query from this specific item. + /// A page of episodes. + /// The filters or the sort parameters are invalid. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/episodes")] + [HttpGet("{identifier:id}/episode", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetSeasons(string slug, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetEpisodes(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 20) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Show.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("{showID:int}/episode")] - [HttpGet("{showID:int}/episodes")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisodes(int showID, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 50) + [FromQuery] int limit = 50, + [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.ShowID == showID), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowID, x => x.Show.Slug)), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); - if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == 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 }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{slug}/episode")] - [HttpGet("{slug}/episodes")] + /// + /// Get people that made this show + /// + /// + /// List staff members that made this show. + /// + /// The ID or slug of the . + /// A key to sort staff members by. + /// An optional list of filters. + /// The number of people to return. + /// An optional person's ID to start the query from this specific item. + /// A page of people. + /// The filters or the sort parameters are invalid. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/people")] + [HttpGet("{identifier:id}/staff", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetEpisodes(string slug, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetPeople(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 50) + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) { try { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Show.Slug == slug), - new Sort(sortBy), - new Pagination(limit, afterID)); + Expression> whereQuery = ApiHelper.ParseWhere(where); + Sort sort = new(sortBy); + Pagination pagination = new(limit, afterID); - if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) + ICollection resources = await identifier.Match( + id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination), + slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination) + ); + + if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{showID:int}/people")] + /// + /// Get genres of this show + /// + /// + /// List the genres that represent this show. + /// + /// The ID or slug of the . + /// A key to sort genres by. + /// An optional list of filters. + /// The number of genres to return. + /// An optional genre's ID to start the query from this specific item. + /// A page of genres. + /// The filters or the sort parameters are invalid. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/genres")] + [HttpGet("{identifier:id}/genre", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetPeople(int showID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetGenres(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetPeopleFromShow(showID, - ApiHelper.ParseWhere(where), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{slug}/people")] - [PartialPermission(Kind.Read)] - public async Task>> GetPeople(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetPeopleFromShow(slug, - ApiHelper.ParseWhere(where), - 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("{showID:int}/genre")] - [HttpGet("{showID:int}/genres")] - [PartialPermission(Kind.Read)] - public async Task>> GetGenres(int showID, - [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.Shows.Any(y => y.ID == showID)), + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); - if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == 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 }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{slug}/genre")] - [HttpGet("{slug}/genres")] + /// + /// Get studio that made the show + /// + /// + /// Get the studio that made the show. + /// + /// The ID or slug of the . + /// The studio that made the show. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/studio")] [PartialPermission(Kind.Read)] - public async Task>> GetGenre(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetStudio(Identifier identifier) { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Shows.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("{showID:int}/studio")] - [PartialPermission(Kind.Read)] - public async Task> GetStudio(int showID) - { - try - { - return await _libraryManager.Get(x => x.Shows.Any(y => y.ID == showID)); - } - catch (ItemNotFoundException) - { + Studio studio = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Shows)); + if (studio == null) return NotFound(); - } + return studio; } - [HttpGet("{slug}/studio")] + /// + /// Get libraries containing this show. + /// + /// + /// List the libraries that contain this show. If this show is contained in a collection that is contained in + /// a library, this library will be returned too. + /// + /// The ID or slug of the . + /// A key to sort libraries by. + /// An optional list of filters. + /// 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 show 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> GetStudio(string slug) - { - try - { - return await _libraryManager.Get(x => x.Shows.Any(y => y.Slug == slug)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{showID:int}/library")] - [HttpGet("{showID:int}/libraries")] - [PartialPermission(Kind.Read)] - public async Task>> GetLibraries(int showID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + 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.Shows.Any(y => y.ID == showID)), + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); - if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == 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 }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{slug}/library")] - [HttpGet("{slug}/libraries")] + /// + /// Get collections containing this show. + /// + /// + /// List the collections that contain this show. + /// + /// The ID or slug of the . + /// A key to sort collections by. + /// An optional list of filters. + /// The number of collections to return. + /// An optional collection's ID to start the query from this specific item. + /// A page of collections. + /// The filters or the sort parameters are invalid. + /// No show with the given ID or slug could be found. + [HttpGet("{identifier:id}/collection")] + [HttpGet("{identifier:id}/collections")] [PartialPermission(Kind.Read)] - public async Task>> GetLibraries(string slug, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetCollections(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Shows.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("{showID:int}/collection")] - [HttpGet("{showID:int}/collections")] - [PartialPermission(Kind.Read)] - public async Task>> GetCollections(int showID, - [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.Shows.Any(y => y.ID == showID)), + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); - if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == 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 }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{slug}/collection")] - [HttpGet("{slug}/collections")] + /// + /// List fonts + /// + /// + /// List available fonts for this show. + /// + /// The ID or slug of the . + /// An object containing the name of the font followed by the url to retrieve it. + [HttpGet("{identifier:id}/fonts")] + [HttpGet("{identifier:id}/font", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetCollections(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetFonts(Identifier identifier) { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Shows.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 }); - } + Show show = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => _libraryManager.GetOrDefault(slug) + ); + if (show == null) + return NotFound(); + string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments"); + return (await _files.ListFiles(path)) + .ToDictionary( + Path.GetFileNameWithoutExtension, + x => $"{BaseURL}api/shows/{identifier}/fonts/{Path.GetFileName(x)}" + ); } - [HttpGet("{slug}/font")] - [HttpGet("{slug}/fonts")] + /// + /// Get font + /// + /// + /// Get a font file that is used in subtitles of this show. + /// + /// The ID or slug of the . + /// The name of the font to retrieve (with it's file extension). + /// A page of collections. + /// The font name is invalid. + /// No show with the given ID/slug could be found or the font does not exist. + [HttpGet("{identifier:id}/fonts/{font}")] + [HttpGet("{identifier:id}/font/{font}", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetFonts(string slug) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetFont(Identifier identifier, string font) { - try - { - Show show = await _libraryManager.Get(slug); - string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments"); - return (await _files.ListFiles(path)) - .ToDictionary(Path.GetFileNameWithoutExtension, - x => $"{BaseURL}api/shows/{slug}/fonts/{Path.GetFileName(x)}"); - } - catch (ItemNotFoundException) - { + if (font.Contains('/') || font.Contains('\\')) + return BadRequest(new RequestError("Invalid font name.")); + Show show = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => _libraryManager.GetOrDefault(slug) + ); + if (show == null) return NotFound(); - } - } - - [HttpGet("{showSlug}/font/{slug}")] - [HttpGet("{showSlug}/fonts/{slug}")] - [PartialPermission(Kind.Read)] - public async Task GetFont(string showSlug, string slug) - { - try - { - Show show = await _libraryManager.Get(showSlug); - string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments", slug); - return _files.FileResult(path); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{slug}/poster")] - public async Task GetPoster(string slug) - { - try - { - Show show = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbs.GetImagePath(show, Images.Poster)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{slug}/logo")] - public async Task GetLogo(string slug) - { - try - { - Show show = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbs.GetImagePath(show, Images.Logo)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{slug}/backdrop")] - [HttpGet("{slug}/thumbnail")] - public async Task GetBackdrop(string slug) - { - try - { - Show show = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbs.GetImagePath(show, Images.Thumbnail)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } + string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments", font); + return _files.FileResult(path); } } }