diff --git a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs
index 84d8dc3d..b8923eef 100644
--- a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs
+++ b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs
@@ -33,7 +33,9 @@ namespace Kyoo.Abstractions.Models.Attributes
[NotNull] public string Name { get; }
///
- /// 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: order:name
. Everything before the first : will be removed but kept for
+ /// th alphabetical ordering.
///
public string Group { get; set; }
diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs
index 521dc2d2..985f0254 100644
--- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs
+++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs
@@ -34,11 +34,17 @@ namespace Kyoo.Abstractions.Models.Utils
///
/// A group name for . It should be used for main resources of kyoo.
///
- public const string ResourcesGroup = "Resources";
+ public const string ResourcesGroup = "0:Resources";
+
+ ///
+ /// A group name for .
+ /// It should be used for sub resources of kyoo that help define the main resources.
+ ///
+ public const string MetadataGroup = "1:Metadata";
///
/// A group name for . It should be used for endpoints useful for playback.
///
- public const string WatchGroup = "Watch";
+ public const string WatchGroup = "2:Watch";
}
}
diff --git a/src/Kyoo.Core/Views/GenreApi.cs b/src/Kyoo.Core/Views/GenreApi.cs
deleted file mode 100644
index 28bc3067..00000000
--- a/src/Kyoo.Core/Views/GenreApi.cs
+++ /dev/null
@@ -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 .
-
-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
- {
- 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>> GetShows(int id,
- [FromQuery] string sortBy,
- [FromQuery] int afterID,
- [FromQuery] Dictionary where,
- [FromQuery] int limit = 20)
- {
- try
- {
- ICollection resources = await _libraryManager.GetAll(
- ApiHelper.ParseWhere(where, x => x.Genres.Any(y => y.ID == id)),
- 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 { Error = ex.Message });
- }
- }
-
- [HttpGet("{slug}/show")]
- [HttpGet("{slug}/shows")]
- [PartialPermission(Kind.Read)]
- public async Task>> GetShows(string slug,
- [FromQuery] string sortBy,
- [FromQuery] int afterID,
- [FromQuery] Dictionary where,
- [FromQuery] int limit = 20)
- {
- try
- {
- ICollection resources = await _libraryManager.GetAll(
- ApiHelper.ParseWhere(where, x => x.Genres.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 });
- }
- }
- }
-}
diff --git a/src/Kyoo.Core/Views/Metadata/GenreApi.cs b/src/Kyoo.Core/Views/Metadata/GenreApi.cs
new file mode 100644
index 00000000..57336138
--- /dev/null
+++ b/src/Kyoo.Core/Views/Metadata/GenreApi.cs
@@ -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 .
+
+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
+{
+ ///
+ /// Information about one or multiple .
+ ///
+ [Route("api/genres")]
+ [Route("api/genre", Order = AlternativeRoute)]
+ [ApiController]
+ [PartialPermission(nameof(GenreApi))]
+ [ApiDefinition("Genres", Group = MetadataGroup)]
+ public class GenreApi : CrudApi
+ {
+ ///
+ /// The library manager used to modify or retrieve information about the data store.
+ ///
+ private readonly ILibraryManager _libraryManager;
+
+ ///
+ /// Create a new .
+ ///
+ ///
+ /// The library manager used to modify or retrieve information about the data store.
+ ///
+ public GenreApi(ILibraryManager libraryManager)
+ : base(libraryManager.GenreRepository)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ ///
+ /// Get shows with genre
+ ///
+ ///
+ /// Lists the shows that have the selected genre.
+ ///
+ /// The ID or slug of the .
+ /// A key to sort shows by.
+ /// 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 genre with the given ID could be found.
+ [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(Identifier identifier,
+ [FromQuery] string sortBy,
+ [FromQuery] Dictionary where,
+ [FromQuery] int limit = 20,
+ [FromQuery] int? afterID = null)
+ {
+ try
+ {
+ ICollection resources = await _libraryManager.GetAll(
+ ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Genres)),
+ new Sort(sortBy),
+ new Pagination(limit, afterID)
+ );
+
+ if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null)
+ return NotFound();
+ return Page(resources, limit);
+ }
+ catch (ArgumentException ex)
+ {
+ return BadRequest(new RequestError(ex.Message));
+ }
+ }
+ }
+}
diff --git a/src/Kyoo.Swagger/ApiSorter.cs b/src/Kyoo.Swagger/ApiSorter.cs
new file mode 100644
index 00000000..f328d354
--- /dev/null
+++ b/src/Kyoo.Swagger/ApiSorter.cs
@@ -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 .
+
+using System.Collections.Generic;
+using System.Linq;
+using NSwag;
+using NSwag.Generation.AspNetCore;
+
+namespace Kyoo.Swagger
+{
+ ///
+ /// A class to sort apis.
+ ///
+ public static class ApiSorter
+ {
+ ///
+ /// Sort apis by alphabetical orders.
+ ///
+ /// The swagger settings to update.
+ 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> 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 tagGroups = (List)list;
+ postProcess.ExtensionData["x-tagGroups"] = tagGroups
+ .OrderBy(x => x.name)
+ .Select(x => new
+ {
+ name = x.name.Substring(x.name.IndexOf(':') + 1),
+ x.tags
+ })
+ .ToList();
+ };
+ }
+ }
+}
diff --git a/src/Kyoo.Swagger/ApiTagsFilter.cs b/src/Kyoo.Swagger/ApiTagsFilter.cs
new file mode 100644
index 00000000..9ee21a01
--- /dev/null
+++ b/src/Kyoo.Swagger/ApiTagsFilter.cs
@@ -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 .
+
+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
+{
+ ///
+ /// A class to handle Api Groups (OpenApi tags and x-tagGroups).
+ /// Tags should be specified via and this filter will map this to the
+ /// .
+ ///
+ public static class ApiTagsFilter
+ {
+ ///
+ /// The main operation filter that will map every .
+ ///
+ /// The processor context, this is given by the AddOperationFilter method.
+ /// This always return true since it should not remove operations.
+ public static bool OperationFilter(OperationProcessorContext context)
+ {
+ ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute();
+ 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();
+ context.Document.ExtensionData.TryAdd("x-tagGroups", new List());
+ List obj = (List)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 { def.Name }
+ });
+ }
+
+ return true;
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// The document to do this for. This should be done in the PostProcess part of the document or after
+ /// the main operation filter (see ) has finished.
+ ///
+ public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess)
+ {
+ List tagGroups = (List)postProcess.ExtensionData["x-tagGroups"];
+ List tagsWithoutGroup = postProcess.Tags
+ .Select(x => x.Name)
+ .Where(x => tagGroups
+ .SelectMany(y => y.tags)
+ .All(y => y != x))
+ .ToList();
+ if (tagsWithoutGroup.Any())
+ {
+ tagGroups.Add(new
+ {
+ name = "Others",
+ tags = tagsWithoutGroup
+ });
+ }
+ }
+
+ ///
+ /// Use to create tags and groups of tags on the resulting swagger
+ /// document.
+ ///
+ /// The settings of the swagger document.
+ public static void UseApiTags(this AspNetCoreOpenApiDocumentGeneratorSettings options)
+ {
+ options.AddOperationFilter(OperationFilter);
+ options.PostProcess += x => x.AddLeftoversToOthersGroup();
+ }
+ }
+}
diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs
index ad98de7d..4cbff4a1 100644
--- a/src/Kyoo.Swagger/SwaggerModule.cs
+++ b/src/Kyoo.Swagger/SwaggerModule.cs
@@ -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> sorted = postProcess.Paths
- .OrderBy(x => x.Key)
- .ToList();
- postProcess.Paths.Clear();
- foreach ((string key, OpenApiPathItem value) in sorted)
- postProcess.Paths.Add(key, value);
-
- List tagGroups = (List)postProcess.ExtensionData["x-tagGroups"];
- List tagsWithoutGroup = postProcess.Tags
- .Select(x => x.Name)
- .Where(x => tagGroups
- .SelectMany(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();
- 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();
- context.Document.ExtensionData.TryAdd("x-tagGroups", new List());
- List obj = (List)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 { def.Name }
- });
- }
-
- return true;
- });
options.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x =>
{
x.IsNullableRaw = false;