API: Documenting the search API

This commit is contained in:
Zoe Roux 2021-10-02 18:31:25 +02:00
parent cb6ea80adb
commit 40e32a1689
19 changed files with 282 additions and 147 deletions

View File

@ -23,8 +23,10 @@ namespace Kyoo.Abstractions.Models.Attributes
{
/// <summary>
/// An attribute to specify on apis to specify it's documentation's name and category.
/// If this is applied on a method, the specified method will be exploded from the controller's page and be
/// included on the specified tag page.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiDefinitionAttribute : Attribute
{
/// <summary>

View File

@ -54,14 +54,9 @@ namespace Kyoo.Abstractions.Models.Permissions
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
/// <param name="type">The type of the action</param>
public PartialPermissionAttribute(string type)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
}

View File

@ -91,17 +91,16 @@ namespace Kyoo.Abstractions.Models.Permissions
/// </summary>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
/// <param name="permission">The kind of permission needed.</param>
/// <param name="permission">
/// The kind of permission needed.
/// </param>
/// <param name="group">
/// The group of this permission (allow grouped permission like overall.read
/// for all read permissions of this group).
/// </param>
public PermissionAttribute(string type, Kind permission, Group group = Group.Overall)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
Kind = permission;
Group = group;

View File

@ -36,7 +36,7 @@ namespace Kyoo.Core.Api
[Route("api/task", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(TaskApi), Group = Group.Admin)]
[PartialPermission("Task", Group = Group.Admin)]
[ApiDefinition("Tasks", Group = AdminGroup)]
public class TaskApi : ControllerBase
{

View File

@ -37,7 +37,7 @@ namespace Kyoo.Core.Api
[Route("api/genres")]
[Route("api/genre", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(GenreApi))]
[PartialPermission(nameof(Genre))]
[ApiDefinition("Genres", Group = MetadataGroup)]
public class GenreApi : CrudApi<Genre>
{

View File

@ -34,7 +34,7 @@ namespace Kyoo.Core.Api
[Route("api/provider", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(ProviderApi))]
[PartialPermission(nameof(Provider))]
[ApiDefinition("Providers", Group = MetadataGroup)]
public class ProviderApi : CrudThumbsApi<Provider>
{

View File

@ -39,7 +39,7 @@ namespace Kyoo.Core.Api
[Route("api/people", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(StaffApi))]
[PartialPermission(nameof(People))]
[ApiDefinition("Staff", Group = MetadataGroup)]
public class StaffApi : CrudThumbsApi<People>
{

View File

@ -37,7 +37,7 @@ namespace Kyoo.Core.Api
[Route("api/studios")]
[Route("api/studio", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(ShowApi))]
[PartialPermission(nameof(Show))]
[ApiDefinition("Studios", Group = MetadataGroup)]
public class StudioApi : CrudApi<Studio>
{

View File

@ -37,7 +37,7 @@ namespace Kyoo.Core.Api
[Route("api/collections")]
[Route("api/collection", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(CollectionApi))]
[PartialPermission(nameof(Collection))]
[ApiDefinition("Collections", Group = ResourcesGroup)]
public class CollectionApi : CrudThumbsApi<Collection>
{

View File

@ -38,7 +38,7 @@ namespace Kyoo.Core.Api
[Route("api/episode", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(EpisodeApi))]
[PartialPermission(nameof(Episode))]
[ApiDefinition("Episodes", Group = ResourcesGroup)]
public class EpisodeApi : CrudThumbsApi<Episode>
{

View File

@ -40,7 +40,7 @@ namespace Kyoo.Core.Api
[Route("api/library", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(LibraryApi), Group = Group.Admin)]
[PartialPermission(nameof(Library), Group = Group.Admin)]
[ApiDefinition("Library", Group = ResourcesGroup)]
public class LibraryApi : CrudApi<Library>
{

View File

@ -38,6 +38,7 @@ namespace Kyoo.Core.Api
[Route("api/item", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(LibraryItem))]
[ApiDefinition("Items", Group = ResourcesGroup)]
public class LibraryItemApi : BaseApi
{
@ -74,7 +75,7 @@ namespace Kyoo.Core.Api
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet]
[Permission(nameof(LibraryItemApi), Kind.Read)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]

View File

@ -0,0 +1,194 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// An endpoint to search for every resources of kyoo. Searching for only a specific type of resource
/// is available on the said endpoint.
/// </summary>
[Route("api/search/{query}")]
[ApiController]
[ResourceView]
[ApiDefinition("Search", Group = ResourcesGroup)]
public class SearchApi : ControllerBase
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="SearchApi"/>.
/// </summary>
/// <param name="libraryManager">The library manager used to interact with the data store.</param>
public SearchApi(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Global search
/// </summary>
/// <remarks>
/// Search for collections, shows, episodes, staff, genre and studios at the same time
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of every resources found for the specified query.</returns>
[HttpGet]
[Permission(nameof(Collection), Kind.Read)]
[Permission(nameof(Show), Kind.Read)]
[Permission(nameof(Episode), Kind.Read)]
[Permission(nameof(People), Kind.Read)]
[Permission(nameof(Genre), Kind.Read)]
[Permission(nameof(Studio), Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<SearchResult>> Search(string query)
{
return new SearchResult
{
Query = query,
Collections = await _libraryManager.Search<Collection>(query),
Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.Search<People>(query),
Genres = await _libraryManager.Search<Genre>(query),
Studios = await _libraryManager.Search<Studio>(query)
};
}
/// <summary>
/// Search collections
/// </summary>
/// <remarks>
/// Search for collections
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of collections found for the specified query.</returns>
[HttpGet("collections")]
[HttpGet("collection", Order = AlternativeRoute)]
[Permission(nameof(Collection), Kind.Read)]
[ApiDefinition("Collections")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Collection>> SearchCollections(string query)
{
return _libraryManager.Search<Collection>(query);
}
/// <summary>
/// Search shows
/// </summary>
/// <remarks>
/// Search for shows
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of shows found for the specified query.</returns>
[HttpGet("shows")]
[HttpGet("show", Order = AlternativeRoute)]
[Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Shows")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Show>> SearchShows(string query)
{
return _libraryManager.Search<Show>(query);
}
/// <summary>
/// Search episodes
/// </summary>
/// <remarks>
/// Search for episodes
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of episodes found for the specified query.</returns>
[HttpGet("episodes")]
[HttpGet("episode", Order = AlternativeRoute)]
[Permission(nameof(Episode), Kind.Read)]
[ApiDefinition("Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Episode>> SearchEpisodes(string query)
{
return _libraryManager.Search<Episode>(query);
}
/// <summary>
/// Search staff
/// </summary>
/// <remarks>
/// Search for staff
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of staff members found for the specified query.</returns>
[HttpGet("staff")]
[HttpGet("person", Order = AlternativeRoute)]
[HttpGet("people", Order = AlternativeRoute)]
[Permission(nameof(People), Kind.Read)]
[ApiDefinition("Staff")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<People>> SearchPeople(string query)
{
return _libraryManager.Search<People>(query);
}
/// <summary>
/// Search genres
/// </summary>
/// <remarks>
/// Search for genres
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of genres found for the specified query.</returns>
[HttpGet("genres")]
[HttpGet("genre", Order = AlternativeRoute)]
[Permission(nameof(Genre), Kind.Read)]
[ApiDefinition("Genres")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Genre>> SearchGenres(string query)
{
return _libraryManager.Search<Genre>(query);
}
/// <summary>
/// Search studios
/// </summary>
/// <remarks>
/// Search for studios
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of studios found for the specified query.</returns>
[HttpGet("studios")]
[HttpGet("studio", Order = AlternativeRoute)]
[Permission(nameof(Studio), Kind.Read)]
[ApiDefinition("Studios")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Studio>> SearchStudios(string query)
{
return _libraryManager.Search<Studio>(query);
}
}
}

View File

@ -37,7 +37,7 @@ namespace Kyoo.Core.Api
[Route("api/seasons")]
[Route("api/season", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(SeasonApi))]
[PartialPermission(nameof(Season))]
[ApiDefinition("Seasons", Group = ResourcesGroup)]
public class SeasonApi : CrudThumbsApi<Season>
{

View File

@ -44,7 +44,7 @@ namespace Kyoo.Core.Api
[Route("api/movie", Order = AlternativeRoute)]
[Route("api/movies", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(ShowApi))]
[PartialPermission(nameof(Show))]
[ApiDefinition("Shows", Group = ResourcesGroup)]
public class ShowApi : CrudThumbsApi<Show>
{

View File

@ -1,107 +0,0 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api
{
[Route("api/search/{query}")]
[ApiController]
public class SearchApi : ControllerBase
{
private readonly ILibraryManager _libraryManager;
public SearchApi(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
[HttpGet]
[Permission(nameof(Collection), Kind.Read)]
[Permission(nameof(Show), Kind.Read)]
[Permission(nameof(Episode), Kind.Read)]
[Permission(nameof(People), Kind.Read)]
[Permission(nameof(Genre), Kind.Read)]
[Permission(nameof(Studio), Kind.Read)]
public async Task<ActionResult<SearchResult>> Search(string query)
{
return new SearchResult
{
Query = query,
Collections = await _libraryManager.Search<Collection>(query),
Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.Search<People>(query),
Genres = await _libraryManager.Search<Genre>(query),
Studios = await _libraryManager.Search<Studio>(query)
};
}
[HttpGet("collection")]
[HttpGet("collections")]
[Permission(nameof(Collection), Kind.Read)]
public Task<ICollection<Collection>> SearchCollections(string query)
{
return _libraryManager.Search<Collection>(query);
}
[HttpGet("show")]
[HttpGet("shows")]
[Permission(nameof(Show), Kind.Read)]
public Task<ICollection<Show>> SearchShows(string query)
{
return _libraryManager.Search<Show>(query);
}
[HttpGet("episode")]
[HttpGet("episodes")]
[Permission(nameof(Episode), Kind.Read)]
public Task<ICollection<Episode>> SearchEpisodes(string query)
{
return _libraryManager.Search<Episode>(query);
}
[HttpGet("people")]
[Permission(nameof(People), Kind.Read)]
public Task<ICollection<People>> SearchPeople(string query)
{
return _libraryManager.Search<People>(query);
}
[HttpGet("genre")]
[HttpGet("genres")]
[Permission(nameof(Genre), Kind.Read)]
public Task<ICollection<Genre>> SearchGenres(string query)
{
return _libraryManager.Search<Genre>(query);
}
[HttpGet("studio")]
[HttpGet("studios")]
[Permission(nameof(Studio), Kind.Read)]
public Task<ICollection<Studio>> SearchStudios(string query)
{
return _libraryManager.Search<Studio>(query);
}
}
}

View File

@ -18,6 +18,7 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Swagger.Models;
using NSwag;
using NSwag.Generation.AspNetCore;
@ -49,13 +50,16 @@ namespace Kyoo.Swagger
{
if (!postProcess.ExtensionData.TryGetValue("x-tagGroups", out object list))
return;
List<dynamic> tagGroups = (List<dynamic>)list;
List<TagGroups> tagGroups = (List<TagGroups>)list;
postProcess.ExtensionData["x-tagGroups"] = tagGroups
.OrderBy(x => x.name)
.Select(x => new
.OrderBy(x => x.Name)
.Select(x =>
{
name = x.name.Substring(x.name.IndexOf(':') + 1),
x.tags
x.Name = x.Name[(x.Name.IndexOf(':') + 1)..];
x.Tags = x.Tags
.OrderBy(y => y)
.ToList();
return x;
})
.ToList();
};

View File

@ -20,6 +20,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Swagger.Models;
using Namotion.Reflection;
using NSwag;
using NSwag.Generation.AspNetCore;
@ -44,6 +45,10 @@ namespace Kyoo.Swagger
ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute<ApiDefinitionAttribute>();
string name = def?.Name ?? context.ControllerType.Name;
ApiDefinitionAttribute methodOverride = context.MethodInfo.GetCustomAttribute<ApiDefinitionAttribute>();
if (methodOverride != null)
name = methodOverride.Name;
context.OperationDescription.Operation.Tags.Add(name);
if (context.Document.Tags.All(x => x.Name != name))
{
@ -58,20 +63,20 @@ namespace Kyoo.Swagger
return true;
context.Document.ExtensionData ??= new Dictionary<string, object>();
context.Document.ExtensionData.TryAdd("x-tagGroups", new List<dynamic>());
List<dynamic> obj = (List<dynamic>)context.Document.ExtensionData["x-tagGroups"];
dynamic existing = obj.FirstOrDefault(x => x.name == def.Group);
context.Document.ExtensionData.TryAdd("x-tagGroups", new List<TagGroups>());
List<TagGroups> obj = (List<TagGroups>)context.Document.ExtensionData["x-tagGroups"];
TagGroups existing = obj.FirstOrDefault(x => x.Name == def.Group);
if (existing != null)
{
if (!existing.tags.Contains(def.Name))
existing.tags.Add(def.Name);
if (!existing.Tags.Contains(def.Name))
existing.Tags.Add(def.Name);
}
else
{
obj.Add(new
obj.Add(new TagGroups
{
name = def.Group,
tags = new List<string> { def.Name }
Name = def.Group,
Tags = new List<string> { def.Name }
});
}
@ -88,19 +93,19 @@ namespace Kyoo.Swagger
/// </param>
public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess)
{
List<dynamic> tagGroups = (List<dynamic>)postProcess.ExtensionData["x-tagGroups"];
List<TagGroups> tagGroups = (List<TagGroups>)postProcess.ExtensionData["x-tagGroups"];
List<string> tagsWithoutGroup = postProcess.Tags
.Select(x => x.Name)
.Where(x => tagGroups
.SelectMany<dynamic, string>(y => y.tags)
.SelectMany(y => y.Tags)
.All(y => y != x))
.ToList();
if (tagsWithoutGroup.Any())
{
tagGroups.Add(new
tagGroups.Add(new TagGroups
{
name = "Others",
tags = tagsWithoutGroup
Name = "Others",
Tags = tagsWithoutGroup
});
}
}

View File

@ -0,0 +1,42 @@
// 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.Collections.Generic;
using Newtonsoft.Json;
using NSwag;
namespace Kyoo.Swagger.Models
{
/// <summary>
/// A class representing a group of tags in the <see cref="OpenApiDocument"/>
/// </summary>
public class TagGroups
{
/// <summary>
/// The name of the tag group.
/// </summary>
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
/// <summary>
/// The list of tags in this group.
/// </summary>
[JsonProperty(PropertyName = "tags")]
public List<string> Tags { get; set; }
}
}