mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Swagger: sorting tags groups & documenting the genre's API
This commit is contained in:
parent
049f545d51
commit
0fdc583d58
@ -33,7 +33,9 @@ namespace Kyoo.Abstractions.Models.Attributes
|
||||
[NotNull] public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the group in witch this API is.
|
||||
/// The name of the group in witch this API is. You can also specify a custom sort order using the following
|
||||
/// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
|
||||
/// th alphabetical ordering.
|
||||
/// </summary>
|
||||
public string Group { get; set; }
|
||||
|
||||
|
@ -34,11 +34,17 @@ namespace Kyoo.Abstractions.Models.Utils
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
||||
/// </summary>
|
||||
public const string ResourcesGroup = "Resources";
|
||||
public const string ResourcesGroup = "0:Resources";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
||||
/// It should be used for sub resources of kyoo that help define the main resources.
|
||||
/// </summary>
|
||||
public const string MetadataGroup = "1:Metadata";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
||||
/// </summary>
|
||||
public const string WatchGroup = "Watch";
|
||||
public const string WatchGroup = "2:Watch";
|
||||
}
|
||||
}
|
||||
|
@ -1,96 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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/genre")]
|
||||
[Route("api/genres")]
|
||||
[ApiController]
|
||||
[PartialPermission(nameof(GenreApi))]
|
||||
public class GenreApi : CrudApi<Genre>
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public GenreApi(ILibraryManager libraryManager)
|
||||
: base(libraryManager.GenreRepository)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/show")]
|
||||
[HttpGet("{id:int}/shows")]
|
||||
[PartialPermission(Kind.Read)]
|
||||
public async Task<ActionResult<Page<Show>>> GetShows(int id,
|
||||
[FromQuery] string sortBy,
|
||||
[FromQuery] int afterID,
|
||||
[FromQuery] Dictionary<string, string> where,
|
||||
[FromQuery] int limit = 20)
|
||||
{
|
||||
try
|
||||
{
|
||||
ICollection<Show> resources = await _libraryManager.GetAll(
|
||||
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.ID == id)),
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Genre>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/show")]
|
||||
[HttpGet("{slug}/shows")]
|
||||
[PartialPermission(Kind.Read)]
|
||||
public async Task<ActionResult<Page<Show>>> GetShows(string slug,
|
||||
[FromQuery] string sortBy,
|
||||
[FromQuery] int afterID,
|
||||
[FromQuery] Dictionary<string, string> where,
|
||||
[FromQuery] int limit = 20)
|
||||
{
|
||||
try
|
||||
{
|
||||
ICollection<Show> resources = await _libraryManager.GetAll(
|
||||
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.Slug == slug)),
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Genre>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
src/Kyoo.Core/Views/Metadata/GenreApi.cs
Normal file
105
src/Kyoo.Core/Views/Metadata/GenreApi.cs
Normal file
@ -0,0 +1,105 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about one or multiple <see cref="Genre"/>.
|
||||
/// </summary>
|
||||
[Route("api/genres")]
|
||||
[Route("api/genre", Order = AlternativeRoute)]
|
||||
[ApiController]
|
||||
[PartialPermission(nameof(GenreApi))]
|
||||
[ApiDefinition("Genres", Group = MetadataGroup)]
|
||||
public class GenreApi : CrudApi<Genre>
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager used to modify or retrieve information about the data store.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="GenreApi"/>.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">
|
||||
/// The library manager used to modify or retrieve information about the data store.
|
||||
/// </param>
|
||||
public GenreApi(ILibraryManager libraryManager)
|
||||
: base(libraryManager.GenreRepository)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get shows with genre
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lists the shows that have the selected genre.
|
||||
/// </remarks>
|
||||
/// <param name="identifier">The ID or slug of the <see cref="Genre"/>.</param>
|
||||
/// <param name="sortBy">A key to sort shows by.</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 genre with the given ID could be found.</response>
|
||||
[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(Identifier identifier,
|
||||
[FromQuery] string sortBy,
|
||||
[FromQuery] Dictionary<string, string> where,
|
||||
[FromQuery] int limit = 20,
|
||||
[FromQuery] int? afterID = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ICollection<Show> resources = await _libraryManager.GetAll(
|
||||
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Genre>(x => x.Genres)),
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID)
|
||||
);
|
||||
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Genre>()) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new RequestError(ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
src/Kyoo.Swagger/ApiSorter.cs
Normal file
64
src/Kyoo.Swagger/ApiSorter.cs
Normal file
@ -0,0 +1,64 @@
|
||||
// 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.Linq;
|
||||
using NSwag;
|
||||
using NSwag.Generation.AspNetCore;
|
||||
|
||||
namespace Kyoo.Swagger
|
||||
{
|
||||
/// <summary>
|
||||
/// A class to sort apis.
|
||||
/// </summary>
|
||||
public static class ApiSorter
|
||||
{
|
||||
/// <summary>
|
||||
/// Sort apis by alphabetical orders.
|
||||
/// </summary>
|
||||
/// <param name="options">The swagger settings to update.</param>
|
||||
public static void SortApis(this AspNetCoreOpenApiDocumentGeneratorSettings options)
|
||||
{
|
||||
options.PostProcess += postProcess =>
|
||||
{
|
||||
// We can't reorder items by assigning the sorted value to the Paths variable since it has no setter.
|
||||
List<KeyValuePair<string, OpenApiPathItem>> sorted = postProcess.Paths
|
||||
.OrderBy(x => x.Key)
|
||||
.ToList();
|
||||
postProcess.Paths.Clear();
|
||||
foreach ((string key, OpenApiPathItem value) in sorted)
|
||||
postProcess.Paths.Add(key, value);
|
||||
};
|
||||
|
||||
options.PostProcess += postProcess =>
|
||||
{
|
||||
if (!postProcess.ExtensionData.TryGetValue("x-tagGroups", out object list))
|
||||
return;
|
||||
List<dynamic> tagGroups = (List<dynamic>)list;
|
||||
postProcess.ExtensionData["x-tagGroups"] = tagGroups
|
||||
.OrderBy(x => x.name)
|
||||
.Select(x => new
|
||||
{
|
||||
name = x.name.Substring(x.name.IndexOf(':') + 1),
|
||||
x.tags
|
||||
})
|
||||
.ToList();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
119
src/Kyoo.Swagger/ApiTagsFilter.cs
Normal file
119
src/Kyoo.Swagger/ApiTagsFilter.cs
Normal file
@ -0,0 +1,119 @@
|
||||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Namotion.Reflection;
|
||||
using NSwag;
|
||||
using NSwag.Generation.AspNetCore;
|
||||
using NSwag.Generation.Processors.Contexts;
|
||||
|
||||
namespace Kyoo.Swagger
|
||||
{
|
||||
/// <summary>
|
||||
/// A class to handle Api Groups (OpenApi tags and x-tagGroups).
|
||||
/// Tags should be specified via <see cref="ApiDefinitionAttribute"/> and this filter will map this to the
|
||||
/// <see cref="OpenApiDocument"/>.
|
||||
/// </summary>
|
||||
public static class ApiTagsFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The main operation filter that will map every <see cref="ApiDefinitionAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The processor context, this is given by the AddOperationFilter method.</param>
|
||||
/// <returns>This always return <c>true</c> since it should not remove operations.</returns>
|
||||
public static bool OperationFilter(OperationProcessorContext context)
|
||||
{
|
||||
ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute<ApiDefinitionAttribute>();
|
||||
string name = def?.Name ?? context.ControllerType.Name;
|
||||
|
||||
context.OperationDescription.Operation.Tags.Add(name);
|
||||
if (context.Document.Tags.All(x => x.Name != name))
|
||||
{
|
||||
context.Document.Tags.Add(new OpenApiTag
|
||||
{
|
||||
Name = name,
|
||||
Description = context.ControllerType.GetXmlDocsSummary()
|
||||
});
|
||||
}
|
||||
|
||||
if (def == null)
|
||||
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);
|
||||
if (existing != null)
|
||||
{
|
||||
if (!existing.tags.Contains(def.Name))
|
||||
existing.tags.Add(def.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.Add(new
|
||||
{
|
||||
name = def.Group,
|
||||
tags = new List<string> { def.Name }
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This add every tags that are not in a x-tagGroups to a new tagGroups named "Other".
|
||||
/// Since tags that are not in a tagGroups are not shown, this is necessary if you want them displayed.
|
||||
/// </summary>
|
||||
/// <param name="postProcess">
|
||||
/// The document to do this for. This should be done in the PostProcess part of the document or after
|
||||
/// the main operation filter (see <see cref="OperationFilter"/>) has finished.
|
||||
/// </param>
|
||||
public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess)
|
||||
{
|
||||
List<dynamic> tagGroups = (List<dynamic>)postProcess.ExtensionData["x-tagGroups"];
|
||||
List<string> tagsWithoutGroup = postProcess.Tags
|
||||
.Select(x => x.Name)
|
||||
.Where(x => tagGroups
|
||||
.SelectMany<dynamic, string>(y => y.tags)
|
||||
.All(y => y != x))
|
||||
.ToList();
|
||||
if (tagsWithoutGroup.Any())
|
||||
{
|
||||
tagGroups.Add(new
|
||||
{
|
||||
name = "Others",
|
||||
tags = tagsWithoutGroup
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use <see cref="ApiDefinitionAttribute"/> to create tags and groups of tags on the resulting swagger
|
||||
/// document.
|
||||
/// </summary>
|
||||
/// <param name="options">The settings of the swagger document.</param>
|
||||
public static void UseApiTags(this AspNetCoreOpenApiDocumentGeneratorSettings options)
|
||||
{
|
||||
options.AddOperationFilter(OperationFilter);
|
||||
options.PostProcess += x => x.AddLeftoversToOthersGroup();
|
||||
}
|
||||
}
|
||||
}
|
@ -18,15 +18,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Namotion.Reflection;
|
||||
using NJsonSchema;
|
||||
using NJsonSchema.Generation.TypeMappers;
|
||||
using NSwag;
|
||||
@ -77,75 +73,15 @@ namespace Kyoo.Swagger
|
||||
Name = "GPL-3.0-or-later",
|
||||
Url = "https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE"
|
||||
};
|
||||
|
||||
// We can't reorder items by assigning the sorted value to the Paths variable since it has no setter.
|
||||
List<KeyValuePair<string, OpenApiPathItem>> sorted = postProcess.Paths
|
||||
.OrderBy(x => x.Key)
|
||||
.ToList();
|
||||
postProcess.Paths.Clear();
|
||||
foreach ((string key, OpenApiPathItem value) in sorted)
|
||||
postProcess.Paths.Add(key, value);
|
||||
|
||||
List<dynamic> tagGroups = (List<dynamic>)postProcess.ExtensionData["x-tagGroups"];
|
||||
List<string> tagsWithoutGroup = postProcess.Tags
|
||||
.Select(x => x.Name)
|
||||
.Where(x => tagGroups
|
||||
.SelectMany<dynamic, string>(y => y.tags)
|
||||
.All(y => y != x))
|
||||
.ToList();
|
||||
if (tagsWithoutGroup.Any())
|
||||
{
|
||||
tagGroups.Add(new
|
||||
{
|
||||
name = "Others",
|
||||
tags = tagsWithoutGroup
|
||||
});
|
||||
}
|
||||
};
|
||||
options.UseApiTags();
|
||||
options.SortApis();
|
||||
options.AddOperationFilter(x =>
|
||||
{
|
||||
if (x is AspNetCoreOperationProcessorContext ctx)
|
||||
return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute;
|
||||
return true;
|
||||
});
|
||||
options.AddOperationFilter(context =>
|
||||
{
|
||||
ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute<ApiDefinitionAttribute>();
|
||||
string name = def?.Name ?? context.ControllerType.Name;
|
||||
|
||||
context.OperationDescription.Operation.Tags.Add(name);
|
||||
if (context.Document.Tags.All(x => x.Name != name))
|
||||
{
|
||||
context.Document.Tags.Add(new OpenApiTag
|
||||
{
|
||||
Name = name,
|
||||
Description = context.ControllerType.GetXmlDocsSummary()
|
||||
});
|
||||
}
|
||||
|
||||
if (def == null)
|
||||
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);
|
||||
if (existing != null)
|
||||
{
|
||||
if (!existing.tags.Contains(def.Name))
|
||||
existing.tags.Add(def.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.Add(new
|
||||
{
|
||||
name = def.Group,
|
||||
tags = new List<string> { def.Name }
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
options.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x =>
|
||||
{
|
||||
x.IsNullableRaw = false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user