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;
+ }));
});
}