From 09ff2e795d2eefc1c1ae27e2a9c63191d70ce947 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 14 Sep 2021 19:20:56 +0200 Subject: [PATCH 01/36] Swagger: Creating a swagger module --- Kyoo.sln | 6 +++++ src/Directory.Build.props | 14 ++++++++++ .../Kyoo.Abstractions.csproj | 1 - src/Kyoo.Core/Kyoo.Core.csproj | 9 +++---- src/Kyoo.Database/Kyoo.Database.csproj | 6 +---- .../Kyoo.Host.Console.csproj | 14 +++++----- .../Kyoo.Host.WindowsTrait.csproj | 4 ++- src/Kyoo.Postgresql/Kyoo.Postgresql.csproj | 6 ++--- src/Kyoo.SqLite/Kyoo.SqLite.csproj | 12 +++------ src/Kyoo.Swagger/Kyoo.Swagger.csproj | 13 +++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 27 +++++++++++++++++++ src/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj | 5 +--- src/Kyoo.TheTvdb/Kyoo.TheTvdb.csproj | 8 +----- src/Kyoo.WebApp/Kyoo.WebApp.csproj | 13 +++++---- 14 files changed, 86 insertions(+), 52 deletions(-) create mode 100644 src/Kyoo.Swagger/Kyoo.Swagger.csproj create mode 100644 src/Kyoo.Swagger/SwaggerModule.cs diff --git a/Kyoo.sln b/Kyoo.sln index faab841f..61d5219f 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.WindowsTrait", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.Console", "src\Kyoo.Host.Console\Kyoo.Host.Console.csproj", "{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Swagger", "src\Kyoo.Swagger\Kyoo.Swagger.csproj", "{7D1A7596-73F6-4D35-842E-A5AD9C620596}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,5 +83,9 @@ Global {D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}.Release|Any CPU.Build.0 = Release|Any CPU + {7D1A7596-73F6-4D35-842E-A5AD9C620596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D1A7596-73F6-4D35-842E-A5AD9C620596}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b9d5b35c..47ab383d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,4 +1,14 @@ + + Kyoo + Kyoo + Copyright (c) Kyoo + GPL-3.0-or-later + true + https://github.com/AnonymusRaccoon/Kyoo + git + + true true @@ -11,6 +21,10 @@ true + + + + diff --git a/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj b/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj index 60af7d84..faf51b1b 100644 --- a/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj +++ b/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj @@ -24,7 +24,6 @@ - diff --git a/src/Kyoo.Core/Kyoo.Core.csproj b/src/Kyoo.Core/Kyoo.Core.csproj index 75db8e82..7b7b3cca 100644 --- a/src/Kyoo.Core/Kyoo.Core.csproj +++ b/src/Kyoo.Core/Kyoo.Core.csproj @@ -1,13 +1,10 @@ - net5.0 - ../Kyoo.Transcoder/ - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo default + Kyoo.Core + Kyoo.Core + ../Kyoo.Transcoder/ diff --git a/src/Kyoo.Database/Kyoo.Database.csproj b/src/Kyoo.Database/Kyoo.Database.csproj index a8fd3c51..5c59b7ef 100644 --- a/src/Kyoo.Database/Kyoo.Database.csproj +++ b/src/Kyoo.Database/Kyoo.Database.csproj @@ -1,12 +1,9 @@ - net5.0 + default Kyoo.Database Kyoo.Database - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo - default @@ -19,5 +16,4 @@ - diff --git a/src/Kyoo.Host.Console/Kyoo.Host.Console.csproj b/src/Kyoo.Host.Console/Kyoo.Host.Console.csproj index 20dc8559..0cd712be 100644 --- a/src/Kyoo.Host.Console/Kyoo.Host.Console.csproj +++ b/src/Kyoo.Host.Console/Kyoo.Host.Console.csproj @@ -2,23 +2,21 @@ Exe net5.0 - Kyoo.Host.Console.Program - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo default + Kyoo.Host.Console + Kyoo.Host.Console + Kyoo.Host.Console.Program - win-x64 - + diff --git a/src/Kyoo.Host.WindowsTrait/Kyoo.Host.WindowsTrait.csproj b/src/Kyoo.Host.WindowsTrait/Kyoo.Host.WindowsTrait.csproj index 084b6541..62cc27a3 100644 --- a/src/Kyoo.Host.WindowsTrait/Kyoo.Host.WindowsTrait.csproj +++ b/src/Kyoo.Host.WindowsTrait/Kyoo.Host.WindowsTrait.csproj @@ -4,8 +4,10 @@ WinExe net5.0-windows + default true - Kyoo.WindowsHost + Kyoo.Host.WindowsTrait + Kyoo.Host.WindowsTrait diff --git a/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj index e356ba8a..8bb4ecf5 100644 --- a/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -1,11 +1,9 @@ net5.0 - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo default + Kyoo.Postgresql + Kyoo.Postgresql diff --git a/src/Kyoo.SqLite/Kyoo.SqLite.csproj b/src/Kyoo.SqLite/Kyoo.SqLite.csproj index f689dbe8..24ba70c2 100644 --- a/src/Kyoo.SqLite/Kyoo.SqLite.csproj +++ b/src/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -1,24 +1,18 @@ net5.0 - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo default + Kyoo.SqLite Kyoo.SqLite - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + diff --git a/src/Kyoo.Swagger/Kyoo.Swagger.csproj b/src/Kyoo.Swagger/Kyoo.Swagger.csproj new file mode 100644 index 00000000..13104cde --- /dev/null +++ b/src/Kyoo.Swagger/Kyoo.Swagger.csproj @@ -0,0 +1,13 @@ + + + net5.0 + default + Kyoo.Swagger + Kyoo.Swagger + + + + + + + diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs new file mode 100644 index 00000000..250efbbd --- /dev/null +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -0,0 +1,27 @@ +// 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 . + +namespace Kyoo.Swagger +{ + /// + /// A module to enable a swagger interface and an OpenAPI endpoint to document Kyoo. + /// + public class SwaggerModule + { + } +} diff --git a/src/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj b/src/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj index 7b841c4a..93a5b921 100644 --- a/src/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj +++ b/src/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj @@ -1,11 +1,8 @@ net5.0 - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo default + Kyoo.TheMovieDb Kyoo.TheMovieDb diff --git a/src/Kyoo.TheTvdb/Kyoo.TheTvdb.csproj b/src/Kyoo.TheTvdb/Kyoo.TheTvdb.csproj index f2c052ff..354b9a35 100644 --- a/src/Kyoo.TheTvdb/Kyoo.TheTvdb.csproj +++ b/src/Kyoo.TheTvdb/Kyoo.TheTvdb.csproj @@ -1,11 +1,8 @@ net5.0 - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo default + Kyoo.TheTvdb Kyoo.TheTvdb @@ -14,9 +11,6 @@ - - - diff --git a/src/Kyoo.WebApp/Kyoo.WebApp.csproj b/src/Kyoo.WebApp/Kyoo.WebApp.csproj index 910d2167..eeb49df7 100644 --- a/src/Kyoo.WebApp/Kyoo.WebApp.csproj +++ b/src/Kyoo.WebApp/Kyoo.WebApp.csproj @@ -1,8 +1,12 @@ - + net5.0 + default + Kyoo.WebApp + Kyoo.WebApp + true Latest false @@ -12,11 +16,6 @@ false - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo - default @@ -41,7 +40,7 @@ - + From ba37a7cb9b1e55adab033c01b880bf2d88368aa5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 15 Sep 2021 21:54:50 +0200 Subject: [PATCH 02/36] Swagger: Creating the swagger page --- src/Kyoo.Core/Application.cs | 2 +- src/Kyoo.Core/Controllers/Transcoder.cs | 2 + src/Kyoo.Core/Kyoo.Core.csproj | 1 + src/Kyoo.Core/PluginsStartup.cs | 4 +- src/Kyoo.Core/Views/Helper/CrudApi.cs | 1 + src/Kyoo.Core/settings.json | 6 +-- src/Kyoo.Swagger/SwaggerModule.cs | 58 ++++++++++++++++++++++++- 7 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/Kyoo.Core/Application.cs b/src/Kyoo.Core/Application.cs index 7b0f4961..f1ee2fda 100644 --- a/src/Kyoo.Core/Application.cs +++ b/src/Kyoo.Core/Application.cs @@ -283,7 +283,7 @@ namespace Kyoo.Core builder.ReadFrom.Services(services); const string template = - "[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 15} " + "[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 25} " + "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}"; if (SystemdHelpers.IsSystemdService()) diff --git a/src/Kyoo.Core/Controllers/Transcoder.cs b/src/Kyoo.Core/Controllers/Transcoder.cs index a29966ba..04510a3d 100644 --- a/src/Kyoo.Core/Controllers/Transcoder.cs +++ b/src/Kyoo.Core/Controllers/Transcoder.cs @@ -27,6 +27,8 @@ using Microsoft.Extensions.Options; // We use threads so tasks are not always awaited. #pragma warning disable 4014 +// Private items that are external can't start with an _ +#pragma warning disable IDE1006 namespace Kyoo.Core.Controllers { diff --git a/src/Kyoo.Core/Kyoo.Core.csproj b/src/Kyoo.Core/Kyoo.Core.csproj index 7b7b3cca..58a23762 100644 --- a/src/Kyoo.Core/Kyoo.Core.csproj +++ b/src/Kyoo.Core/Kyoo.Core.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Kyoo.Core/PluginsStartup.cs b/src/Kyoo.Core/PluginsStartup.cs index 1a554a8b..77618842 100644 --- a/src/Kyoo.Core/PluginsStartup.cs +++ b/src/Kyoo.Core/PluginsStartup.cs @@ -28,6 +28,7 @@ using Kyoo.Core.Models.Options; using Kyoo.Core.Tasks; using Kyoo.Postgresql; using Kyoo.SqLite; +using Kyoo.Swagger; using Kyoo.TheMovieDb; using Kyoo.TheTvdb; using Kyoo.Utils; @@ -75,7 +76,8 @@ namespace Kyoo.Core typeof(PostgresModule), typeof(SqLiteModule), typeof(PluginTvdb), - typeof(PluginTmdb) + typeof(PluginTmdb), + typeof(SwaggerModule) ); } diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index c621b993..8bbc47dc 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -208,6 +208,7 @@ namespace Kyoo.Core.Api return Ok(); } + [HttpDelete] [PartialPermission(Kind.Delete)] public virtual async Task Delete(Dictionary where) { diff --git a/src/Kyoo.Core/settings.json b/src/Kyoo.Core/settings.json index 64466270..6c472a6e 100644 --- a/src/Kyoo.Core/settings.json +++ b/src/Kyoo.Core/settings.json @@ -8,7 +8,7 @@ "metadataInShow": true, "metadataPath": "metadata/" }, - + "database": { "enabled": "sqlite", "configurations": { @@ -58,7 +58,7 @@ "^(?.+)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$" ] }, - + "authentication": { "certificate": { "file": "certificate.pfx", @@ -72,7 +72,7 @@ "profilePicturePath": "users/", "clients": [] }, - + "tvdb": { "apiKey": "" }, diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 250efbbd..a8aea7d4 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -16,12 +16,68 @@ // 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.IO; +using Kyoo.Abstractions.Controllers; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + namespace Kyoo.Swagger { /// /// A module to enable a swagger interface and an OpenAPI endpoint to document Kyoo. /// - public class SwaggerModule + public class SwaggerModule : IPlugin { + /// + public string Slug => "swagger"; + + /// + public string Name => "Swagger"; + + /// + public string Description => "A swagger interface and an OpenAPI endpoint to document Kyoo."; + + /// + public Dictionary Configuration => new(); + + /// + public void Configure(IServiceCollection services) + { + services.AddSwaggerGen(x => + { + x.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Kyoo API", + Description = "The Kyoo's public API", + Contact = new OpenApiContact + { + Name = "Kyoo's github", + Url = new Uri("https://github.com/AnonymusRaccoon/Kyoo/issues/new/choose") + }, + License = new OpenApiLicense + { + Name = "GPL-3.0-or-later", + Url = new Uri("https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE") + } + }); + + foreach (string documentation in Directory.GetFiles(AppContext.BaseDirectory, "*.xml")) + x.IncludeXmlComments(documentation); + }); + } + + /// + public IEnumerable ConfigureSteps => new IStartupAction[] + { + SA.New(app => app.UseSwagger(), SA.Before + 1), + SA.New(app => app.UseSwaggerUI(x => + { + x.SwaggerEndpoint("/swagger/v1/swagger.json", "Kyoo v1"); + }), SA.Before) + }; } } From 340950177e96bf9206cf5f2b05841da0d9900bbf Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 16 Sep 2021 21:37:37 +0200 Subject: [PATCH 03/36] Swagger: Adding every module, not just the core --- src/Kyoo.Authentication/Views/AccountApi.cs | 2 ++ src/Kyoo.Core/CoreModule.cs | 4 +++- src/Kyoo.Core/PluginsStartup.cs | 4 ++++ src/Kyoo.Swagger/SwaggerModule.cs | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Kyoo.Authentication/Views/AccountApi.cs b/src/Kyoo.Authentication/Views/AccountApi.cs index af569655..92af8127 100644 --- a/src/Kyoo.Authentication/Views/AccountApi.cs +++ b/src/Kyoo.Authentication/Views/AccountApi.cs @@ -177,6 +177,7 @@ namespace Kyoo.Authentication.Views } /// + [ApiExplorerSettings(IgnoreApi = true)] public async Task GetProfileDataAsync(ProfileDataRequestContext context) { User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); @@ -187,6 +188,7 @@ namespace Kyoo.Authentication.Views } /// + [ApiExplorerSettings(IgnoreApi = true)] public async Task IsActiveAsync(IsActiveContext context) { User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); diff --git a/src/Kyoo.Core/CoreModule.cs b/src/Kyoo.Core/CoreModule.cs index fa7f9819..fc83caf5 100644 --- a/src/Kyoo.Core/CoreModule.cs +++ b/src/Kyoo.Core/CoreModule.cs @@ -138,7 +138,9 @@ namespace Kyoo.Core { string publicUrl = _configuration.GetPublicUrl(); - services.AddMvc().AddControllersAsServices(); + services.AddMvcCore() + .AddControllersAsServices() + .AddApiExplorer(); services.AddControllers() .AddNewtonsoftJson(x => { diff --git a/src/Kyoo.Core/PluginsStartup.cs b/src/Kyoo.Core/PluginsStartup.cs index 77618842..047077d5 100644 --- a/src/Kyoo.Core/PluginsStartup.cs +++ b/src/Kyoo.Core/PluginsStartup.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Autofac; using Kyoo.Abstractions; using Kyoo.Abstractions.Controllers; @@ -108,6 +109,9 @@ namespace Kyoo.Core /// The service collection to fill. public void ConfigureServices(IServiceCollection services) { + foreach (Assembly assembly in _plugins.GetAllPlugins().Select(x => x.GetType().Assembly)) + services.AddMvcCore().AddApplicationPart(assembly); + foreach (IPlugin plugin in _plugins.GetAllPlugins()) plugin.Configure(services); diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index a8aea7d4..c53703d4 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.IO; using Kyoo.Abstractions.Controllers; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; From 4ce462f88fe82a9602031a6c25a270754070f337 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 19 Sep 2021 19:01:48 +0200 Subject: [PATCH 04/36] Swagger: Adding alternative routes --- src/Directory.Build.props | 8 ++++ .../Kyoo.Abstractions.csproj | 17 ++------ .../AltRoute/AltHttpGetAttribute.cs | 19 +++++++++ .../Attributes/AltRoute/AltRouteAttribute.cs | 39 +++++++++++++++++++ src/Kyoo.Core/Kyoo.Core.csproj | 1 + src/Kyoo.Core/Views/CollectionApi.cs | 11 +++--- src/Kyoo.Swagger/Kyoo.Swagger.csproj | 1 + src/Kyoo.Swagger/SwaggerModule.cs | 21 ++++++++-- 8 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs create mode 100644 src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltRouteAttribute.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 47ab383d..5efb3ebc 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,10 +3,18 @@ Kyoo Kyoo Copyright (c) Kyoo + true GPL-3.0-or-later true + https://github.com/AnonymusRaccoon/Kyoo git + true + https://github.com/AnonymusRaccoon/Kyoo + + 1.0.0 + true + snupkg diff --git a/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj b/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj index faf51b1b..59fdf472 100644 --- a/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj +++ b/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj @@ -1,20 +1,9 @@ - net5.0 - Kyoo.Abstractions - Zoe Roux - Base package to create plugins for Kyoo. - https://github.com/AnonymusRaccoon/Kyoo - true - https://github.com/AnonymusRaccoon/Kyoo - SDG - GPL-3.0-or-later - true - 1.0.0 - true - snupkg default + Kyoo.Abstractions + Base package to create plugins for Kyoo. Kyoo.Abstractions @@ -22,9 +11,9 @@ + - diff --git a/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs new file mode 100644 index 00000000..99f843b6 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Abstractions.Models.Attributes +{ + /// + /// A custom that indicate an alternatives, hidden route. + /// + public class AltHttpGetAttribute : HttpGetAttribute + { + /// + /// Create a new . + /// + /// The route template, see . + public AltHttpGetAttribute([NotNull] [RouteTemplateAttribute] string template) + : base(template) + { } + } +} diff --git a/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltRouteAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltRouteAttribute.cs new file mode 100644 index 00000000..71734979 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltRouteAttribute.cs @@ -0,0 +1,39 @@ +// 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 JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Abstractions.Models.Attributes +{ + /// + /// A custom that indicate an alternatives, hidden route. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] + public class AltRouteAttribute : RouteAttribute + { + /// + /// Create a new . + /// + /// The route template, see . + public AltRouteAttribute([NotNull] [RouteTemplateAttribute] string template) + : base(template) + { } + } +} diff --git a/src/Kyoo.Core/Kyoo.Core.csproj b/src/Kyoo.Core/Kyoo.Core.csproj index 58a23762..03e8785e 100644 --- a/src/Kyoo.Core/Kyoo.Core.csproj +++ b/src/Kyoo.Core/Kyoo.Core.csproj @@ -60,6 +60,7 @@ + diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index db4ac5bc..6ff5fe9b 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -22,6 +22,7 @@ using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Core.Models.Options; @@ -30,8 +31,8 @@ using Microsoft.Extensions.Options; namespace Kyoo.Core.Api { - [Route("api/collection")] [Route("api/collections")] + [AltRoute("api/collection")] [ApiController] [PartialPermission(nameof(CollectionApi))] public class CollectionApi : CrudApi @@ -51,8 +52,8 @@ namespace Kyoo.Core.Api _thumbs = thumbs; } - [HttpGet("{id:int}/show")] [HttpGet("{id:int}/shows")] + [AltHttpGet("{id:int}/show")] [PartialPermission(Kind.Read)] public async Task>> GetShows(int id, [FromQuery] string sortBy, @@ -77,8 +78,8 @@ namespace Kyoo.Core.Api } } - [HttpGet("{slug}/show")] [HttpGet("{slug}/shows")] + [AltHttpGet("{slug}/show")] [PartialPermission(Kind.Read)] public async Task>> GetShows(string slug, [FromQuery] string sortBy, @@ -103,8 +104,8 @@ namespace Kyoo.Core.Api } } - [HttpGet("{id:int}/library")] [HttpGet("{id:int}/libraries")] + [AltHttpGet("{id:int}/library")] [PartialPermission(Kind.Read)] public async Task>> GetLibraries(int id, [FromQuery] string sortBy, @@ -129,8 +130,8 @@ namespace Kyoo.Core.Api } } - [HttpGet("{slug}/library")] [HttpGet("{slug}/libraries")] + [AltHttpGet("{slug}/library")] [PartialPermission(Kind.Read)] public async Task>> GetLibraries(string slug, [FromQuery] string sortBy, diff --git a/src/Kyoo.Swagger/Kyoo.Swagger.csproj b/src/Kyoo.Swagger/Kyoo.Swagger.csproj index 13104cde..713eef6e 100644 --- a/src/Kyoo.Swagger/Kyoo.Swagger.csproj +++ b/src/Kyoo.Swagger/Kyoo.Swagger.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index c53703d4..84375c49 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -19,9 +19,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Attributes; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; @@ -47,9 +48,9 @@ namespace Kyoo.Swagger /// public void Configure(IServiceCollection services) { - services.AddSwaggerGen(x => + services.AddSwaggerGen(options => { - x.SwaggerDoc("v1", new OpenApiInfo + options.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "Kyoo API", @@ -67,7 +68,15 @@ namespace Kyoo.Swagger }); foreach (string documentation in Directory.GetFiles(AppContext.BaseDirectory, "*.xml")) - x.IncludeXmlComments(documentation); + options.IncludeXmlComments(documentation); + + options.UseAllOfForInheritance(); + + options.DocInclusionPredicate((_, apiDescription) => + { + return apiDescription.ActionDescriptor.EndpointMetadata + .All(x => x is not AltRouteAttribute && x is not AltHttpGetAttribute); + }); }); } @@ -78,6 +87,10 @@ namespace Kyoo.Swagger SA.New(app => app.UseSwaggerUI(x => { x.SwaggerEndpoint("/swagger/v1/swagger.json", "Kyoo v1"); + }), SA.Before), + SA.New(app => app.UseReDoc(x => + { + x.SpecUrl = "/swagger/v1/swagger.json"; }), SA.Before) }; } From ced12c2fe65db50f588ad3790f5ffc76daccc9f9 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 19 Sep 2021 22:25:07 +0200 Subject: [PATCH 05/36] Swagger: Cleaning up alternative routes and sort order --- .../Kyoo.Abstractions.csproj | 1 - .../AltRoute/AltHttpGetAttribute.cs | 19 ------------------- .../Constants.cs} | 19 ++++++------------- src/Kyoo.Core/Views/CollectionApi.cs | 12 ++++++------ src/Kyoo.Swagger/SwaggerModule.cs | 12 ++++-------- 5 files changed, 16 insertions(+), 47 deletions(-) delete mode 100644 src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs rename src/Kyoo.Abstractions/Models/{Attributes/AltRoute/AltRouteAttribute.cs => Utils/Constants.cs} (58%) diff --git a/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj b/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj index 59fdf472..3bc50076 100644 --- a/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj +++ b/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs deleted file mode 100644 index 99f843b6..00000000 --- a/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltHttpGetAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Abstractions.Models.Attributes -{ - /// - /// A custom that indicate an alternatives, hidden route. - /// - public class AltHttpGetAttribute : HttpGetAttribute - { - /// - /// Create a new . - /// - /// The route template, see . - public AltHttpGetAttribute([NotNull] [RouteTemplateAttribute] string template) - : base(template) - { } - } -} diff --git a/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltRouteAttribute.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs similarity index 58% rename from src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltRouteAttribute.cs rename to src/Kyoo.Abstractions/Models/Utils/Constants.cs index 71734979..595877fb 100644 --- a/src/Kyoo.Abstractions/Models/Attributes/AltRoute/AltRouteAttribute.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -16,24 +16,17 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Abstractions.Models.Attributes +namespace Kyoo.Abstractions.Models.Utils { /// - /// A custom that indicate an alternatives, hidden route. + /// A class containing constant numbers. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] - public class AltRouteAttribute : RouteAttribute + public static class Constants { /// - /// Create a new . + /// A property to use on a Microsoft.AspNet.MVC.Route.Order property to mark it as an alternative route + /// that won't be included on the swagger. /// - /// The route template, see . - public AltRouteAttribute([NotNull] [RouteTemplateAttribute] string template) - : base(template) - { } + public const int AlternativeRoute = 1; } } diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index 6ff5fe9b..93c54520 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -22,17 +22,17 @@ using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { [Route("api/collections")] - [AltRoute("api/collection")] + [Route("api/collection", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(CollectionApi))] public class CollectionApi : CrudApi @@ -53,7 +53,7 @@ namespace Kyoo.Core.Api } [HttpGet("{id:int}/shows")] - [AltHttpGet("{id:int}/show")] + [HttpGet("{id:int}/show", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] public async Task>> GetShows(int id, [FromQuery] string sortBy, @@ -79,7 +79,7 @@ namespace Kyoo.Core.Api } [HttpGet("{slug}/shows")] - [AltHttpGet("{slug}/show")] + [HttpGet("{slug}/show", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] public async Task>> GetShows(string slug, [FromQuery] string sortBy, @@ -105,7 +105,7 @@ namespace Kyoo.Core.Api } [HttpGet("{id:int}/libraries")] - [AltHttpGet("{id:int}/library")] + [HttpGet("{id:int}/library", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] public async Task>> GetLibraries(int id, [FromQuery] string sortBy, @@ -131,7 +131,7 @@ namespace Kyoo.Core.Api } [HttpGet("{slug}/libraries")] - [AltHttpGet("{slug}/library")] + [HttpGet("{slug}/library", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] public async Task>> GetLibraries(string slug, [FromQuery] string sortBy, diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 84375c49..92c947eb 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -19,12 +19,11 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models.Attributes; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Swagger { @@ -71,12 +70,9 @@ namespace Kyoo.Swagger options.IncludeXmlComments(documentation); options.UseAllOfForInheritance(); - - options.DocInclusionPredicate((_, apiDescription) => - { - return apiDescription.ActionDescriptor.EndpointMetadata - .All(x => x is not AltRouteAttribute && x is not AltHttpGetAttribute); - }); + options.SwaggerGeneratorOptions.SortKeySelector = x => x.RelativePath; + options.DocInclusionPredicate((_, apiDescription) + => apiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute); }); } From e32dcd0f300fc63fd1bd557026a1331f52c70b1e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 Sep 2021 12:44:29 +0200 Subject: [PATCH 06/36] Swagger: Trying to clean the xml documentation before giving it to swashbuckle --- src/Kyoo.Core/Views/CollectionApi.cs | 28 +++++++++ src/Kyoo.Core/Views/Helper/CrudApi.cs | 26 +++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 4 +- src/Kyoo.Swagger/XmlDocumentationLoader.cs | 68 ++++++++++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/Kyoo.Swagger/XmlDocumentationLoader.cs diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index 93c54520..e491effa 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -31,14 +31,28 @@ using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { + /// + /// Information about one or multiple . + /// [Route("api/collections")] [Route("api/collection", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(CollectionApi))] public class CollectionApi : CrudApi { + /// + /// 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, @@ -52,6 +66,20 @@ namespace Kyoo.Core.Api _thumbs = thumbs; } + /// + /// Lists that are contained in the with id . + /// + /// The ID of the . + /// A key to sort shows by. See for more information. + /// An optional show's ID to start the query from this specific item. + /// + /// An optional list of filters. See for more details. + /// + /// The number of shows to return. + /// A page of shows. + /// A page of shows. + /// or is invalid. + /// No collection with the ID could be found. [HttpGet("{id:int}/shows")] [HttpGet("{id:int}/show", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index 8bbc47dc..4f795f47 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -28,21 +28,47 @@ using Microsoft.AspNetCore.Mvc; namespace Kyoo.Core.Api { + /// + /// A base class to handle CRUD operations on a specific resource type . + /// + /// The type of resource to make CRUD apis for. [ApiController] [ResourceView] public class CrudApi : ControllerBase where T : class, IResource { + /// + /// The repository of the resource, used to retrieve, save and do operations on the baking store. + /// private readonly IRepository _repository; + /// + /// The base URL of Kyoo. This will be used to create links for images and . + /// protected Uri BaseURL { get; } + /// + /// Create a new using the given repository and base url. + /// + /// + /// The repository to use as a baking store for the type . + /// + /// + /// The base URL of Kyoo to use to create links. + /// public CrudApi(IRepository repository, Uri baseURL) { _repository = repository; BaseURL = baseURL; } + /// + /// Get a by ID. + /// + /// The ID of the resource to retrieve. + /// The retrieved . + /// The exist and is returned. + /// A resource with the ID does not exist. [HttpGet("{id:int}")] [PartialPermission(Kind.Read)] public virtual async Task> Get(int id) diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 92c947eb..9343cc33 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -66,9 +66,7 @@ namespace Kyoo.Swagger } }); - foreach (string documentation in Directory.GetFiles(AppContext.BaseDirectory, "*.xml")) - options.IncludeXmlComments(documentation); - + options.LoadXmlDocumentation(); options.UseAllOfForInheritance(); options.SwaggerGeneratorOptions.SortKeySelector = x => x.RelativePath; options.DocInclusionPredicate((_, apiDescription) diff --git a/src/Kyoo.Swagger/XmlDocumentationLoader.cs b/src/Kyoo.Swagger/XmlDocumentationLoader.cs new file mode 100644 index 00000000..b580e351 --- /dev/null +++ b/src/Kyoo.Swagger/XmlDocumentationLoader.cs @@ -0,0 +1,68 @@ +// 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.IO; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.Extensions.DependencyInjection; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Kyoo.Swagger +{ + /// + /// A static class containing a custom way to include XML to Swagger. + /// + public static class XmlDocumentationLoader + { + /// + /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files + /// + /// The swagger generator to add documentation to. + public static void LoadXmlDocumentation(this SwaggerGenOptions options) + { + ICollection docs = Directory.GetFiles(AppContext.BaseDirectory, "*.xml") + .Select(XDocument.Load) + .ToList(); + Dictionary elements = docs + .SelectMany(x => x.XPathSelectElements("/doc/members/member[@name and not(inheritdoc)]")) + .ToDictionary(x => x.Attribute("name")!.Value, x => x); + + foreach (XElement doc in docs + .SelectMany(x => x.XPathSelectElements("/doc/members/member[inheritdoc[@cref]]"))) + { + if (elements.TryGetValue(doc.Attribute("cref")!.Value, out XElement member)) + doc.Element("inheritdoc")!.ReplaceWith(member); + } + foreach (XElement doc in docs.SelectMany(x => x.XPathSelectElements("//see[@cref]"))) + { + string fullName = doc.Attribute("cref")!.Value; + string shortName = fullName[(fullName.LastIndexOf('.') + 1)..]; + // TODO won't work with fully qualified methods. + if (fullName.StartsWith("M:")) + shortName += "()"; + doc.ReplaceWith(shortName); + } + + foreach (XDocument doc in docs) + options.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true); + } + } +} From 561b8e81b29564b9ae7efe6b518e3f6e30b8ae7e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 Sep 2021 21:40:56 +0200 Subject: [PATCH 07/36] Swagger: Using NSwag instead of Swackbuckles --- .../Models/Utils/RequestError.cs | 57 +++++++ src/Kyoo.Core/CoreModule.cs | 18 +- src/Kyoo.Core/Views/CollectionApi.cs | 1 - src/Kyoo.Core/Views/Helper/CrudApi.cs | 155 ++++++++++++------ src/Kyoo.Swagger/GenericResponseProvider.cs | 65 ++++++++ src/Kyoo.Swagger/Kyoo.Swagger.csproj | 3 +- src/Kyoo.Swagger/SwaggerModule.cs | 53 +++--- src/Kyoo.Swagger/XmlDocumentationLoader.cs | 68 -------- 8 files changed, 273 insertions(+), 147 deletions(-) create mode 100644 src/Kyoo.Abstractions/Models/Utils/RequestError.cs create mode 100644 src/Kyoo.Swagger/GenericResponseProvider.cs delete mode 100644 src/Kyoo.Swagger/XmlDocumentationLoader.cs diff --git a/src/Kyoo.Abstractions/Models/Utils/RequestError.cs b/src/Kyoo.Abstractions/Models/Utils/RequestError.cs new file mode 100644 index 00000000..e37bf63c --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Utils/RequestError.cs @@ -0,0 +1,57 @@ +// 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.Linq; +using JetBrains.Annotations; + +namespace Kyoo.Abstractions.Models.Utils +{ + /// + /// The list of errors that where made in the request. + /// + public class RequestError + { + /// + /// The list of errors that where made in the request. + /// + [NotNull] public string[] Errors { get; set; } + + /// + /// Create a new with one error. + /// + /// The error to specify in the response. + public RequestError([NotNull] string error) + { + if (error == null) + throw new ArgumentNullException(nameof(error)); + Errors = new[] { error }; + } + + /// + /// Create a new with multiple errors. + /// + /// The errors to specify in the response. + public RequestError([NotNull] string[] errors) + { + if (errors == null || !errors.Any()) + throw new ArgumentException("Errors must be non null and not empty", nameof(errors)); + Errors = errors; + } + } +} diff --git a/src/Kyoo.Core/CoreModule.cs b/src/Kyoo.Core/CoreModule.cs index fc83caf5..bdae8b38 100644 --- a/src/Kyoo.Core/CoreModule.cs +++ b/src/Kyoo.Core/CoreModule.cs @@ -18,18 +18,21 @@ using System; using System.Collections.Generic; +using System.Linq; using Autofac; using Autofac.Core; using Autofac.Core.Registration; using Autofac.Extras.AttributeMetadata; using Kyoo.Abstractions; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Utils; using Kyoo.Core.Api; using Kyoo.Core.Controllers; using Kyoo.Core.Models.Options; using Kyoo.Core.Tasks; using Kyoo.Database; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -139,8 +142,21 @@ namespace Kyoo.Core string publicUrl = _configuration.GetPublicUrl(); services.AddMvcCore() + .AddDataAnnotations() .AddControllersAsServices() - .AddApiExplorer(); + .AddApiExplorer() + .ConfigureApiBehaviorOptions(options => + { + options.SuppressMapClientErrors = true; + options.InvalidModelStateResponseFactory = ctx => + { + string[] errors = ctx.ModelState + .SelectMany(x => x.Value.Errors) + .Select(x => x.ErrorMessage) + .ToArray(); + return new BadRequestObjectResult(new RequestError(errors)); + }; + }); services.AddControllers() .AddNewtonsoftJson(x => { diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index e491effa..91e9c24e 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -77,7 +77,6 @@ namespace Kyoo.Core.Api /// /// The number of shows to return. /// A page of shows. - /// A page of shows. /// or is invalid. /// No collection with the ID could be found. [HttpGet("{id:int}/shows")] diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index 4f795f47..ad10bfb9 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -24,6 +24,8 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; +using Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Kyoo.Core.Api @@ -63,15 +65,38 @@ namespace Kyoo.Core.Api } /// - /// Get a by ID. + /// Construct and return a page from an api. /// + /// The list of resources that should be included in the current page. + /// + /// The max number of items that should be present per page. This should be the same as in the request, + /// it is used to calculate if this is the last page and so on. + /// + /// The type of items on the page. + /// A Page representing the response. + protected Page Page(ICollection resources, int limit) + where TResult : IResource + { + return new Page(resources, + new Uri(BaseURL, Request.Path), + Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), + limit); + } + + /// + /// Get by ID + /// + /// + /// Get a specific resource via it's ID. + /// /// The ID of the resource to retrieve. - /// The retrieved . - /// The exist and is returned. - /// A resource with the ID does not exist. + /// The retrieved resource. + /// A resource with the given ID does not exist. [HttpGet("{id:int}")] [PartialPermission(Kind.Read)] - public virtual async Task> Get(int id) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(int id) { T ret = await _repository.GetOrDefault(id); if (ret == null) @@ -79,9 +104,20 @@ namespace Kyoo.Core.Api return ret; } + /// + /// Get by slug + /// + /// + /// Get a specific resource via it's slug (a unique, human readable identifier). + /// + /// The slug of the resource to retrieve. + /// The retrieved resource. + /// A resource with the given ID does not exist. [HttpGet("{slug}")] [PartialPermission(Kind.Read)] - public virtual async Task> Get(string slug) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(string slug) { T ret = await _repository.GetOrDefault(slug); if (ret == null) @@ -89,9 +125,20 @@ namespace Kyoo.Core.Api return ret; } + /// + /// Get count + /// + /// + /// Get the number of resources that match the filters. + /// + /// A list of filters to respect. + /// How many resources matched that filter. + /// Invalid filters. [HttpGet("count")] [PartialPermission(Kind.Read)] - public virtual async Task> GetCount([FromQuery] Dictionary where) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + public async Task> GetCount([FromQuery] Dictionary where) { try { @@ -99,13 +146,27 @@ namespace Kyoo.Core.Api } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + return BadRequest(new RequestError(ex.Message)); } } + /// + /// Get all + /// + /// + /// 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. + /// A list of resources that match every filters. + /// Invalid filters or sort information. [HttpGet] [PartialPermission(Kind.Read)] - public virtual async Task>> GetAll([FromQuery] string sortBy, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + public async Task>> GetAll([FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, [FromQuery] int limit = 20) @@ -120,21 +181,25 @@ namespace Kyoo.Core.Api } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + return BadRequest(new RequestError(ex.Message)); } } - protected Page Page(ICollection resources, int limit) - where TResult : IResource - { - return new Page(resources, - new Uri(BaseURL, Request.Path), - Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), - limit); - } - + /// + /// Create new + /// + /// + /// Create a new item and store it. You may leave the ID unspecified, it will be filed by Kyoo. + /// + /// The resource to create. + /// The created resource. + /// The resource in the request body is invalid. + /// This item already exists (maybe a duplicated slug). [HttpPost] [PartialPermission(Kind.Create)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))] public virtual async Task> Create([FromBody] T resource) { try @@ -143,7 +208,7 @@ namespace Kyoo.Core.Api } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + return BadRequest(new RequestError(ex.Message)); } catch (DuplicatedItemException) { @@ -152,9 +217,26 @@ namespace Kyoo.Core.Api } } + /// + /// Edit + /// + /// + /// Edit an item. If the ID is specified it will be used to identify the resource. + /// If not, the slug will be used to identify it. + /// + /// The resource to edit. + /// + /// Should old properties of the resource be discarded or should null values considered as not changed? + /// + /// The created resource. + /// The resource in the request body is invalid. + /// No item found with the specified ID (or slug). [HttpPut] [PartialPermission(Kind.Write)] - public virtual async Task> Edit([FromQuery] bool resetOld, [FromBody] T resource) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Edit([FromBody] T resource, [FromQuery] bool resetOld = true) { try { @@ -171,37 +253,6 @@ namespace Kyoo.Core.Api } } - [HttpPut("{id:int}")] - [PartialPermission(Kind.Write)] - public virtual async Task> Edit(int id, [FromQuery] bool resetOld, [FromBody] T resource) - { - resource.ID = id; - try - { - return await _repository.Edit(resource, resetOld); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpPut("{slug}")] - [PartialPermission(Kind.Write)] - public virtual async Task> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource) - { - try - { - T old = await _repository.Get(slug); - resource.ID = old.ID; - return await _repository.Edit(resource, resetOld); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - [HttpDelete("{id:int}")] [PartialPermission(Kind.Delete)] public virtual async Task Delete(int id) diff --git a/src/Kyoo.Swagger/GenericResponseProvider.cs b/src/Kyoo.Swagger/GenericResponseProvider.cs new file mode 100644 index 00000000..f9ebd37c --- /dev/null +++ b/src/Kyoo.Swagger/GenericResponseProvider.cs @@ -0,0 +1,65 @@ +// 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.Utils; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace Kyoo.Swagger +{ + /// + /// A filter that change 's + /// that where set to to the + /// return type of the method. + /// + /// + /// This is only useful when the return type of the method is a generics type and that can't be specified in the + /// attribute directly (since attributes don't support generics). This should not be used otherwise. + /// + public class GenericResponseProvider : IApplicationModelProvider + { + /// + public int Order => -1; + + /// + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { } + + /// + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + foreach (ActionModel action in context.Result.Controllers.SelectMany(x => x.Actions)) + { + IEnumerable responses = action.Filters + .OfType() + .Where(x => x.Type == typeof(ActionResult<>)); + foreach (ProducesResponseTypeAttribute response in responses) + { + Type type = action.ActionMethod.ReturnType; + type = Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0] ?? type; + type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type; + response.Type = type; + } + } + } + } +} diff --git a/src/Kyoo.Swagger/Kyoo.Swagger.csproj b/src/Kyoo.Swagger/Kyoo.Swagger.csproj index 713eef6e..54d4e2f3 100644 --- a/src/Kyoo.Swagger/Kyoo.Swagger.csproj +++ b/src/Kyoo.Swagger/Kyoo.Swagger.csproj @@ -7,8 +7,7 @@ - - + diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 9343cc33..8bea787a 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -18,11 +18,12 @@ using System; using System.Collections.Generic; -using System.IO; using Kyoo.Abstractions.Controllers; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.DependencyInjection; -using Microsoft.OpenApi.Models; +using NSwag; +using NSwag.Generation.AspNetCore; using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Swagger @@ -47,44 +48,50 @@ namespace Kyoo.Swagger /// public void Configure(IServiceCollection services) { - services.AddSwaggerGen(options => + services.AddTransient(); + services.AddOpenApiDocument(options => { - options.SwaggerDoc("v1", new OpenApiInfo + options.Title = "Kyoo API"; + // TODO use a real multi-line description in markdown. + options.Description = "The Kyoo's public API"; + options.Version = "1.0.0"; + options.DocumentName = "v1"; + options.UseControllerSummaryAsTagDescription = true; + options.GenerateExamples = true; + options.PostProcess = x => { - Version = "v1", - Title = "Kyoo API", - Description = "The Kyoo's public API", - Contact = new OpenApiContact + x.Info.Contact = new OpenApiContact { Name = "Kyoo's github", - Url = new Uri("https://github.com/AnonymusRaccoon/Kyoo/issues/new/choose") - }, - License = new OpenApiLicense + Url = "https://github.com/AnonymusRaccoon/Kyoo" + }; + x.Info.License = new OpenApiLicense { Name = "GPL-3.0-or-later", - Url = new Uri("https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE") - } + Url = "https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE" + }; + }; + options.AddOperationFilter(x => + { + if (x is AspNetCoreOperationProcessorContext ctx) + return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute; + return true; }); - - options.LoadXmlDocumentation(); - options.UseAllOfForInheritance(); - options.SwaggerGeneratorOptions.SortKeySelector = x => x.RelativePath; - options.DocInclusionPredicate((_, apiDescription) - => apiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute); }); } /// public IEnumerable ConfigureSteps => new IStartupAction[] { - SA.New(app => app.UseSwagger(), SA.Before + 1), - SA.New(app => app.UseSwaggerUI(x => + SA.New(app => app.UseOpenApi(), SA.Before + 1), + SA.New(app => app.UseSwaggerUi3(x => { - x.SwaggerEndpoint("/swagger/v1/swagger.json", "Kyoo v1"); + x.OperationsSorter = "alpha"; + x.TagsSorter = "alpha"; }), SA.Before), SA.New(app => app.UseReDoc(x => { - x.SpecUrl = "/swagger/v1/swagger.json"; + x.Path = "/redoc"; }), SA.Before) }; } diff --git a/src/Kyoo.Swagger/XmlDocumentationLoader.cs b/src/Kyoo.Swagger/XmlDocumentationLoader.cs deleted file mode 100644 index b580e351..00000000 --- a/src/Kyoo.Swagger/XmlDocumentationLoader.cs +++ /dev/null @@ -1,68 +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.IO; -using System.Linq; -using System.Xml.Linq; -using System.Xml.XPath; -using Microsoft.Extensions.DependencyInjection; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Kyoo.Swagger -{ - /// - /// A static class containing a custom way to include XML to Swagger. - /// - public static class XmlDocumentationLoader - { - /// - /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files - /// - /// The swagger generator to add documentation to. - public static void LoadXmlDocumentation(this SwaggerGenOptions options) - { - ICollection docs = Directory.GetFiles(AppContext.BaseDirectory, "*.xml") - .Select(XDocument.Load) - .ToList(); - Dictionary elements = docs - .SelectMany(x => x.XPathSelectElements("/doc/members/member[@name and not(inheritdoc)]")) - .ToDictionary(x => x.Attribute("name")!.Value, x => x); - - foreach (XElement doc in docs - .SelectMany(x => x.XPathSelectElements("/doc/members/member[inheritdoc[@cref]]"))) - { - if (elements.TryGetValue(doc.Attribute("cref")!.Value, out XElement member)) - doc.Element("inheritdoc")!.ReplaceWith(member); - } - foreach (XElement doc in docs.SelectMany(x => x.XPathSelectElements("//see[@cref]"))) - { - string fullName = doc.Attribute("cref")!.Value; - string shortName = fullName[(fullName.LastIndexOf('.') + 1)..]; - // TODO won't work with fully qualified methods. - if (fullName.StartsWith("M:")) - shortName += "()"; - doc.ReplaceWith(shortName); - } - - foreach (XDocument doc in docs) - options.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true); - } - } -} From f0e9054b36a942769d8e8d17083641eb25784772 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 21 Sep 2021 12:37:59 +0200 Subject: [PATCH 08/36] CollectionAPI: Documenting the public api --- .../Controllers/IRepository.cs | 1 - .../Models/Utils/RequestError.cs | 1 + src/Kyoo.Core/Views/CollectionApi.cs | 55 ++++++++++++++++--- src/Kyoo.Core/Views/Helper/CrudApi.cs | 51 ++++++++++++++--- 4 files changed, 90 insertions(+), 18 deletions(-) diff --git a/src/Kyoo.Abstractions/Controllers/IRepository.cs b/src/Kyoo.Abstractions/Controllers/IRepository.cs index 805de8ec..21a65c12 100644 --- a/src/Kyoo.Abstractions/Controllers/IRepository.cs +++ b/src/Kyoo.Abstractions/Controllers/IRepository.cs @@ -179,7 +179,6 @@ namespace Kyoo.Abstractions.Controllers /// Delete all resources that match the predicate. /// /// A predicate to filter resources to delete. Every resource that match this will be deleted. - /// If the item is not found /// A representing the asynchronous operation. Task DeleteAll([NotNull] Expression> where); } diff --git a/src/Kyoo.Abstractions/Models/Utils/RequestError.cs b/src/Kyoo.Abstractions/Models/Utils/RequestError.cs index e37bf63c..fa40fc64 100644 --- a/src/Kyoo.Abstractions/Models/Utils/RequestError.cs +++ b/src/Kyoo.Abstractions/Models/Utils/RequestError.cs @@ -30,6 +30,7 @@ namespace Kyoo.Abstractions.Models.Utils /// /// The list of errors that where made in the request. /// + /// ["InvalidFilter: no field 'startYear' on a collection"] [NotNull] public string[] Errors { get; set; } /// diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index 91e9c24e..e920f52c 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -24,7 +24,9 @@ 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; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using static Kyoo.Abstractions.Models.Utils.Constants; @@ -67,21 +69,25 @@ namespace Kyoo.Core.Api } /// - /// Lists that are contained in the with id . + /// Get shows in collection (via id) /// + /// + /// Lists the shows that are contained in the collection with the given id. + /// /// The ID of the . - /// A key to sort shows by. See for more information. + /// A key to sort shows by. /// An optional show's ID to start the query from this specific item. - /// - /// An optional list of filters. See for more details. - /// + /// An optional list of filters. /// The number of shows to return. /// A page of shows. - /// or is invalid. - /// No collection with the ID could be found. + /// 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)] [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetShows(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -101,13 +107,30 @@ namespace Kyoo.Core.Api } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + 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, @@ -127,10 +150,24 @@ namespace Kyoo.Core.Api } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + return BadRequest(new RequestError(ex.Message)); } } + /// + /// Get libraries containing this collection + /// + /// + /// Lists the libraries that contain the collection with the given id. + /// + /// 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("{id:int}/libraries")] [HttpGet("{id:int}/library", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index ad10bfb9..1c214da3 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -45,7 +45,8 @@ namespace Kyoo.Core.Api private readonly IRepository _repository; /// - /// The base URL of Kyoo. This will be used to create links for images and . + /// The base URL of Kyoo. This will be used to create links for images and + /// . /// protected Uri BaseURL { get; } @@ -110,7 +111,7 @@ namespace Kyoo.Core.Api /// /// Get a specific resource via it's slug (a unique, human readable identifier). /// - /// The slug of the resource to retrieve. + /// The slug of the resource to retrieve. /// The retrieved resource. /// A resource with the given ID does not exist. [HttpGet("{slug}")] @@ -166,7 +167,8 @@ namespace Kyoo.Core.Api [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] - public async Task>> GetAll([FromQuery] string sortBy, + public async Task>> GetAll( + [FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, [FromQuery] int limit = 20) @@ -253,9 +255,20 @@ namespace Kyoo.Core.Api } } + /// + /// Delete by ID + /// + /// + /// Delete one item via it's ID. + /// + /// The ID of the resource to delete. + /// The item has successfully been deleted. + /// No item could be found with the given id. [HttpDelete("{id:int}")] [PartialPermission(Kind.Delete)] - public virtual async Task Delete(int id) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(int id) { try { @@ -269,9 +282,20 @@ namespace Kyoo.Core.Api return Ok(); } + /// + /// Delete by slug + /// + /// + /// Delete one item via it's slug (an unique, human-readable identifier). + /// + /// The slug of the resource to delete. + /// The item has successfully been deleted. + /// No item could be found with the given slug. [HttpDelete("{slug}")] [PartialPermission(Kind.Delete)] - public virtual async Task Delete(string slug) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(string slug) { try { @@ -285,17 +309,28 @@ namespace Kyoo.Core.Api return Ok(); } + /// + /// Delete all where + /// + /// + /// Delete all items matching the given filters. If no filter is specified, delete all items. + /// + /// The list of filters. + /// The item(s) has successfully been deleted. + /// One or multiple filters are invalid. [HttpDelete] [PartialPermission(Kind.Delete)] - public virtual async Task Delete(Dictionary where) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + public async Task Delete([FromQuery] Dictionary where) { try { await _repository.DeleteAll(ApiHelper.ParseWhere(where)); } - catch (ItemNotFoundException) + catch (ArgumentException ex) { - return NotFound(); + return BadRequest(new RequestError(ex.Message)); } return Ok(); From 41cbc50940f4dbc5b495c9ae4565c6a1d869387b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 21 Sep 2021 16:42:49 +0200 Subject: [PATCH 09/36] API: Starting to merge id/slug routes using a new Identifier --- .../Models/Utils/Identifier.cs | 112 ++++++++++++++++++ .../Controllers/IdentifierRouteConstraint.cs | 40 +++++++ src/Kyoo.Core/CoreModule.cs | 17 ++- src/Kyoo.Core/Views/Helper/CrudApi.cs | 82 ++++--------- src/Kyoo.Swagger/SwaggerModule.cs | 8 ++ 5 files changed, 191 insertions(+), 68 deletions(-) create mode 100644 src/Kyoo.Abstractions/Models/Utils/Identifier.cs create mode 100644 src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs diff --git a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs new file mode 100644 index 00000000..356ff3d8 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -0,0 +1,112 @@ +// 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.ComponentModel; +using System.Globalization; +using JetBrains.Annotations; + +namespace Kyoo.Abstractions.Models.Utils +{ + /// + /// A class that represent a resource. It is made to be used as a parameter in a query and not used somewhere else + /// on the application. + /// This class allow routes to be used via ether IDs or Slugs, this is suitable for every . + /// + [TypeConverter(typeof(IdentifierConvertor))] + public class Identifier + { + /// + /// The ID of the resource or null if the slug is specified. + /// + private readonly int? _id; + + /// + /// The slug of the resource or null if the id is specified. + /// + private readonly string _slug; + + /// + /// Create a new for the given id. + /// + /// The id of the resource. + public Identifier(int id) + { + _id = id; + } + + /// + /// Create a new for the given slug. + /// + /// The slug of the resource. + public Identifier([NotNull] string slug) + { + if (slug == null) + throw new ArgumentNullException(nameof(slug)); + _slug = slug; + } + + /// + /// Pattern match out of the identifier to a resource. + /// + /// The function to match the ID to a type . + /// The function to match the slug to a type . + /// The return type that will be converted to from an ID or a slug. + /// + /// The result of the or depending on the pattern. + /// + /// + /// Example usage: + /// + /// T ret = await identifier.Match( + /// id => _repository.GetOrDefault(id), + /// slug => _repository.GetOrDefault(slug) + /// ); + /// + /// + public T Match(Func idFunc, Func slugFunc) + { + return _id.HasValue + ? idFunc(_id.Value) + : slugFunc(_slug); + } + + public class IdentifierConvertor : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(int) || sourceType == typeof(string)) + return true; + return base.CanConvertFrom(context, sourceType); + } + + /// + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is int id) + return new Identifier(id); + if (value is not string slug) + return base.ConvertFrom(context, culture, value); + return int.TryParse(slug, out id) + ? new Identifier(id) + : new Identifier(slug); + } + } + } +} diff --git a/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs b/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs new file mode 100644 index 00000000..cda79146 --- /dev/null +++ b/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs @@ -0,0 +1,40 @@ +// 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 Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Kyoo.Core.Controllers +{ + /// + /// The route constraint that goes with the . + /// + public class IdentifierRouteConstraint : IRouteConstraint + { + /// + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + return values.ContainsKey(routeKey); + } + } +} diff --git a/src/Kyoo.Core/CoreModule.cs b/src/Kyoo.Core/CoreModule.cs index bdae8b38..5b0fe2cc 100644 --- a/src/Kyoo.Core/CoreModule.cs +++ b/src/Kyoo.Core/CoreModule.cs @@ -33,6 +33,7 @@ using Kyoo.Core.Tasks; using Kyoo.Database; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -145,6 +146,11 @@ namespace Kyoo.Core .AddDataAnnotations() .AddControllersAsServices() .AddApiExplorer() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); + x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); + }) .ConfigureApiBehaviorOptions(options => { options.SuppressMapClientErrors = true; @@ -157,12 +163,11 @@ namespace Kyoo.Core return new BadRequestObjectResult(new RequestError(errors)); }; }); - services.AddControllers() - .AddNewtonsoftJson(x => - { - x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); - x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); - }); + + services.Configure(x => + { + x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint)); + }); services.AddResponseCompression(x => { diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index 1c214da3..a90cfce1 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -85,42 +85,24 @@ namespace Kyoo.Core.Api } /// - /// Get by ID + /// Get item /// /// - /// Get a specific resource via it's ID. + /// Get a specific resource via it's ID or it's slug. /// - /// The ID of the resource to retrieve. + /// The ID or slug of the resource to retrieve. /// The retrieved resource. - /// A resource with the given ID does not exist. - [HttpGet("{id:int}")] + /// A resource with the given ID or slug does not exist. + [HttpGet("{identifier:id}")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get(int id) + public async Task> Get(Identifier identifier) { - T ret = await _repository.GetOrDefault(id); - if (ret == null) - return NotFound(); - return ret; - } - - /// - /// Get by slug - /// - /// - /// Get a specific resource via it's slug (a unique, human readable identifier). - /// - /// The slug of the resource to retrieve. - /// The retrieved resource. - /// A resource with the given ID does not exist. - [HttpGet("{slug}")] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get(string slug) - { - T ret = await _repository.GetOrDefault(slug); + T ret = await identifier.Match( + id => _repository.GetOrDefault(id), + slug => _repository.GetOrDefault(slug) + ); if (ret == null) return NotFound(); return ret; @@ -256,50 +238,26 @@ namespace Kyoo.Core.Api } /// - /// Delete by ID + /// Delete an item /// /// - /// Delete one item via it's ID. + /// Delete one item via it's ID or it's slug. /// - /// The ID of the resource to delete. + /// The ID or slug of the resource to delete. /// The item has successfully been deleted. - /// No item could be found with the given id. - [HttpDelete("{id:int}")] + /// No item could be found with the given id or slug. + [HttpDelete("{identifier:id}")] [PartialPermission(Kind.Delete)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Delete(int id) + public async Task Delete(Identifier identifier) { try { - await _repository.Delete(id); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - - return Ok(); - } - - /// - /// Delete by slug - /// - /// - /// Delete one item via it's slug (an unique, human-readable identifier). - /// - /// The slug of the resource to delete. - /// The item has successfully been deleted. - /// No item could be found with the given slug. - [HttpDelete("{slug}")] - [PartialPermission(Kind.Delete)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Delete(string slug) - { - try - { - await _repository.Delete(slug); + await identifier.Match( + id => _repository.Delete(id), + slug => _repository.Delete(slug) + ); } catch (ItemNotFoundException) { diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 8bea787a..dfc4135f 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -19,9 +19,12 @@ using System; using System.Collections.Generic; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.DependencyInjection; +using NJsonSchema; +using NJsonSchema.Generation.TypeMappers; using NSwag; using NSwag.Generation.AspNetCore; using static Kyoo.Abstractions.Models.Utils.Constants; @@ -77,6 +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) + ); }); } From 9b3eb7fede561e6bbb015b55efad2fbc8acb5891 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 21 Sep 2021 21:13:03 +0200 Subject: [PATCH 10/36] API: Creating a common thumbs api --- .../Models/Utils/Identifier.cs | 24 +++ .../Models/Utils/Pagination.cs | 4 +- .../Repositories/LocalRepository.cs | 4 +- src/Kyoo.Core/Views/CollectionApi.cs | 181 +++--------------- src/Kyoo.Core/Views/Helper/CrudApi.cs | 34 ++-- src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 146 ++++++++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 10 +- 7 files changed, 224 insertions(+), 179 deletions(-) create mode 100644 src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs 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; + })); }); } From 2a22661c4689dc131c663485093ede90ea4991d0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 22 Sep 2021 14:44:21 +0200 Subject: [PATCH 11/36] Swagger: handling tags and sort order --- .../Attributes/ApiDefinitionAttribute.cs | 51 ++++++++++++ .../Models/Utils/Constants.cs | 7 ++ src/Kyoo.Core/Views/CollectionApi.cs | 17 ++++ src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 46 +++++++---- src/Kyoo.Swagger/SwaggerModule.cs | 78 +++++++++++++++++-- 5 files changed, 176 insertions(+), 23 deletions(-) create mode 100644 src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs diff --git a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs new file mode 100644 index 00000000..84d8dc3d --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs @@ -0,0 +1,51 @@ +// 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 JetBrains.Annotations; + +namespace Kyoo.Abstractions.Models.Attributes +{ + /// + /// An attribute to specify on apis to specify it's documentation's name and category. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ApiDefinitionAttribute : Attribute + { + /// + /// The public name of this api. + /// + [NotNull] public string Name { get; } + + /// + /// The name of the group in witch this API is. + /// + public string Group { get; set; } + + /// + /// Create a new . + /// + /// The name of the api that will be used on the documentation page. + public ApiDefinitionAttribute([NotNull] string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + Name = name; + } + } +} diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 595877fb..190307af 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -16,6 +16,8 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using Kyoo.Abstractions.Models.Attributes; + namespace Kyoo.Abstractions.Models.Utils { /// @@ -28,5 +30,10 @@ namespace Kyoo.Abstractions.Models.Utils /// that won't be included on the swagger. /// public const int AlternativeRoute = 1; + + /// + /// A group name for . It should be used for every . + /// + public const string ResourceGroup = "Resource"; } } diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index b9a627cf..22b0608e 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -22,12 +22,14 @@ 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 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 @@ -39,6 +41,7 @@ namespace Kyoo.Core.Api [Route("api/collection", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(CollectionApi))] + [ApiDefinition("Collection", Group = ResourceGroup)] public class CollectionApi : CrudThumbsApi { /// @@ -46,6 +49,17 @@ namespace Kyoo.Core.Api /// private readonly ILibraryManager _libraryManager; + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information about the data store. + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + /// + /// Options used to retrieve the base URL of Kyoo. + /// public CollectionApi(ILibraryManager libraryManager, IFileSystem files, IThumbnailsManager thumbs, @@ -115,6 +129,9 @@ namespace Kyoo.Core.Api [HttpGet("{identifier:id}/libraries")] [HttpGet("{identifier:id}/library", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetLibraries(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, diff --git a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 7d3ccc42..95a5782c 100644 --- a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -28,12 +28,37 @@ using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { + /// + /// A base class to handle CRUD operations and services thumbnails for + /// a specific resource type . + /// + /// The type of resource to make CRUD and thumbnails apis for. + [ApiController] + [ResourceView] public class CrudThumbsApi : CrudApi where T : class, IResource, IThumbnails { + /// + /// The file manager used to send images. + /// private readonly IFileSystem _files; + + /// + /// The thumbnail manager used to retrieve images paths. + /// private readonly IThumbnailsManager _thumbs; + /// + /// Create a new that handles crud requests and thumbnails. + /// + /// + /// The repository to use as a baking store for the type . + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + /// + /// The base URL of Kyoo to use to create links. + /// public CrudThumbsApi(IRepository repository, IFileSystem files, IThumbnailsManager thumbs, @@ -49,26 +74,17 @@ namespace Kyoo.Core.Api /// /// /// 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 - /// - /// + /// 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. - /// + /// 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)] diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 4d743b3a..ad98de7d 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -18,11 +18,15 @@ 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; @@ -61,18 +65,42 @@ namespace Kyoo.Swagger options.DocumentName = "v1"; options.UseControllerSummaryAsTagDescription = true; options.GenerateExamples = true; - options.PostProcess = x => + options.PostProcess = postProcess => { - x.Info.Contact = new OpenApiContact + postProcess.Info.Contact = new OpenApiContact { Name = "Kyoo's github", Url = "https://github.com/AnonymusRaccoon/Kyoo" }; - x.Info.License = new OpenApiLicense + postProcess.Info.License = new OpenApiLicense { 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.AddOperationFilter(x => { @@ -80,6 +108,44 @@ namespace Kyoo.Swagger 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; @@ -92,11 +158,7 @@ namespace Kyoo.Swagger public IEnumerable ConfigureSteps => new IStartupAction[] { SA.New(app => app.UseOpenApi(), SA.Before + 1), - SA.New(app => app.UseSwaggerUi3(x => - { - x.OperationsSorter = "alpha"; - x.TagsSorter = "alpha"; - }), SA.Before), + SA.New(app => app.UseSwaggerUi3(), SA.Before), SA.New(app => app.UseReDoc(x => { x.Path = "/redoc"; From bd4fd848e16ba654f9ffd9bbdffcdbdbdef32d5a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 22 Sep 2021 20:54:48 +0200 Subject: [PATCH 12/36] 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); } } } From 8a5e7fea068a5900a699e53fb92304915421c3b5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 23 Sep 2021 11:44:37 +0200 Subject: [PATCH 13/36] API: documenting the season api --- .../Models/Utils/Constants.cs | 2 +- .../Models/Utils/Identifier.cs | 17 ++ src/Kyoo.Core/Views/SeasonApi.cs | 181 +++++++----------- src/Kyoo.Core/Views/ShowApi.cs | 8 +- 4 files changed, 88 insertions(+), 120 deletions(-) diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 190307af..dd0ac925 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -34,6 +34,6 @@ namespace Kyoo.Abstractions.Models.Utils /// /// A group name for . It should be used for every . /// - public const string ResourceGroup = "Resource"; + public const string ResourceGroup = "Resources"; } } diff --git a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs index bd83e975..228a6a6e 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -110,6 +110,23 @@ namespace Kyoo.Abstractions.Models.Utils return Expression.Lambda>(equal); } + /// + /// A matcher overload for nullable IDs. See + /// + /// 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. + 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. /// diff --git a/src/Kyoo.Core/Views/SeasonApi.cs b/src/Kyoo.Core/Views/SeasonApi.cs index 126603ea..99bac0dc 100644 --- a/src/Kyoo.Core/Views/SeasonApi.cs +++ b/src/Kyoo.Core/Views/SeasonApi.cs @@ -22,163 +22,114 @@ 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 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/season")] + /// + /// Information about one or multiple . + /// [Route("api/seasons")] + [Route("api/season", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(SeasonApi))] - public class SeasonApi : CrudApi + [ApiDefinition("Seasons", Group = ResourceGroup)] + public class SeasonApi : CrudThumbsApi { + /// + /// The library manager used to modify or retrieve information in the data store. + /// private readonly ILibraryManager _libraryManager; - private readonly IThumbnailsManager _thumbs; - private readonly IFileSystem _files; + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + /// + /// Options used to retrieve the base URL of Kyoo. + /// public SeasonApi(ILibraryManager libraryManager, - IOptions options, + IFileSystem files, IThumbnailsManager thumbs, - IFileSystem files) - : base(libraryManager.SeasonRepository, options.Value.PublicUrl) + IOptions options) + : base(libraryManager.SeasonRepository, files, thumbs, options.Value.PublicUrl) { _libraryManager = libraryManager; - _thumbs = thumbs; - _files = files; } - [HttpGet("{seasonID:int}/episode")] - [HttpGet("{seasonID:int}/episodes")] + /// + /// Get episodes in the season + /// + /// + /// List the episodes that are part of the specified season. + /// + /// 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 season 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>> GetEpisode(int seasonID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetEpisode(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.SeasonID == seasonID), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.SeasonID, x => x.Season.Slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetOrDefault(seasonID) == 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("{showSlug}-s{seasonNumber:int}/episode")] - [HttpGet("{showSlug}-s{seasonNumber:int}/episodes")] + /// + /// Get season's show + /// + /// + /// Get the show that this season is part of. + /// + /// The ID or slug of the . + /// The show that contains this season. + /// No season with the given ID or slug could be found. + [HttpGet("{identifier:id}/show")] [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(string showSlug, - int seasonNumber, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetShow(Identifier identifier) { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Show.Slug == showSlug - && x.SeasonNumber == seasonNumber), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(showSlug, seasonNumber) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{showID:int}-s{seasonNumber:int}/episode")] - [HttpGet("{showID:int}-s{seasonNumber:int}/episodes")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(int showID, - int seasonNumber, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.ShowID == showID && x.SeasonNumber == seasonNumber), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(showID, seasonNumber) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{seasonID:int}/show")] - [PartialPermission(Kind.Read)] - public async Task> GetShow(int seasonID) - { - Show ret = await _libraryManager.GetOrDefault(x => x.Seasons.Any(y => y.ID == seasonID)); + Show ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Seasons)); if (ret == null) return NotFound(); return ret; } - - [HttpGet("{showSlug}-s{seasonNumber:int}/show")] - [PartialPermission(Kind.Read)] - public async Task> GetShow(string showSlug, int seasonNumber) - { - Show ret = await _libraryManager.GetOrDefault(showSlug); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{showID:int}-s{seasonNumber:int}/show")] - [PartialPermission(Kind.Read)] - public async Task> GetShow(int showID, int seasonNumber) - { - Show ret = await _libraryManager.GetOrDefault(showID); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{id:int}/poster")] - public async Task GetPoster(int id) - { - Season season = await _libraryManager.GetOrDefault(id); - if (season == null) - return NotFound(); - await _libraryManager.Load(season, x => x.Show); - return _files.FileResult(await _thumbs.GetImagePath(season, Images.Poster)); - } - - [HttpGet("{slug}/poster")] - public async Task GetPoster(string slug) - { - Season season = await _libraryManager.GetOrDefault(slug); - if (season == null) - return NotFound(); - await _libraryManager.Load(season, x => x.Show); - return _files.FileResult(await _thumbs.GetImagePath(season, Images.Poster)); - } } } diff --git a/src/Kyoo.Core/Views/ShowApi.cs b/src/Kyoo.Core/Views/ShowApi.cs index f819e9ab..306de40b 100644 --- a/src/Kyoo.Core/Views/ShowApi.cs +++ b/src/Kyoo.Core/Views/ShowApi.cs @@ -48,7 +48,7 @@ namespace Kyoo.Core.Api public class ShowApi : CrudThumbsApi { /// - /// The library manager used to modify or retrieve information about the data store. + /// The library manager used to modify or retrieve information in the data store. /// private readonly ILibraryManager _libraryManager; @@ -279,7 +279,7 @@ namespace Kyoo.Core.Api } /// - /// Get libraries containing this show. + /// Get libraries containing this show /// /// /// List the libraries that contain this show. If this show is contained in a collection that is contained in @@ -324,7 +324,7 @@ namespace Kyoo.Core.Api } /// - /// Get collections containing this show. + /// Get collections containing this show /// /// /// List the collections that contain this show. @@ -337,8 +337,8 @@ namespace Kyoo.Core.Api /// 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")] + [HttpGet("{identifier:id}/collection", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] From e4d703223cee6dc267c8de29d7ad12093b6970c6 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 23 Sep 2021 13:20:51 +0200 Subject: [PATCH 14/36] API: Documenting the episode's api --- AUTHORS.md | 2 +- CONTRIBUTING.md | 1 + .../Models/Resources/Track.cs | 31 ++- .../Models/Utils/Constants.cs | 2 +- .../Repositories/EpisodeRepository.cs | 1 - src/Kyoo.Core/Views/CollectionApi.cs | 2 +- src/Kyoo.Core/Views/EpisodeApi.cs | 259 +++++++----------- src/Kyoo.Core/Views/SeasonApi.cs | 2 +- src/Kyoo.Core/Views/ShowApi.cs | 2 +- 9 files changed, 123 insertions(+), 179 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index b7f88d9d..ad32290a 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,4 +1,4 @@ # Authors -Alphabetical order by first name. +Ordered by the date of the first commit. * Zoe Roux ([@AnonymusRaccoon](http://github.com/AnonymusRaccoon)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e36365a..6dbaad61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,7 @@ Here are a few things you can do that will increase the likelihood of your pull ## Resources +- [Why should you indent with tabs](https://www.reddit.com/r/javascript/comments/c8drjo/nobody_talks_about_the_real_reason_to_use_tabs/) - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Using Pull Requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) - [GitHub Help](https://docs.github.com/en) diff --git a/src/Kyoo.Abstractions/Models/Resources/Track.cs b/src/Kyoo.Abstractions/Models/Resources/Track.cs index e6d5d7e1..14f471b0 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Track.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Track.cs @@ -73,7 +73,7 @@ namespace Kyoo.Abstractions.Models { string type = Type.ToString().ToLower(); string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; - string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString(); + string episode = _episodeSlug ?? Episode?.Slug ?? EpisodeID.ToString(); return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : string.Empty)}.{type}"; } @@ -90,7 +90,7 @@ namespace Kyoo.Abstractions.Models "Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]"); } - EpisodeSlug = match.Groups["ep"].Value; + _episodeSlug = match.Groups["ep"].Value; Language = match.Groups["lang"].Value; if (Language == "und") Language = null; @@ -100,11 +100,6 @@ namespace Kyoo.Abstractions.Models } } - /// - /// The slug of the episode that contain this track. If this is not set, this track is ill-formed. - /// - [SerializeIgnore] public string EpisodeSlug { private get; set; } - /// /// The title of the stream. /// @@ -153,7 +148,16 @@ namespace Kyoo.Abstractions.Models /// /// The episode that uses this track. /// - [LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; } + [LoadableRelation(nameof(EpisodeID))] public Episode Episode + { + get => _episode; + set + { + _episode = value; + if (_episode != null) + _episodeSlug = _episode.Slug; + } + } /// /// The index of this track on the episode. @@ -184,6 +188,17 @@ namespace Kyoo.Abstractions.Models } } + /// + /// The slug of the episode that contain this track. If this is not set, this track is ill-formed. + /// + [SerializeIgnore] private string _episodeSlug; + + /// + /// The episode that uses this track. + /// This is the baking field of . + /// + [SerializeIgnore] private Episode _episode; + // Converting mkv track language to c# system language tag. private static string _GetLanguage(string mkvLanguage) { diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index dd0ac925..5267347b 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -34,6 +34,6 @@ namespace Kyoo.Abstractions.Models.Utils /// /// A group name for . It should be used for every . /// - public const string ResourceGroup = "Resources"; + public const string ResourcesGroup = "Resources"; } } diff --git a/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index f2042c65..2ceebd9c 100644 --- a/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -169,7 +169,6 @@ namespace Kyoo.Core.Controllers resource.Tracks = await resource.Tracks.SelectAsync(x => { x.Episode = resource; - x.EpisodeSlug = resource.Slug; return _tracks.Create(x); }).ToListAsync(); _database.Tracks.AttachRange(resource.Tracks); diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index 65f49a90..705fe745 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -40,7 +40,7 @@ namespace Kyoo.Core.Api [Route("api/collection", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(CollectionApi))] - [ApiDefinition("Collections", Group = ResourceGroup)] + [ApiDefinition("Collections", Group = ResourcesGroup)] public class CollectionApi : CrudThumbsApi { /// diff --git a/src/Kyoo.Core/Views/EpisodeApi.cs b/src/Kyoo.Core/Views/EpisodeApi.cs index 37591b99..fc5b03ae 100644 --- a/src/Kyoo.Core/Views/EpisodeApi.cs +++ b/src/Kyoo.Core/Views/EpisodeApi.cs @@ -22,216 +22,145 @@ using System.Linq; 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/episode")] + /// + /// Information about one or multiple . + /// [Route("api/episodes")] + [Route("api/episode", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(EpisodeApi))] - public class EpisodeApi : CrudApi + [ApiDefinition("Episodes", Group = ResourcesGroup)] + public class EpisodeApi : CrudThumbsApi { + /// + /// The library manager used to modify or retrieve information in the data store. + /// private readonly ILibraryManager _libraryManager; - private readonly IThumbnailsManager _thumbnails; - private readonly IFileSystem _files; + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + /// + /// Options used to retrieve the base URL of Kyoo. + /// public EpisodeApi(ILibraryManager libraryManager, - IOptions options, IFileSystem files, - IThumbnailsManager thumbnails) - : base(libraryManager.EpisodeRepository, options.Value.PublicUrl) + IThumbnailsManager thumbnails, + IOptions options) + : base(libraryManager.EpisodeRepository, files, thumbnails, options.Value.PublicUrl) { _libraryManager = libraryManager; - _files = files; - _thumbnails = thumbnails; } - [HttpGet("{episodeID:int}/show")] + /// + /// Get episode's show + /// + /// + /// Get the show that this episode is part of. + /// + /// The ID or slug of the . + /// The show that contains this episode. + /// No episode with the given ID or slug could be found. + [HttpGet("{identifier:id}/show")] [PartialPermission(Kind.Read)] - public async Task> GetShow(int episodeID) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetShow(Identifier identifier) { - Show ret = await _libraryManager.GetOrDefault(x => x.Episodes.Any(y => y.ID == episodeID)); + Show ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Episodes)); if (ret == null) return NotFound(); return ret; } - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/show")] + /// + /// Get episode's season + /// + /// + /// Get the season that this episode is part of. + /// + /// The ID or slug of the . + /// The season that contains this episode. + /// The episode is not part of a season. + /// No episode with the given ID or slug could be found. + [HttpGet("{identifier:id}/season")] [PartialPermission(Kind.Read)] - public async Task> GetShow(string showSlug, int seasonNumber, int episodeNumber) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetSeason(Identifier identifier) { - Show ret = await _libraryManager.GetOrDefault(showSlug); - if (ret == null) - return NotFound(); - return ret; + Season ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Episodes)); + if (ret != null) + return ret; + Episode episode = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => _libraryManager.GetOrDefault(slug) + ); + return episode == null + ? NotFound() + : NoContent(); } - [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")] + /// + /// Get tracks + /// + /// + /// List the tracks (video, audio and subtitles) available for this episode. + /// This endpoint provide the list of raw tracks, without transcode on it. To get a schema easier to watch + /// on a player, see the [/watch endpoint](#/watch). + /// + /// The ID or slug of the . + /// A key to sort tracks by. + /// An optional list of filters. + /// The number of tracks to return. + /// An optional track's ID to start the query from this specific item. + /// A page of tracks. + /// The filters or the sort parameters are invalid. + /// No track with the given ID or slug could be found. + /// TODO fix the /watch endpoint link (when operations ID are specified). + [HttpGet("{identifier:id}/tracks")] + [HttpGet("{identifier:id}/track", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task> GetShow(int showID, int seasonNumber, int episodeNumber) - { - Show ret = await _libraryManager.GetOrDefault(showID); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{episodeID:int}/season")] - [PartialPermission(Kind.Read)] - public async Task> GetSeason(int episodeID) - { - Season ret = await _libraryManager.GetOrDefault(x => x.Episodes.Any(y => y.ID == episodeID)); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")] - [PartialPermission(Kind.Read)] - public async Task> GetSeason(string showSlug, int seasonNumber, int episodeNumber) - { - try - { - return await _libraryManager.Get(showSlug, seasonNumber); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/season")] - [PartialPermission(Kind.Read)] - public async Task> GetSeason(int showID, int seasonNumber, int episodeNumber) - { - try - { - return await _libraryManager.Get(showID, seasonNumber); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{episodeID:int}/track")] - [HttpGet("{episodeID:int}/tracks")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(int episodeID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetEpisode(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.Episode.ID == episodeID), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.EpisodeID, x => x.Episode.Slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetOrDefault(episodeID) == 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("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/track")] - [HttpGet("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(int showID, - int seasonNumber, - int episodeNumber, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Episode.ShowID == showID - && x.Episode.SeasonNumber == seasonNumber - && x.Episode.EpisodeNumber == episodeNumber), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(showID, seasonNumber, episodeNumber) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/track")] - [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(string slug, - int seasonNumber, - int episodeNumber, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Episode.Show.Slug == slug - && x.Episode.SeasonNumber == seasonNumber - && x.Episode.EpisodeNumber == episodeNumber), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(slug, seasonNumber, episodeNumber) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{id:int}/thumbnail")] - [HttpGet("{id:int}/backdrop")] - public async Task GetThumb(int id) - { - try - { - Episode episode = await _libraryManager.Get(id); - return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{slug}/thumbnail")] - [HttpGet("{slug}/backdrop")] - public async Task GetThumb(string slug) - { - try - { - Episode episode = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail)); - } - catch (ItemNotFoundException) - { - return NotFound(); + return BadRequest(new RequestError(ex.Message)); } } } diff --git a/src/Kyoo.Core/Views/SeasonApi.cs b/src/Kyoo.Core/Views/SeasonApi.cs index 99bac0dc..75da206d 100644 --- a/src/Kyoo.Core/Views/SeasonApi.cs +++ b/src/Kyoo.Core/Views/SeasonApi.cs @@ -40,7 +40,7 @@ namespace Kyoo.Core.Api [Route("api/season", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(SeasonApi))] - [ApiDefinition("Seasons", Group = ResourceGroup)] + [ApiDefinition("Seasons", Group = ResourcesGroup)] public class SeasonApi : CrudThumbsApi { /// diff --git a/src/Kyoo.Core/Views/ShowApi.cs b/src/Kyoo.Core/Views/ShowApi.cs index 306de40b..42bde2ae 100644 --- a/src/Kyoo.Core/Views/ShowApi.cs +++ b/src/Kyoo.Core/Views/ShowApi.cs @@ -44,7 +44,7 @@ namespace Kyoo.Core.Api [Route("api/movies", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(ShowApi))] - [ApiDefinition("Shows", Group = ResourceGroup)] + [ApiDefinition("Shows", Group = ResourcesGroup)] public class ShowApi : CrudThumbsApi { /// From 38ec742db6336e5626cbedf1947f420a85e14e4a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 23 Sep 2021 13:49:25 +0200 Subject: [PATCH 15/36] API: Documenting the track's api --- .../Models/Resources/Track.cs | 2 +- src/Kyoo.Core/Views/TrackApi.cs | 63 +++++++++++-------- tests/Kyoo.Tests/Database/TestSample.cs | 31 +++++---- 3 files changed, 56 insertions(+), 40 deletions(-) diff --git a/src/Kyoo.Abstractions/Models/Resources/Track.cs b/src/Kyoo.Abstractions/Models/Resources/Track.cs index 14f471b0..8e41c7ee 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Track.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Track.cs @@ -52,7 +52,7 @@ namespace Kyoo.Abstractions.Models Subtitle = 3, /// - /// The stream is an attachement (a font, an image or something else). + /// The stream is an attachment (a font, an image or something else). /// Only fonts are handled by kyoo but they are not saved to the database. /// Attachment = 4 diff --git a/src/Kyoo.Core/Views/TrackApi.cs b/src/Kyoo.Core/Views/TrackApi.cs index 7075bcbc..1d2e89cb 100644 --- a/src/Kyoo.Core/Views/TrackApi.cs +++ b/src/Kyoo.Core/Views/TrackApi.cs @@ -16,58 +16,69 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System.Linq; 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/track")] + /// + /// Information about one or multiple . + /// [Route("api/tracks")] + [Route("api/track", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(Track))] + [ApiDefinition("Tracks", Group = ResourcesGroup)] public class TrackApi : CrudApi { + /// + /// The library manager used to modify or retrieve information in the data store. + /// private readonly ILibraryManager _libraryManager; + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// + /// + /// Options used to retrieve the base URL of Kyoo. + /// public TrackApi(ILibraryManager libraryManager, IOptions options) : base(libraryManager.TrackRepository, options.Value.PublicUrl) { _libraryManager = libraryManager; } - [HttpGet("{id:int}/episode")] + /// + /// Get track's episode + /// + /// + /// Get the episode that uses this track. + /// + /// The ID or slug of the . + /// The episode that uses this track. + /// No track with the given ID or slug could be found. + [HttpGet("{identifier:id}/episode")] [PartialPermission(Kind.Read)] - public async Task> GetEpisode(int id) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetEpisode(Identifier identifier) { - try - { - return await _libraryManager.Get(x => x.Tracks.Any(y => y.ID == id)); - } - catch (ItemNotFoundException) - { + Episode ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Tracks)); + if (ret == null) return NotFound(); - } - } - - [HttpGet("{slug}/episode")] - [PartialPermission(Kind.Read)] - public async Task> GetEpisode(string slug) - { - try - { - return await _libraryManager.Get(x => x.Tracks.Any(y => y.Slug == slug)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } + return ret; } } } diff --git a/tests/Kyoo.Tests/Database/TestSample.cs b/tests/Kyoo.Tests/Database/TestSample.cs index 13b9a0e9..a7327753 100644 --- a/tests/Kyoo.Tests/Database/TestSample.cs +++ b/tests/Kyoo.Tests/Database/TestSample.cs @@ -241,20 +241,25 @@ namespace Kyoo.Tests }, { typeof(Track), - () => new Track + () => { - ID = 1, - EpisodeID = 1, - Codec = "subrip", - Language = "eng", - Path = "/path", - Title = "Subtitle track", - Type = StreamType.Subtitle, - EpisodeSlug = Get().Slug, - IsDefault = true, - IsExternal = false, - IsForced = false, - TrackIndex = 1 + Track ret = new() + { + ID = 1, + EpisodeID = 1, + Codec = "subrip", + Language = "eng", + Path = "/path", + Title = "Subtitle track", + Episode = Get(), + Type = StreamType.Subtitle, + IsDefault = true, + IsExternal = false, + IsForced = false, + TrackIndex = 1 + }; + ret.Episode = null; + return ret; } }, { From 2287919b60a1dc09804ed2f9946fd352978ea907 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 24 Sep 2021 10:22:02 +0200 Subject: [PATCH 16/36] API: Documenting the libraries'api --- .../Permission/PartialPermissionAttribute.cs | 5 + .../Controllers/PermissionValidator.cs | 25 +- src/Kyoo.Core/Views/EpisodeApi.cs | 2 +- src/Kyoo.Core/Views/Helper/CrudApi.cs | 2 +- src/Kyoo.Core/Views/LibraryApi.cs | 238 +++++++++--------- 5 files changed, 137 insertions(+), 135 deletions(-) diff --git a/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs index 58e6366a..3cad4b6d 100644 --- a/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs +++ b/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs @@ -39,6 +39,11 @@ namespace Kyoo.Abstractions.Models.Permissions /// public Kind Kind { get; } + /// + /// The group of this permission. + /// + public Group Group { get; set; } + /// /// Ask a permission to run an action. /// diff --git a/src/Kyoo.Authentication/Controllers/PermissionValidator.cs b/src/Kyoo.Authentication/Controllers/PermissionValidator.cs index 2f4a84f4..490c7649 100644 --- a/src/Kyoo.Authentication/Controllers/PermissionValidator.cs +++ b/src/Kyoo.Authentication/Controllers/PermissionValidator.cs @@ -61,7 +61,7 @@ namespace Kyoo.Authentication /// public IFilterMetadata Create(PartialPermissionAttribute attribute) { - return new PermissionValidatorFilter((object)attribute.Type ?? attribute.Kind, _options); + return new PermissionValidatorFilter((object)attribute.Type ?? attribute.Kind, attribute.Group, _options); } /// @@ -109,15 +109,24 @@ namespace Kyoo.Authentication /// Create a new permission validator with the given options. /// /// The partial permission to validate. + /// The group of the permission. /// The option containing default values. - public PermissionValidatorFilter(object partialInfo, IOptionsMonitor options) + public PermissionValidatorFilter(object partialInfo, Group? group, IOptionsMonitor options) { - if (partialInfo is Kind kind) - _kind = kind; - else if (partialInfo is string perm) - _permission = perm; - else - throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind."); + switch (partialInfo) + { + case Kind kind: + _kind = kind; + break; + case string perm: + _permission = perm; + break; + default: + throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind."); + } + + if (group != null) + _group = group.Value; _options = options; } diff --git a/src/Kyoo.Core/Views/EpisodeApi.cs b/src/Kyoo.Core/Views/EpisodeApi.cs index fc5b03ae..bdba37a2 100644 --- a/src/Kyoo.Core/Views/EpisodeApi.cs +++ b/src/Kyoo.Core/Views/EpisodeApi.cs @@ -133,7 +133,7 @@ namespace Kyoo.Core.Api /// An optional track's ID to start the query from this specific item. /// A page of tracks. /// The filters or the sort parameters are invalid. - /// No track with the given ID or slug could be found. + /// No episode with the given ID or slug could be found. /// TODO fix the /watch endpoint link (when operations ID are specified). [HttpGet("{identifier:id}/tracks")] [HttpGet("{identifier:id}/track", Order = AlternativeRoute)] diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index 6e11ed6b..1b16b2b0 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -186,7 +186,7 @@ namespace Kyoo.Core.Api [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))] - public virtual async Task> Create([FromBody] T resource) + public async Task> Create([FromBody] T resource) { try { diff --git a/src/Kyoo.Core/Views/LibraryApi.cs b/src/Kyoo.Core/Views/LibraryApi.cs index 0199aa3c..26ebebde 100644 --- a/src/Kyoo.Core/Views/LibraryApi.cs +++ b/src/Kyoo.Core/Views/LibraryApi.cs @@ -19,198 +19,186 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; 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 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/library")] + /// + /// Information about one or multiple . + /// [Route("api/libraries")] + [Route("api/library", Order = AlternativeRoute)] [ApiController] - [PartialPermission(nameof(LibraryApi))] + [PartialPermission(nameof(LibraryApi), Group = Group.Admin)] + [ApiDefinition("Library", Group = ResourcesGroup)] public class LibraryApi : CrudApi { + /// + /// The library manager used to modify or retrieve information in the data store. + /// private readonly ILibraryManager _libraryManager; - private readonly ITaskManager _taskManager; - public LibraryApi(ILibraryManager libraryManager, ITaskManager taskManager, IOptions options) + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// + /// + /// Options used to retrieve the base URL of Kyoo. + /// + public LibraryApi(ILibraryManager libraryManager, IOptions options) : base(libraryManager.LibraryRepository, options.Value.PublicUrl) { _libraryManager = libraryManager; - _taskManager = taskManager; } - [PartialPermission(Kind.Create)] - public override async Task> Create(Library resource) - { - ActionResult result = await base.Create(resource); - if (result.Value != null) - { - _taskManager.StartTask("scan", - new Progress(), - new Dictionary { { "slug", result.Value.Slug } }); - } - return result; - } - - [HttpGet("{id:int}/show")] - [HttpGet("{id:int}/shows")] + /// + /// Get shows + /// + /// + /// List the shows that are part of this library. + /// + /// 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 library with the given ID or slug could be found. + [HttpGet("{identifier:id}/shows")] + [HttpGet("{identifier:id}/show", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetShows(int id, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetShows(Identifier identifier, [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.Libraries.Any(y => y.ID == id)), + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Libraries)), new Sort(sortBy), - new Pagination(limit, afterID)); + 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 }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{slug}/show")] - [HttpGet("{slug}/shows")] + /// + /// Get collections + /// + /// + /// List the collections that are part of this library. + /// + /// 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 library with the given ID or slug could be found. + [HttpGet("{identifier:id}/collections")] + [HttpGet("{identifier:id}/collection", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetShows(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 = 20) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Libraries.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("{id:int}/collection")] - [HttpGet("{id:int}/collections")] - [PartialPermission(Kind.Read)] - public async Task>> GetCollections(int id, - [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.Libraries.Any(y => y.ID == id)), + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Libraries)), new Sort(sortBy), - new Pagination(limit, afterID)); + 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 }); + return BadRequest(new RequestError(ex.Message)); } } - [HttpGet("{slug}/collection")] - [HttpGet("{slug}/collections")] + /// + /// Get items + /// + /// + /// List all items of this library. + /// An item can ether represent a collection or a show. + /// This endpoint allow one to retrieve all collections and shows that are not contained in a collection. + /// This is what is displayed on the /browse page of the webapp. + /// + /// The ID or slug of the . + /// A key to sort items by. + /// An optional list of filters. + /// The number of items to return. + /// An optional item's ID to start the query from this specific item. + /// A page of items. + /// The filters or the sort parameters are invalid. + /// No library with the given ID or slug could be found. + [HttpGet("{identifier:id}/items")] + [HttpGet("{identifier:id}/item", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetCollections(string slug, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetItems(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 20) + [FromQuery] int limit = 50, + [FromQuery] int? afterID = null) { try { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Libraries.Any(y => y.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.GetItemsFromLibrary(id, whereQuery, sort, pagination), + slug => _libraryManager.GetItemsFromLibrary(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 }); - } - } - - [HttpGet("{id:int}/item")] - [HttpGet("{id:int}/items")] - [PartialPermission(Kind.Read)] - public async Task>> GetItems(int id, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 50) - { - try - { - ICollection resources = await _libraryManager.GetItemsFromLibrary(id, - ApiHelper.ParseWhere(where), - 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}/item")] - [HttpGet("{slug}/items")] - [PartialPermission(Kind.Read)] - public async Task>> GetItems(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 50) - { - try - { - ICollection resources = await _libraryManager.GetItemsFromLibrary(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 }); + return BadRequest(new RequestError(ex.Message)); } } } From 7e4fab1ee1d1020338a2764b44899949615841c7 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 24 Sep 2021 11:45:00 +0200 Subject: [PATCH 17/36] API: Documenting the items's API AND removing the BaseUrl need on APIs constructor, retriving it on the controller DI service --- .editorconfig | 2 + .../Models/Utils/Constants.cs | 7 +- src/Kyoo.Abstractions/Module.cs | 6 +- .../AuthenticationModule.cs | 10 +-- src/Kyoo.Core/CoreModule.cs | 2 +- src/Kyoo.Core/Views/CollectionApi.cs | 10 +-- src/Kyoo.Core/Views/EpisodeApi.cs | 11 +-- src/Kyoo.Core/Views/GenreApi.cs | 6 +- src/Kyoo.Core/Views/Helper/ApiHelper.cs | 4 ++ src/Kyoo.Core/Views/Helper/BaseApi.cs | 60 ++++++++++++++++ src/Kyoo.Core/Views/Helper/CrudApi.cs | 34 +-------- src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 9 +-- .../Helper/Serializers/JsonPropertyIgnorer.cs | 4 +- .../Helper/Serializers/SerializeAsProvider.cs | 10 +-- src/Kyoo.Core/Views/LibraryApi.cs | 12 ++-- src/Kyoo.Core/Views/LibraryItemApi.cs | 70 +++++++++++++------ src/Kyoo.Core/Views/PeopleApi.cs | 5 +- src/Kyoo.Core/Views/ProviderApi.cs | 5 +- src/Kyoo.Core/Views/SeasonApi.cs | 10 +-- src/Kyoo.Core/Views/ShowApi.cs | 11 ++- src/Kyoo.Core/Views/StudioApi.cs | 6 +- src/Kyoo.Core/Views/TrackApi.cs | 13 ++-- 22 files changed, 172 insertions(+), 135 deletions(-) create mode 100644 src/Kyoo.Core/Views/Helper/BaseApi.cs diff --git a/.editorconfig b/.editorconfig index 01763e5f..7b812979 100644 --- a/.editorconfig +++ b/.editorconfig @@ -89,3 +89,5 @@ resharper_xmldoc_attribute_indent = align_by_first_attribute resharper_xmldoc_indent_child_elements = RemoveIndent resharper_xmldoc_indent_text = RemoveIndent +# Waiting for https://github.com/dotnet/roslyn/issues/44596 to get fixed. +# file_header_template = Kyoo - A portable and vast media library solution.\nCopyright (c) Kyoo.\n\nSee AUTHORS.md and LICENSE file in the project root for full license information.\n\nKyoo is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\nany later version.\n\nKyoo is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with Kyoo. If not, see . diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 5267347b..521dc2d2 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -32,8 +32,13 @@ namespace Kyoo.Abstractions.Models.Utils public const int AlternativeRoute = 1; /// - /// A group name for . It should be used for every . + /// A group name for . It should be used for main resources of kyoo. /// public const string ResourcesGroup = "Resources"; + + /// + /// A group name for . It should be used for endpoints useful for playback. + /// + public const string WatchGroup = "Watch"; } } diff --git a/src/Kyoo.Abstractions/Module.cs b/src/Kyoo.Abstractions/Module.cs index 102b9e2e..e0f8c53f 100644 --- a/src/Kyoo.Abstractions/Module.cs +++ b/src/Kyoo.Abstractions/Module.cs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using System; using Autofac; using Autofac.Builder; using Kyoo.Abstractions.Controllers; @@ -96,9 +97,10 @@ namespace Kyoo.Abstractions /// /// The configuration instance /// The public URl of kyoo (without a slash at the end) - public static string GetPublicUrl(this IConfiguration configuration) + public static Uri GetPublicUrl(this IConfiguration configuration) { - return configuration["basics:publicUrl"]?.TrimEnd('/') ?? "http://localhost:5000"; + string uri = configuration["basics:publicUrl"]?.TrimEnd('/') ?? "http://localhost:5000"; + return new Uri(uri); } } } diff --git a/src/Kyoo.Authentication/AuthenticationModule.cs b/src/Kyoo.Authentication/AuthenticationModule.cs index 82a224fa..d5e73eb9 100644 --- a/src/Kyoo.Authentication/AuthenticationModule.cs +++ b/src/Kyoo.Authentication/AuthenticationModule.cs @@ -104,7 +104,7 @@ namespace Kyoo.Authentication DefaultCorsPolicyService cors = new(_logger) { - AllowedOrigins = { new Uri(_configuration.GetPublicUrl()).GetLeftPart(UriPartial.Authority) } + AllowedOrigins = { _configuration.GetPublicUrl().GetLeftPart(UriPartial.Authority) } }; builder.RegisterInstance(cors).As().SingleInstance(); } @@ -112,7 +112,7 @@ namespace Kyoo.Authentication /// public void Configure(IServiceCollection services) { - string publicUrl = _configuration.GetPublicUrl(); + Uri publicUrl = _configuration.GetPublicUrl(); if (_environment.IsDevelopment()) IdentityModelEventSource.ShowPII = true; @@ -136,7 +136,7 @@ namespace Kyoo.Authentication services.AddIdentityServer(options => { - options.IssuerUri = publicUrl; + options.IssuerUri = publicUrl.ToString(); options.UserInteraction.LoginUrl = $"{publicUrl}/login"; options.UserInteraction.ErrorUrl = $"{publicUrl}/error"; options.UserInteraction.LogoutUrl = $"{publicUrl}/logout"; @@ -151,7 +151,7 @@ namespace Kyoo.Authentication services.AddAuthentication() .AddJwtBearer(options => { - options.Authority = publicUrl; + options.Authority = publicUrl.ToString(); options.Audience = "kyoo"; options.RequireHttpsMetadata = false; }); @@ -189,7 +189,7 @@ namespace Kyoo.Authentication { app.Use((ctx, next) => { - ctx.SetIdentityServerOrigin(_configuration.GetPublicUrl()); + ctx.SetIdentityServerOrigin(_configuration.GetPublicUrl().ToString()); return next(); }); app.UseIdentityServer(); diff --git a/src/Kyoo.Core/CoreModule.cs b/src/Kyoo.Core/CoreModule.cs index 5b0fe2cc..f601ad25 100644 --- a/src/Kyoo.Core/CoreModule.cs +++ b/src/Kyoo.Core/CoreModule.cs @@ -140,7 +140,7 @@ namespace Kyoo.Core /// public void Configure(IServiceCollection services) { - string publicUrl = _configuration.GetPublicUrl(); + Uri publicUrl = _configuration.GetPublicUrl(); services.AddMvcCore() .AddDataAnnotations() diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index 705fe745..89091a7d 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -25,10 +25,8 @@ using Kyoo.Abstractions.Models; 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 @@ -56,14 +54,10 @@ namespace Kyoo.Core.Api /// /// The file manager used to send images. /// The thumbnail manager used to retrieve images paths. - /// - /// Options used to retrieve the base URL of Kyoo. - /// public CollectionApi(ILibraryManager libraryManager, IFileSystem files, - IThumbnailsManager thumbs, - IOptions options) - : base(libraryManager.CollectionRepository, files, thumbs, options.Value.PublicUrl) + IThumbnailsManager thumbs) + : base(libraryManager.CollectionRepository, files, thumbs) { _libraryManager = libraryManager; } diff --git a/src/Kyoo.Core/Views/EpisodeApi.cs b/src/Kyoo.Core/Views/EpisodeApi.cs index bdba37a2..d716dbe7 100644 --- a/src/Kyoo.Core/Views/EpisodeApi.cs +++ b/src/Kyoo.Core/Views/EpisodeApi.cs @@ -25,10 +25,8 @@ using Kyoo.Abstractions.Models; 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 @@ -39,6 +37,7 @@ namespace Kyoo.Core.Api [Route("api/episodes")] [Route("api/episode", Order = AlternativeRoute)] [ApiController] + [ResourceView] [PartialPermission(nameof(EpisodeApi))] [ApiDefinition("Episodes", Group = ResourcesGroup)] public class EpisodeApi : CrudThumbsApi @@ -56,14 +55,10 @@ namespace Kyoo.Core.Api /// /// The file manager used to send images. /// The thumbnail manager used to retrieve images paths. - /// - /// Options used to retrieve the base URL of Kyoo. - /// public EpisodeApi(ILibraryManager libraryManager, IFileSystem files, - IThumbnailsManager thumbnails, - IOptions options) - : base(libraryManager.EpisodeRepository, files, thumbnails, options.Value.PublicUrl) + IThumbnailsManager thumbnails) + : base(libraryManager.EpisodeRepository, files, thumbnails) { _libraryManager = libraryManager; } diff --git a/src/Kyoo.Core/Views/GenreApi.cs b/src/Kyoo.Core/Views/GenreApi.cs index 3ec1c7ed..28bc3067 100644 --- a/src/Kyoo.Core/Views/GenreApi.cs +++ b/src/Kyoo.Core/Views/GenreApi.cs @@ -23,9 +23,7 @@ using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace Kyoo.Core.Api { @@ -37,8 +35,8 @@ namespace Kyoo.Core.Api { private readonly ILibraryManager _libraryManager; - public GenreApi(ILibraryManager libraryManager, IOptions options) - : base(libraryManager.GenreRepository, options.Value.PublicUrl) + public GenreApi(ILibraryManager libraryManager) + : base(libraryManager.GenreRepository) { _libraryManager = libraryManager; } diff --git a/src/Kyoo.Core/Views/Helper/ApiHelper.cs b/src/Kyoo.Core/Views/Helper/ApiHelper.cs index 7365c5a7..d2e926fa 100644 --- a/src/Kyoo.Core/Views/Helper/ApiHelper.cs +++ b/src/Kyoo.Core/Views/Helper/ApiHelper.cs @@ -23,6 +23,10 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; using Kyoo.Abstractions.Models; +using Kyoo.Core.Models.Options; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Kyoo.Core.Api { diff --git a/src/Kyoo.Core/Views/Helper/BaseApi.cs b/src/Kyoo.Core/Views/Helper/BaseApi.cs new file mode 100644 index 00000000..315e92fd --- /dev/null +++ b/src/Kyoo.Core/Views/Helper/BaseApi.cs @@ -0,0 +1,60 @@ +// 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 Kyoo.Abstractions; +using Kyoo.Abstractions.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Core.Api +{ + public class BaseApi : ControllerBase + { + /// + /// Construct and return a page from an api. + /// + /// The list of resources that should be included in the current page. + /// + /// The max number of items that should be present per page. This should be the same as in the request, + /// it is used to calculate if this is the last page and so on. + /// + /// The type of items on the page. + /// A Page representing the response. + protected Page Page(ICollection resources, int limit) + where TResult : IResource + { + Uri publicUrl = HttpContext.RequestServices + .GetRequiredService() + .GetPublicUrl(); + return new Page( + resources, + new Uri(publicUrl, Request.Path), + Request.Query.ToDictionary( + x => x.Key, + x => x.Value.ToString(), + StringComparer.InvariantCultureIgnoreCase + ), + limit + ); + } + } +} diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index 1b16b2b0..16e8beb1 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -18,7 +18,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; @@ -36,7 +35,7 @@ namespace Kyoo.Core.Api /// The type of resource to make CRUD apis for. [ApiController] [ResourceView] - public class CrudApi : ControllerBase + public class CrudApi : BaseApi where T : class, IResource { /// @@ -44,44 +43,15 @@ namespace Kyoo.Core.Api /// protected IRepository Repository { get; } - /// - /// The base URL of Kyoo. This will be used to create links for images and - /// . - /// - protected Uri BaseURL { get; } - /// /// Create a new using the given repository and base url. /// /// /// The repository to use as a baking store for the type . /// - /// - /// The base URL of Kyoo to use to create links. - /// - public CrudApi(IRepository repository, Uri baseURL) + public CrudApi(IRepository repository) { Repository = repository; - BaseURL = baseURL; - } - - /// - /// Construct and return a page from an api. - /// - /// The list of resources that should be included in the current page. - /// - /// The max number of items that should be present per page. This should be the same as in the request, - /// it is used to calculate if this is the last page and so on. - /// - /// The type of items on the page. - /// A Page representing the response. - protected Page Page(ICollection resources, int limit) - where TResult : IResource - { - return new Page(resources, - new Uri(BaseURL, Request.Path), - Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), - limit); } /// diff --git a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 68acd112..89e2e8c0 100644 --- a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -16,7 +16,6 @@ // 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; @@ -56,14 +55,10 @@ namespace Kyoo.Core.Api /// /// The file manager used to send images. /// The thumbnail manager used to retrieve images paths. - /// - /// The base URL of Kyoo to use to create links. - /// public CrudThumbsApi(IRepository repository, IFileSystem files, - IThumbnailsManager thumbs, - Uri baseURL) - : base(repository, baseURL) + IThumbnailsManager thumbs) + : base(repository) { _files = files; _thumbs = thumbs; diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs index d5f23fce..65c00f7f 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs @@ -30,9 +30,9 @@ namespace Kyoo.Core.Api public class JsonPropertyIgnorer : CamelCasePropertyNamesContractResolver { private int _depth = -1; - private string _host; + private readonly Uri _host; - public JsonPropertyIgnorer(string host) + public JsonPropertyIgnorer(Uri host) { _host = host; } diff --git a/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs b/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs index b16e63ac..ff104fa7 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs @@ -26,13 +26,13 @@ namespace Kyoo.Core.Api { public class SerializeAsProvider : IValueProvider { - private string _format; - private string _host; + private readonly string _format; + private readonly Uri _host; - public SerializeAsProvider(string format, string host) + public SerializeAsProvider(string format, Uri host) { _format = format; - _host = host.TrimEnd('/'); + _host = host; } public object GetValue(object target) @@ -43,7 +43,7 @@ namespace Kyoo.Core.Api string modifier = x.Groups[3].Value; if (value == "HOST") - return _host; + return _host.ToString().TrimEnd('/'); PropertyInfo properties = target.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) diff --git a/src/Kyoo.Core/Views/LibraryApi.cs b/src/Kyoo.Core/Views/LibraryApi.cs index 26ebebde..045b7b70 100644 --- a/src/Kyoo.Core/Views/LibraryApi.cs +++ b/src/Kyoo.Core/Views/LibraryApi.cs @@ -26,10 +26,8 @@ using Kyoo.Abstractions.Models; 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 @@ -40,6 +38,7 @@ namespace Kyoo.Core.Api [Route("api/libraries")] [Route("api/library", Order = AlternativeRoute)] [ApiController] + [ResourceView] [PartialPermission(nameof(LibraryApi), Group = Group.Admin)] [ApiDefinition("Library", Group = ResourcesGroup)] public class LibraryApi : CrudApi @@ -55,11 +54,8 @@ namespace Kyoo.Core.Api /// /// The library manager used to modify or retrieve information in the data store. /// - /// - /// Options used to retrieve the base URL of Kyoo. - /// - public LibraryApi(ILibraryManager libraryManager, IOptions options) - : base(libraryManager.LibraryRepository, options.Value.PublicUrl) + public LibraryApi(ILibraryManager libraryManager) + : base(libraryManager.LibraryRepository) { _libraryManager = libraryManager; } @@ -159,7 +155,7 @@ namespace Kyoo.Core.Api /// List all items of this library. /// An item can ether represent a collection or a show. /// This endpoint allow one to retrieve all collections and shows that are not contained in a collection. - /// This is what is displayed on the /browse page of the webapp. + /// This is what is displayed on the /browse/library page of the webapp. /// /// The ID or slug of the . /// A key to sort items by. diff --git a/src/Kyoo.Core/Views/LibraryItemApi.cs b/src/Kyoo.Core/Views/LibraryItemApi.cs index 1b1137fb..030c60e0 100644 --- a/src/Kyoo.Core/Views/LibraryItemApi.cs +++ b/src/Kyoo.Core/Views/LibraryItemApi.cs @@ -18,59 +18,85 @@ 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.Exceptions; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; +using Kyoo.Abstractions.Models.Utils; +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/item")] + /// + /// Endpoint for items that are not part of a specific library. + /// An item can ether represent a collection or a show. + /// [Route("api/items")] + [Route("api/item", Order = AlternativeRoute)] [ApiController] [ResourceView] - public class LibraryItemApi : ControllerBase + [ApiDefinition("Items", Group = ResourcesGroup)] + public class LibraryItemApi : BaseApi { + /// + /// The library item repository used to modify or retrieve information in the data store. + /// private readonly ILibraryItemRepository _libraryItems; - private readonly Uri _baseURL; - public LibraryItemApi(ILibraryItemRepository libraryItems, IOptions options) + /// + /// Create a new . + /// + /// + /// The library item repository used to modify or retrieve information in the data store. + /// + public LibraryItemApi(ILibraryItemRepository libraryItems) { _libraryItems = libraryItems; - _baseURL = options.Value.PublicUrl; } + /// + /// Get items + /// + /// + /// List all items of kyoo. + /// An item can ether represent a collection or a show. + /// This endpoint allow one to retrieve all collections and shows that are not contained in a collection. + /// This is what is displayed on the /browse page of the webapp. + /// + /// A key to sort items by. + /// An optional list of filters. + /// The number of items to return. + /// An optional item's ID to start the query from this specific item. + /// A page of items. + /// The filters or the sort parameters are invalid. + /// No library with the given ID or slug could be found. [HttpGet] [Permission(nameof(LibraryItemApi), Kind.Read)] - public async Task>> GetAll([FromQuery] string sortBy, - [FromQuery] int afterID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetAll( + [FromQuery] string sortBy, [FromQuery] Dictionary where, - [FromQuery] int limit = 50) + [FromQuery] int limit = 50, + [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryItems.GetAll( ApiHelper.ParseWhere(where), new Sort(sortBy), - new Pagination(limit, afterID)); + new Pagination(limit, afterID) + ); - return new Page(resources, - new Uri(_baseURL, Request.Path), - Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), - limit); - } - catch (ItemNotFoundException) - { - return NotFound(); + return Page(resources, limit); } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + return BadRequest(new RequestError(ex.Message)); } } } diff --git a/src/Kyoo.Core/Views/PeopleApi.cs b/src/Kyoo.Core/Views/PeopleApi.cs index 2d810927..2246d2ad 100644 --- a/src/Kyoo.Core/Views/PeopleApi.cs +++ b/src/Kyoo.Core/Views/PeopleApi.cs @@ -23,9 +23,7 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace Kyoo.Core.Api { @@ -39,10 +37,9 @@ namespace Kyoo.Core.Api private readonly IThumbnailsManager _thumbs; public PeopleApi(ILibraryManager libraryManager, - IOptions options, IFileSystem files, IThumbnailsManager thumbs) - : base(libraryManager.PeopleRepository, options.Value.PublicUrl) + : base(libraryManager.PeopleRepository) { _libraryManager = libraryManager; _files = files; diff --git a/src/Kyoo.Core/Views/ProviderApi.cs b/src/Kyoo.Core/Views/ProviderApi.cs index 582c4d3d..0ba3244a 100644 --- a/src/Kyoo.Core/Views/ProviderApi.cs +++ b/src/Kyoo.Core/Views/ProviderApi.cs @@ -20,9 +20,7 @@ using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace Kyoo.Core.Api { @@ -37,10 +35,9 @@ namespace Kyoo.Core.Api private readonly IFileSystem _files; public ProviderApi(ILibraryManager libraryManager, - IOptions options, IFileSystem files, IThumbnailsManager thumbnails) - : base(libraryManager.ProviderRepository, options.Value.PublicUrl) + : base(libraryManager.ProviderRepository) { _libraryManager = libraryManager; _files = files; diff --git a/src/Kyoo.Core/Views/SeasonApi.cs b/src/Kyoo.Core/Views/SeasonApi.cs index 75da206d..a92cc256 100644 --- a/src/Kyoo.Core/Views/SeasonApi.cs +++ b/src/Kyoo.Core/Views/SeasonApi.cs @@ -25,10 +25,8 @@ using Kyoo.Abstractions.Models; 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 @@ -56,14 +54,10 @@ namespace Kyoo.Core.Api /// /// The file manager used to send images. /// The thumbnail manager used to retrieve images paths. - /// - /// Options used to retrieve the base URL of Kyoo. - /// public SeasonApi(ILibraryManager libraryManager, IFileSystem files, - IThumbnailsManager thumbs, - IOptions options) - : base(libraryManager.SeasonRepository, files, thumbs, options.Value.PublicUrl) + IThumbnailsManager thumbs) + : base(libraryManager.SeasonRepository, files, thumbs) { _libraryManager = libraryManager; } diff --git a/src/Kyoo.Core/Views/ShowApi.cs b/src/Kyoo.Core/Views/ShowApi.cs index 42bde2ae..ec179df0 100644 --- a/src/Kyoo.Core/Views/ShowApi.cs +++ b/src/Kyoo.Core/Views/ShowApi.cs @@ -57,6 +57,12 @@ namespace Kyoo.Core.Api /// private readonly IFileSystem _files; + /// + /// The base URL of Kyoo. This will be used to create links for images and + /// . + /// + private readonly Uri _baseURL; + /// /// Create a new . /// @@ -72,10 +78,11 @@ namespace Kyoo.Core.Api IFileSystem files, IThumbnailsManager thumbs, IOptions options) - : base(libraryManager.ShowRepository, files, thumbs, options.Value.PublicUrl) + : base(libraryManager.ShowRepository, files, thumbs) { _libraryManager = libraryManager; _files = files; + _baseURL = options.Value.PublicUrl; } /// @@ -392,7 +399,7 @@ namespace Kyoo.Core.Api return (await _files.ListFiles(path)) .ToDictionary( Path.GetFileNameWithoutExtension, - x => $"{BaseURL}api/shows/{identifier}/fonts/{Path.GetFileName(x)}" + x => $"{_baseURL}api/shows/{identifier}/fonts/{Path.GetFileName(x)}" ); } diff --git a/src/Kyoo.Core/Views/StudioApi.cs b/src/Kyoo.Core/Views/StudioApi.cs index f7bd9c77..d15242f5 100644 --- a/src/Kyoo.Core/Views/StudioApi.cs +++ b/src/Kyoo.Core/Views/StudioApi.cs @@ -23,9 +23,7 @@ using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace Kyoo.Core.Api { @@ -37,8 +35,8 @@ namespace Kyoo.Core.Api { private readonly ILibraryManager _libraryManager; - public StudioApi(ILibraryManager libraryManager, IOptions options) - : base(libraryManager.StudioRepository, options.Value.PublicUrl) + public StudioApi(ILibraryManager libraryManager) + : base(libraryManager.StudioRepository) { _libraryManager = libraryManager; } diff --git a/src/Kyoo.Core/Views/TrackApi.cs b/src/Kyoo.Core/Views/TrackApi.cs index 1d2e89cb..60a5d0c2 100644 --- a/src/Kyoo.Core/Views/TrackApi.cs +++ b/src/Kyoo.Core/Views/TrackApi.cs @@ -22,22 +22,22 @@ using Kyoo.Abstractions.Models; 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 { /// /// Information about one or multiple . + /// A track contain metadata about a video, an audio or a subtitles. /// [Route("api/tracks")] [Route("api/track", Order = AlternativeRoute)] [ApiController] + [ResourceView] [PartialPermission(nameof(Track))] - [ApiDefinition("Tracks", Group = ResourcesGroup)] + [ApiDefinition("Tracks", Group = WatchGroup)] public class TrackApi : CrudApi { /// @@ -51,11 +51,8 @@ namespace Kyoo.Core.Api /// /// The library manager used to modify or retrieve information in the data store. /// - /// - /// Options used to retrieve the base URL of Kyoo. - /// - public TrackApi(ILibraryManager libraryManager, IOptions options) - : base(libraryManager.TrackRepository, options.Value.PublicUrl) + public TrackApi(ILibraryManager libraryManager) + : base(libraryManager.TrackRepository) { _libraryManager = libraryManager; } From 049f545d51c5e026c3c605c8826a674b637cca93 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 24 Sep 2021 14:17:53 +0200 Subject: [PATCH 18/36] API: documenting watch items --- .../Controllers/ILibraryManager.cs | 3 +- .../Controllers/IRepository.cs | 5 +- src/Kyoo.Abstractions/Models/WatchItem.cs | 60 +++++++++----- src/Kyoo.Core/Controllers/LibraryManager.cs | 4 +- .../Repositories/LocalRepository.cs | 7 +- src/Kyoo.Core/Views/Helper/ApiHelper.cs | 4 - .../Helper/Serializers/JsonPropertyIgnorer.cs | 2 +- .../Views/{ => Resources}/CollectionApi.cs | 0 .../Views/{ => Resources}/EpisodeApi.cs | 0 .../Views/{ => Resources}/LibraryApi.cs | 0 .../Views/{ => Resources}/LibraryItemApi.cs | 0 .../Views/{ => Resources}/SeasonApi.cs | 0 .../Views/{ => Resources}/ShowApi.cs | 0 src/Kyoo.Core/Views/{ => Watch}/TrackApi.cs | 0 src/Kyoo.Core/Views/Watch/WatchApi.cs | 83 +++++++++++++++++++ src/Kyoo.Core/Views/WatchApi.cs | 54 ------------ 16 files changed, 134 insertions(+), 88 deletions(-) rename src/Kyoo.Core/Views/{ => Resources}/CollectionApi.cs (100%) rename src/Kyoo.Core/Views/{ => Resources}/EpisodeApi.cs (100%) rename src/Kyoo.Core/Views/{ => Resources}/LibraryApi.cs (100%) rename src/Kyoo.Core/Views/{ => Resources}/LibraryItemApi.cs (100%) rename src/Kyoo.Core/Views/{ => Resources}/SeasonApi.cs (100%) rename src/Kyoo.Core/Views/{ => Resources}/ShowApi.cs (100%) rename src/Kyoo.Core/Views/{ => Watch}/TrackApi.cs (100%) create mode 100644 src/Kyoo.Core/Views/Watch/WatchApi.cs delete mode 100644 src/Kyoo.Core/Views/WatchApi.cs diff --git a/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs b/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs index f4218760..1bbe147f 100644 --- a/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs +++ b/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs @@ -200,10 +200,11 @@ namespace Kyoo.Abstractions.Controllers /// Get the resource by a filter function or null if it is not found. /// /// The filter function. + /// A custom sort method to handle cases where multiples items match the filters. /// The type of the resource /// The first resource found that match the where function [ItemCanBeNull] - Task GetOrDefault(Expression> where) + Task GetOrDefault(Expression> where, Sort sortBy = default) where T : class, IResource; /// diff --git a/src/Kyoo.Abstractions/Controllers/IRepository.cs b/src/Kyoo.Abstractions/Controllers/IRepository.cs index 21a65c12..4deae6e8 100644 --- a/src/Kyoo.Abstractions/Controllers/IRepository.cs +++ b/src/Kyoo.Abstractions/Controllers/IRepository.cs @@ -81,9 +81,10 @@ namespace Kyoo.Abstractions.Controllers /// Get the first resource that match the predicate or null if it is not found. /// /// A predicate to filter the resource. + /// A custom sort method to handle cases where multiples items match the filters. /// The resource found [ItemCanBeNull] - Task GetOrDefault(Expression> where); + Task GetOrDefault(Expression> where, Sort sortBy = default); /// /// Search for resources. @@ -263,6 +264,8 @@ namespace Kyoo.Abstractions.Controllers /// public interface IEpisodeRepository : IRepository { + // TODO replace the next methods with extension methods. + /// /// Get a episode from it's showID, it's seasonNumber and it's episode number. /// diff --git a/src/Kyoo.Abstractions/Models/WatchItem.cs b/src/Kyoo.Abstractions/Models/WatchItem.cs index 8c4daab9..5bbf0fb8 100644 --- a/src/Kyoo.Abstractions/Models/WatchItem.cs +++ b/src/Kyoo.Abstractions/Models/WatchItem.cs @@ -158,36 +158,50 @@ namespace Kyoo.Abstractions.Models /// A new WatchItem representing the given episode. public static async Task FromEpisode(Episode ep, ILibraryManager library) { - Episode previous = null; - Episode next = null; - await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Tracks); - if (!ep.Show.IsMovie && ep.SeasonNumber != null && ep.EpisodeNumber != null) + Episode previous = null; + Episode next = null; + if (!ep.Show.IsMovie) { - if (ep.EpisodeNumber > 1) - previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value - 1); - else if (ep.SeasonNumber > 1) + if (ep.AbsoluteNumber != null) { - previous = (await library.GetAll(x => x.ShowID == ep.ShowID - && x.SeasonNumber == ep.SeasonNumber.Value - 1, - limit: 1, - sort: new Sort(x => x.EpisodeNumber, true)) - ).FirstOrDefault(); + previous = await library.GetOrDefault( + x => x.ShowID == ep.ShowID && x.AbsoluteNumber <= ep.AbsoluteNumber, + new Sort(x => x.AbsoluteNumber, true) + ); + next = await library.GetOrDefault( + x => x.ShowID == ep.ShowID && x.AbsoluteNumber >= ep.AbsoluteNumber, + new Sort(x => x.AbsoluteNumber) + ); } + else if (ep.SeasonNumber != null && ep.EpisodeNumber != null) + { + previous = await library.GetOrDefault( + x => x.ShowID == ep.ShowID + && x.SeasonNumber == ep.SeasonNumber + && x.EpisodeNumber < ep.EpisodeNumber, + new Sort(x => x.EpisodeNumber, true) + ); + previous ??= await library.GetOrDefault( + x => x.ShowID == ep.ShowID + && x.SeasonNumber == ep.SeasonNumber - 1, + new Sort(x => x.EpisodeNumber, true) + ); - if (ep.EpisodeNumber >= await library.GetCount(x => x.SeasonID == ep.SeasonID)) - next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value + 1, 1); - else - next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value + 1); - } - else if (!ep.Show.IsMovie && ep.AbsoluteNumber != null) - { - previous = await library.GetOrDefault(x => x.ShowID == ep.ShowID - && x.AbsoluteNumber == ep.EpisodeNumber + 1); - next = await library.GetOrDefault(x => x.ShowID == ep.ShowID - && x.AbsoluteNumber == ep.AbsoluteNumber + 1); + next = await library.GetOrDefault( + x => x.ShowID == ep.ShowID + && x.SeasonNumber == ep.SeasonNumber + && x.EpisodeNumber > ep.EpisodeNumber, + new Sort(x => x.EpisodeNumber) + ); + next ??= await library.GetOrDefault( + x => x.ShowID == ep.ShowID + && x.SeasonNumber == ep.SeasonNumber + 1, + new Sort(x => x.EpisodeNumber) + ); + } } return new WatchItem diff --git a/src/Kyoo.Core/Controllers/LibraryManager.cs b/src/Kyoo.Core/Controllers/LibraryManager.cs index a6cd9f44..5243ee36 100644 --- a/src/Kyoo.Core/Controllers/LibraryManager.cs +++ b/src/Kyoo.Core/Controllers/LibraryManager.cs @@ -163,10 +163,10 @@ namespace Kyoo.Core.Controllers } /// - public async Task GetOrDefault(Expression> where) + public async Task GetOrDefault(Expression> where, Sort sortBy) where T : class, IResource { - return await GetRepository().GetOrDefault(where); + return await GetRepository().GetOrDefault(where, sortBy); } /// diff --git a/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index 3cc80036..59928d02 100644 --- a/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -115,9 +115,12 @@ namespace Kyoo.Core.Controllers } /// - public virtual Task GetOrDefault(Expression> where) + public virtual Task GetOrDefault(Expression> where, Sort sortBy = default) { - return Database.Set().FirstOrDefaultAsync(where); + IQueryable query = Database.Set(); + Expression> sortKey = sortBy.Key ?? DefaultSort; + query = sortBy.Descendant ? query.OrderByDescending(sortKey) : query.OrderBy(sortKey); + return query.FirstOrDefaultAsync(where); } /// diff --git a/src/Kyoo.Core/Views/Helper/ApiHelper.cs b/src/Kyoo.Core/Views/Helper/ApiHelper.cs index d2e926fa..7365c5a7 100644 --- a/src/Kyoo.Core/Views/Helper/ApiHelper.cs +++ b/src/Kyoo.Core/Views/Helper/ApiHelper.cs @@ -23,10 +23,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; using Kyoo.Abstractions.Models; -using Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; namespace Kyoo.Core.Api { diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs index 65c00f7f..40a682a9 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs @@ -29,8 +29,8 @@ namespace Kyoo.Core.Api { public class JsonPropertyIgnorer : CamelCasePropertyNamesContractResolver { - private int _depth = -1; private readonly Uri _host; + private int _depth = -1; public JsonPropertyIgnorer(Uri host) { diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/Resources/CollectionApi.cs similarity index 100% rename from src/Kyoo.Core/Views/CollectionApi.cs rename to src/Kyoo.Core/Views/Resources/CollectionApi.cs diff --git a/src/Kyoo.Core/Views/EpisodeApi.cs b/src/Kyoo.Core/Views/Resources/EpisodeApi.cs similarity index 100% rename from src/Kyoo.Core/Views/EpisodeApi.cs rename to src/Kyoo.Core/Views/Resources/EpisodeApi.cs diff --git a/src/Kyoo.Core/Views/LibraryApi.cs b/src/Kyoo.Core/Views/Resources/LibraryApi.cs similarity index 100% rename from src/Kyoo.Core/Views/LibraryApi.cs rename to src/Kyoo.Core/Views/Resources/LibraryApi.cs diff --git a/src/Kyoo.Core/Views/LibraryItemApi.cs b/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs similarity index 100% rename from src/Kyoo.Core/Views/LibraryItemApi.cs rename to src/Kyoo.Core/Views/Resources/LibraryItemApi.cs diff --git a/src/Kyoo.Core/Views/SeasonApi.cs b/src/Kyoo.Core/Views/Resources/SeasonApi.cs similarity index 100% rename from src/Kyoo.Core/Views/SeasonApi.cs rename to src/Kyoo.Core/Views/Resources/SeasonApi.cs diff --git a/src/Kyoo.Core/Views/ShowApi.cs b/src/Kyoo.Core/Views/Resources/ShowApi.cs similarity index 100% rename from src/Kyoo.Core/Views/ShowApi.cs rename to src/Kyoo.Core/Views/Resources/ShowApi.cs diff --git a/src/Kyoo.Core/Views/TrackApi.cs b/src/Kyoo.Core/Views/Watch/TrackApi.cs similarity index 100% rename from src/Kyoo.Core/Views/TrackApi.cs rename to src/Kyoo.Core/Views/Watch/TrackApi.cs diff --git a/src/Kyoo.Core/Views/Watch/WatchApi.cs b/src/Kyoo.Core/Views/Watch/WatchApi.cs new file mode 100644 index 00000000..f72b1901 --- /dev/null +++ b/src/Kyoo.Core/Views/Watch/WatchApi.cs @@ -0,0 +1,83 @@ +// 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.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 +{ + /// + /// Retrieve information of an as a . + /// A watch item is another representation of an episode in a form easier to read and display for playback. + /// It contains streams (video, audio, subtitles) information, chapters, next and previous episodes and a bit of + /// information of the show. + /// + [Route("api/watch")] + [Route("api/watchitem", Order = AlternativeRoute)] + [ApiController] + [ApiDefinition("Watch Items", Group = WatchGroup)] + public class WatchApi : ControllerBase + { + /// + /// The library manager used to modify or retrieve information in the data store. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// + public WatchApi(ILibraryManager libraryManager) + { + _libraryManager = libraryManager; + } + + /// + /// Get a watch item + /// + /// + /// Retrieve a watch item of an episode. + /// + /// The ID or slug of the . + /// A page of items. + /// No episode with the given ID or slug could be found. + [HttpGet("{identifier:id}")] + [Permission("watch", Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetWatchItem(Identifier identifier) + { + Episode item = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => _libraryManager.GetOrDefault(slug) + ); + if (item == null) + return NotFound(); + return await WatchItem.FromEpisode(item, _libraryManager); + } + } +} diff --git a/src/Kyoo.Core/Views/WatchApi.cs b/src/Kyoo.Core/Views/WatchApi.cs deleted file mode 100644 index 74dba487..00000000 --- a/src/Kyoo.Core/Views/WatchApi.cs +++ /dev/null @@ -1,54 +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.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; -using Kyoo.Abstractions.Models.Permissions; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Core.Api -{ - [Route("api/watch")] - [ApiController] - public class WatchApi : ControllerBase - { - private readonly ILibraryManager _libraryManager; - - public WatchApi(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - - [HttpGet("{slug}")] - [Permission("video", Kind.Read)] - public async Task> GetWatchItem(string slug) - { - try - { - Episode item = await _libraryManager.Get(slug); - return await WatchItem.FromEpisode(item, _libraryManager); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - } -} From 0fdc583d582b489a2249d9491e241fdb92f64de6 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 26 Sep 2021 18:20:46 +0200 Subject: [PATCH 19/36] Swagger: sorting tags groups & documenting the genre's API --- .../Attributes/ApiDefinitionAttribute.cs | 4 +- .../Models/Utils/Constants.cs | 10 +- src/Kyoo.Core/Views/GenreApi.cs | 96 -------------- src/Kyoo.Core/Views/Metadata/GenreApi.cs | 105 ++++++++++++++++ src/Kyoo.Swagger/ApiSorter.cs | 64 ++++++++++ src/Kyoo.Swagger/ApiTagsFilter.cs | 119 ++++++++++++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 68 +--------- 7 files changed, 301 insertions(+), 165 deletions(-) delete mode 100644 src/Kyoo.Core/Views/GenreApi.cs create mode 100644 src/Kyoo.Core/Views/Metadata/GenreApi.cs create mode 100644 src/Kyoo.Swagger/ApiSorter.cs create mode 100644 src/Kyoo.Swagger/ApiTagsFilter.cs 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; From f4bac4e6704f9602e4afd95ed2b11879dfad7bc0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 26 Sep 2021 19:08:00 +0200 Subject: [PATCH 20/36] API: Documenting the studios api --- src/Kyoo.Core/Views/StudioApi.cs | 81 ++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/src/Kyoo.Core/Views/StudioApi.cs b/src/Kyoo.Core/Views/StudioApi.cs index d15242f5..ae138be3 100644 --- a/src/Kyoo.Core/Views/StudioApi.cs +++ b/src/Kyoo.Core/Views/StudioApi.cs @@ -22,74 +22,83 @@ 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 { - [Route("api/studio")] + /// + /// Information about one or multiple . + /// [Route("api/studios")] + [Route("api/studio", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(ShowApi))] + [ApiDefinition("Studios", Group = MetadataGroup)] public class StudioApi : CrudApi { + /// + /// The library manager used to modify or retrieve information in the data store. + /// private readonly ILibraryManager _libraryManager; + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// public StudioApi(ILibraryManager libraryManager) : base(libraryManager.StudioRepository) { _libraryManager = libraryManager; } - [HttpGet("{id:int}/show")] - [HttpGet("{id:int}/shows")] + /// + /// Get shows + /// + /// + /// List shows that were made by this specific studio. + /// + /// 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 studio with the given ID or slug could be found. + [HttpGet("{identifier:id}/shows")] + [HttpGet("{identifier:id}/show", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task>> GetShows(int id, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetShows(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.StudioID == id), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.StudioID, x => x.Studio.Slug)), new Sort(sortBy), - new Pagination(limit, afterID)); + 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}/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.Studio.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 }); + return BadRequest(new RequestError(ex.Message)); } } } From cd6c5895291de5165240b2b3ba24007e39ea3097 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 27 Sep 2021 20:50:57 +0200 Subject: [PATCH 21/36] API: Documenting the staff's api --- .../Controllers/ILibraryManager.cs | 12 ++ .../Controllers/IRepository.cs | 12 ++ .../Repositories/PeopleRepository.cs | 15 ++- src/Kyoo.Core/Views/Metadata/StaffApi.cs | 116 +++++++++++++++++ src/Kyoo.Core/Views/PeopleApi.cs | 123 ------------------ src/Kyoo.Core/Views/Resources/LibraryApi.cs | 7 +- src/Kyoo.Core/Views/Resources/ShowApi.cs | 14 +- 7 files changed, 166 insertions(+), 133 deletions(-) create mode 100644 src/Kyoo.Core/Views/Metadata/StaffApi.cs delete mode 100644 src/Kyoo.Core/Views/PeopleApi.cs diff --git a/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs b/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs index 1bbe147f..3b1a1b7f 100644 --- a/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs +++ b/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs @@ -318,6 +318,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No library exist with the given ID. /// A list of items that match every filters Task> GetItemsFromLibrary(int id, Expression> where = null, @@ -331,6 +332,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No library exist with the given ID. /// A list of items that match every filters Task> GetItemsFromLibrary(int id, [Optional] Expression> where, @@ -345,6 +347,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No library exist with the given slug. /// A list of items that match every filters Task> GetItemsFromLibrary(string slug, Expression> where = null, @@ -358,6 +361,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No library exist with the given slug. /// A list of items that match every filters Task> GetItemsFromLibrary(string slug, [Optional] Expression> where, @@ -372,6 +376,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetPeopleFromShow(int showID, Expression> where = null, @@ -385,6 +390,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetPeopleFromShow(int showID, [Optional] Expression> where, @@ -399,6 +405,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetPeopleFromShow(string showSlug, Expression> where = null, @@ -412,6 +419,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetPeopleFromShow(string showSlug, [Optional] Expression> where, @@ -426,6 +434,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetRolesFromPeople(int id, Expression> where = null, @@ -439,6 +448,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetRolesFromPeople(int id, [Optional] Expression> where, @@ -453,6 +463,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetRolesFromPeople(string slug, Expression> where = null, @@ -466,6 +477,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetRolesFromPeople(string slug, [Optional] Expression> where, diff --git a/src/Kyoo.Abstractions/Controllers/IRepository.cs b/src/Kyoo.Abstractions/Controllers/IRepository.cs index 4deae6e8..1d996e97 100644 --- a/src/Kyoo.Abstractions/Controllers/IRepository.cs +++ b/src/Kyoo.Abstractions/Controllers/IRepository.cs @@ -345,6 +345,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No library exist with the given ID. /// A list of items that match every filters public Task> GetFromLibrary(int id, Expression> where = null, @@ -358,6 +359,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No library exist with the given ID. /// A list of items that match every filters public Task> GetFromLibrary(int id, [Optional] Expression> where, @@ -372,6 +374,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No library exist with the given slug. /// A list of items that match every filters public Task> GetFromLibrary(string slug, Expression> where = null, @@ -385,6 +388,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No library exist with the given slug. /// A list of items that match every filters public Task> GetFromLibrary(string slug, [Optional] Expression> where, @@ -420,6 +424,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetFromShow(int showID, Expression> where = null, @@ -433,6 +438,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetFromShow(int showID, [Optional] Expression> where, @@ -447,6 +453,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetFromShow(string showSlug, Expression> where = null, @@ -460,6 +467,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetFromShow(string showSlug, [Optional] Expression> where, @@ -474,6 +482,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetFromPeople(int id, Expression> where = null, @@ -487,6 +496,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given ID. /// A list of items that match every filters Task> GetFromPeople(int id, [Optional] Expression> where, @@ -501,6 +511,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// Sort information (sort order and sort by) /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetFromPeople(string slug, Expression> where = null, @@ -514,6 +525,7 @@ namespace Kyoo.Abstractions.Controllers /// A filter function /// A sort by method /// How many items to return and where to start + /// No exist with the given slug. /// A list of items that match every filters Task> GetFromPeople(string slug, [Optional] Expression> where, diff --git a/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs b/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs index d599d6ee..09809426 100644 --- a/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs +++ b/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs @@ -23,6 +23,7 @@ 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; @@ -159,6 +160,8 @@ 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; @@ -179,6 +182,8 @@ 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; @@ -190,7 +195,7 @@ namespace Kyoo.Core.Controllers Sort sort = default, Pagination limit = default) { - return await ApplyFilters(_database.PeopleRoles + ICollection roles = await ApplyFilters(_database.PeopleRoles .Where(x => x.PeopleID == id) .Include(x => x.Show), y => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == y), @@ -198,6 +203,9 @@ namespace Kyoo.Core.Controllers where, sort, limit); + if (!roles.Any() && await GetOrDefault(id) == null) + throw new ItemNotFoundException(); + return roles; } /// @@ -206,7 +214,7 @@ namespace Kyoo.Core.Controllers Sort sort = default, Pagination limit = default) { - return await ApplyFilters(_database.PeopleRoles + ICollection roles = await ApplyFilters(_database.PeopleRoles .Where(x => x.People.Slug == slug) .Include(x => x.Show), id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), @@ -214,6 +222,9 @@ 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/Metadata/StaffApi.cs b/src/Kyoo.Core/Views/Metadata/StaffApi.cs new file mode 100644 index 00000000..fc20365a --- /dev/null +++ b/src/Kyoo.Core/Views/Metadata/StaffApi.cs @@ -0,0 +1,116 @@ +// 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.Expressions; +using System.Threading.Tasks; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Exceptions; +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 staff member. + /// + [Route("api/staff")] + [Route("api/people", Order = AlternativeRoute)] + [ApiController] + [ResourceView] + [PartialPermission(nameof(StaffApi))] + [ApiDefinition("Staff", Group = MetadataGroup)] + public class StaffApi : CrudThumbsApi + { + /// + /// The library manager used to modify or retrieve information in the data store. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// 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. + public StaffApi(ILibraryManager libraryManager, + IFileSystem files, + IThumbnailsManager thumbs) + : base(libraryManager.PeopleRepository, files, thumbs) + { + _libraryManager = libraryManager; + } + + /// + /// Get roles + /// + /// + /// List the roles in witch this person has played, written or worked in a way. + /// + /// The ID or slug of the person. + /// A key to sort roles by. + /// An optional list of filters. + /// The number of roles to return. + /// An optional role's ID to start the query from this specific item. + /// A page of roles. + /// The filters or the sort parameters are invalid. + /// No person with the given ID or slug could be found. + [HttpGet("{identifier:id}/roles")] + [HttpGet("{identifier:id}/role", Order = AlternativeRoute)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetRoles(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20, + [FromQuery] int? afterID = null) + { + try + { + Expression> whereQuery = ApiHelper.ParseWhere(where); + Sort sort = new(sortBy); + Pagination pagination = new(limit, afterID); + + ICollection resources = await identifier.Match( + id => _libraryManager.GetRolesFromPeople(id, whereQuery, sort, pagination), + slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sort, pagination) + ); + + return Page(resources, limit); + } + catch (ItemNotFoundException) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + } +} diff --git a/src/Kyoo.Core/Views/PeopleApi.cs b/src/Kyoo.Core/Views/PeopleApi.cs deleted file mode 100644 index 2246d2ad..00000000 --- a/src/Kyoo.Core/Views/PeopleApi.cs +++ /dev/null @@ -1,123 +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.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; -using Kyoo.Abstractions.Models.Permissions; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Core.Api -{ - [Route("api/people")] - [ApiController] - [PartialPermission(nameof(PeopleApi))] - public class PeopleApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _files; - private readonly IThumbnailsManager _thumbs; - - public PeopleApi(ILibraryManager libraryManager, - IFileSystem files, - IThumbnailsManager thumbs) - : base(libraryManager.PeopleRepository) - { - _libraryManager = libraryManager; - _files = files; - _thumbs = thumbs; - } - - [HttpGet("{id:int}/role")] - [HttpGet("{id:int}/roles")] - [PartialPermission(Kind.Read)] - public async Task>> GetRoles(int id, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 20) - { - try - { - ICollection resources = await _libraryManager.GetRolesFromPeople(id, - ApiHelper.ParseWhere(where), - new Sort(sortBy), - new Pagination(limit, afterID)); - - return Page(resources, limit); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{slug}/role")] - [HttpGet("{slug}/roles")] - [PartialPermission(Kind.Read)] - public async Task>> GetRoles(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 20) - { - try - { - ICollection resources = await _libraryManager.GetRolesFromPeople(slug, - ApiHelper.ParseWhere(where), - new Sort(sortBy), - new Pagination(limit, afterID)); - - return Page(resources, limit); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{id:int}/poster")] - public async Task GetPeopleIcon(int id) - { - People people = await _libraryManager.GetOrDefault(id); - if (people == null) - return NotFound(); - return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster)); - } - - [HttpGet("{slug}/poster")] - public async Task GetPeopleIcon(string slug) - { - People people = await _libraryManager.GetOrDefault(slug); - if (people == null) - return NotFound(); - return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster)); - } - } -} diff --git a/src/Kyoo.Core/Views/Resources/LibraryApi.cs b/src/Kyoo.Core/Views/Resources/LibraryApi.cs index 045b7b70..d440fae3 100644 --- a/src/Kyoo.Core/Views/Resources/LibraryApi.cs +++ b/src/Kyoo.Core/Views/Resources/LibraryApi.cs @@ -24,6 +24,7 @@ using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Microsoft.AspNetCore.Http; @@ -188,10 +189,12 @@ namespace Kyoo.Core.Api slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination) ); - if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) - return NotFound(); return Page(resources, limit); } + catch (ItemNotFoundException) + { + return NotFound(); + } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); diff --git a/src/Kyoo.Core/Views/Resources/ShowApi.cs b/src/Kyoo.Core/Views/Resources/ShowApi.cs index ec179df0..37f3ccd1 100644 --- a/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -25,6 +25,7 @@ using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Core.Models.Options; @@ -174,7 +175,7 @@ namespace Kyoo.Core.Api } /// - /// Get people that made this show + /// Get staff /// /// /// List staff members that made this show. @@ -187,8 +188,8 @@ namespace Kyoo.Core.Api /// 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)] + [HttpGet("{identifier:id}/staff")] + [HttpGet("{identifier:id}/people", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] @@ -209,11 +210,12 @@ namespace Kyoo.Core.Api 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 (ItemNotFoundException) + { + return NotFound(); + } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); From 19023fbaf5b66699cbc16e70c651da5b0caedbdf Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 27 Sep 2021 21:08:03 +0200 Subject: [PATCH 22/36] API: Documenting the providers API --- .../Views/{ => Metadata}/ProviderApi.cs | 52 ++++++++----------- .../Views/{ => Metadata}/StudioApi.cs | 0 2 files changed, 21 insertions(+), 31 deletions(-) rename src/Kyoo.Core/Views/{ => Metadata}/ProviderApi.cs (54%) rename src/Kyoo.Core/Views/{ => Metadata}/StudioApi.cs (100%) diff --git a/src/Kyoo.Core/Views/ProviderApi.cs b/src/Kyoo.Core/Views/Metadata/ProviderApi.cs similarity index 54% rename from src/Kyoo.Core/Views/ProviderApi.cs rename to src/Kyoo.Core/Views/Metadata/ProviderApi.cs index 0ba3244a..1150214d 100644 --- a/src/Kyoo.Core/Views/ProviderApi.cs +++ b/src/Kyoo.Core/Views/Metadata/ProviderApi.cs @@ -16,50 +16,40 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -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.Mvc; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { - [Route("api/provider")] + /// + /// Information about one or multiple . + /// Providers are links to external websites or database. + /// They are mostly linked to plugins that provide metadata from those websites. + /// [Route("api/providers")] + [Route("api/provider", Order = AlternativeRoute)] [ApiController] + [ResourceView] [PartialPermission(nameof(ProviderApi))] - public class ProviderApi : CrudApi + [ApiDefinition("Providers", Group = MetadataGroup)] + public class ProviderApi : CrudThumbsApi { - private readonly IThumbnailsManager _thumbnails; - private readonly ILibraryManager _libraryManager; - 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. public ProviderApi(ILibraryManager libraryManager, IFileSystem files, IThumbnailsManager thumbnails) - : base(libraryManager.ProviderRepository) - { - _libraryManager = libraryManager; - _files = files; - _thumbnails = thumbnails; - } - - [HttpGet("{id:int}/logo")] - public async Task GetLogo(int id) - { - Provider provider = await _libraryManager.GetOrDefault(id); - if (provider == null) - return NotFound(); - return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo)); - } - - [HttpGet("{slug}/logo")] - public async Task GetLogo(string slug) - { - Provider provider = await _libraryManager.GetOrDefault(slug); - if (provider == null) - return NotFound(); - return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo)); - } + : base(libraryManager.ProviderRepository, files, thumbnails) + { } } } diff --git a/src/Kyoo.Core/Views/StudioApi.cs b/src/Kyoo.Core/Views/Metadata/StudioApi.cs similarity index 100% rename from src/Kyoo.Core/Views/StudioApi.cs rename to src/Kyoo.Core/Views/Metadata/StudioApi.cs From 24a45c0b72364b601210be3751ac9caf18a49fcc Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 29 Sep 2021 16:22:53 +0200 Subject: [PATCH 23/36] API: Doucmenting subtitles's API --- src/Kyoo.Core/Views/SubtitleApi.cs | 121 +++++++++++++++++++---------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/src/Kyoo.Core/Views/SubtitleApi.cs b/src/Kyoo.Core/Views/SubtitleApi.cs index 784a9022..41b25063 100644 --- a/src/Kyoo.Core/Views/SubtitleApi.cs +++ b/src/Kyoo.Core/Views/SubtitleApi.cs @@ -22,39 +22,79 @@ 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 { - [Route("subtitle")] + /// + /// An endpoint to retrieve subtitles for a specific episode. + /// + [Route("subtitles")] + [Route("subtitle", Order = AlternativeRoute)] + [PartialPermission(nameof(SubtitleApi))] [ApiController] + [ApiDefinition("Subtitles", Group = WatchGroup)] public class SubtitleApi : ControllerBase { + /// + /// The library manager used to modify or retrieve information about the data store. + /// private readonly ILibraryManager _libraryManager; + + /// + /// The file manager used to send subtitles files. + /// private readonly IFileSystem _files; + /// + /// Create a new . + /// + /// The library manager used to interact with the data store. + /// The file manager used to send subtitle files. public SubtitleApi(ILibraryManager libraryManager, IFileSystem files) { _libraryManager = libraryManager; _files = files; } - [HttpGet("{id:int}")] - [Permission(nameof(SubtitleApi), Kind.Read)] - public async Task GetSubtitle(int id) + /// + /// Get subtitle + /// + /// + /// Get the subtitle file with the given identifier. + /// The extension is optional and can be used to ask Kyoo to convert the subtitle file on the fly. + /// + /// + /// The ID or slug of the subtitle (the same as the corresponding ). + /// + /// An optional extension for the subtitle file. + /// The subtitle file + /// No subtitle exist with the given ID or slug. + [HttpGet("{identifier:id}", Order = AlternativeRoute)] + [HttpGet("{identifier:id}.{extension}")] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetSubtitle(Identifier identifier, string extension) { - Track subtitle = await _libraryManager.GetOrDefault(id); - return subtitle != null - ? _files.FileResult(subtitle.Path) - : NotFound(); - } + Track subtitle = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => + { + if (slug.Count(x => x == '.') == 3) + { + int idx = slug.LastIndexOf('.'); + extension = slug[(idx + 1)..]; + slug = slug[..idx]; + } + return _libraryManager.GetOrDefault(Track.BuildSlug(slug, StreamType.Subtitle)); + }); - [HttpGet("{id:int}.{extension}")] - [Permission(nameof(SubtitleApi), Kind.Read)] - public async Task GetSubtitle(int id, string extension) - { - Track subtitle = await _libraryManager.GetOrDefault(id); if (subtitle == null) return NotFound(); if (subtitle.Codec == "subrip" && extension == "vtt") @@ -62,38 +102,37 @@ namespace Kyoo.Core.Api return _files.FileResult(subtitle.Path); } - [HttpGet("{slug}")] - [Permission(nameof(SubtitleApi), Kind.Read)] - public async Task GetSubtitle(string slug) - { - string extension = null; - - if (slug.Count(x => x == '.') == 3) - { - int idx = slug.LastIndexOf('.'); - extension = slug[(idx + 1)..]; - slug = slug[..idx]; - } - - Track subtitle = await _libraryManager.GetOrDefault(Track.BuildSlug(slug, StreamType.Subtitle)); - if (subtitle == null) - return NotFound(); - if (subtitle.Codec == "subrip" && extension == "vtt") - return new ConvertSubripToVtt(subtitle.Path, _files); - return _files.FileResult(subtitle.Path); - } - - public class ConvertSubripToVtt : IActionResult + /// + /// An action result that convert a subrip subtitle to vtt. + /// + private class ConvertSubripToVtt : IActionResult { + /// + /// The path of the file to convert. It can be any path supported by a . + /// private readonly string _path; + + /// + /// The file system used to manipulate the given file. + /// private readonly IFileSystem _files; + /// + /// Create a new . + /// + /// + /// The path of the subtitle file. It can be any path supported by the given . + /// + /// + /// The file system used to interact with the file at the given . + /// public ConvertSubripToVtt(string subtitlePath, IFileSystem files) { _path = subtitlePath; _files = files; } + /// public async Task ExecuteResultAsync(ActionContext context) { List lines = new(); @@ -127,7 +166,12 @@ namespace Kyoo.Core.Api await context.HttpContext.Response.Body.FlushAsync(); } - private static IEnumerable _ConvertBlock(IList lines) + /// + /// Convert a block from subrip to vtt. + /// + /// All the lines in the block. + /// The given block, converted to vtt. + private static IList _ConvertBlock(IList lines) { if (lines.Count < 3) return lines; @@ -150,8 +194,7 @@ namespace Kyoo.Core.Api } if (lines[2].StartsWith("{\\an")) - lines[2] = lines[2].Substring(6); - + lines[2] = lines[2][6..]; return lines; } } From 9fded9e4a2dee877b9791a0932339c74bbd9e11a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 29 Sep 2021 16:39:09 +0200 Subject: [PATCH 24/36] API: Documenting the task's API --- .../Models/Utils/Constants.cs | 5 + src/Kyoo.Core/Views/Admin/TaskApi.cs | 108 ++++++++++++++++++ src/Kyoo.Core/Views/TaskApi.cs | 67 ----------- .../Views/{ => Watch}/SubtitleApi.cs | 0 4 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 src/Kyoo.Core/Views/Admin/TaskApi.cs delete mode 100644 src/Kyoo.Core/Views/TaskApi.cs rename src/Kyoo.Core/Views/{ => Watch}/SubtitleApi.cs (100%) diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 985f0254..7f857df1 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -46,5 +46,10 @@ namespace Kyoo.Abstractions.Models.Utils /// A group name for . It should be used for endpoints useful for playback. /// public const string WatchGroup = "2:Watch"; + + /// + /// A group name for . It should be used for endpoints used by admins. + /// + public const string AdminGroup = "3:Admin"; } } diff --git a/src/Kyoo.Core/Views/Admin/TaskApi.cs b/src/Kyoo.Core/Views/Admin/TaskApi.cs new file mode 100644 index 00000000..310cad81 --- /dev/null +++ b/src/Kyoo.Core/Views/Admin/TaskApi.cs @@ -0,0 +1,108 @@ +// 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 Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Exceptions; +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 +{ + /// + /// An endpoint to list and run tasks in the background. + /// + [Route("api/tasks")] + [Route("api/task", Order = AlternativeRoute)] + [ApiController] + [ResourceView] + [PartialPermission(nameof(TaskApi), Group = Group.Admin)] + [ApiDefinition("Tasks", Group = AdminGroup)] + public class TaskApi : ControllerBase + { + /// + /// The task manager used to retrieve and start tasks. + /// + private readonly ITaskManager _taskManager; + + /// + /// Create a new . + /// + /// The task manager used to start tasks. + public TaskApi(ITaskManager taskManager) + { + _taskManager = taskManager; + } + + /// + /// Get all tasks + /// + /// + /// Retrieve all tasks available in this instance of Kyoo. + /// + /// A list of every tasks that this instance know. + [HttpGet] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetTasks() + { + return Ok(_taskManager.GetAllTasks()); + } + + /// + /// Start task + /// + /// + /// Start a task with the given arguments. If a task is already running, it may be queued and started only when + /// a runner become available. + /// + /// The slug of the task to start. + /// The list of arguments to give to the task. + /// The task has been started or is queued. + /// The task misses an argument or an argument is invalid. + /// No task could be found with the given slug. + [HttpPut("{taskSlug}")] + [HttpGet("{taskSlug}", Order = AlternativeRoute)] + [PartialPermission(Kind.Create)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public IActionResult RunTask(string taskSlug, + [FromQuery] Dictionary args) + { + try + { + _taskManager.StartTask(taskSlug, new Progress(), args); + return Ok(); + } + catch (ItemNotFoundException) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + } +} diff --git a/src/Kyoo.Core/Views/TaskApi.cs b/src/Kyoo.Core/Views/TaskApi.cs deleted file mode 100644 index d7b91427..00000000 --- a/src/Kyoo.Core/Views/TaskApi.cs +++ /dev/null @@ -1,67 +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 Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models.Exceptions; -using Kyoo.Abstractions.Models.Permissions; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Core.Api -{ - [Route("api/task")] - [Route("api/tasks")] - [ApiController] - public class TaskApi : ControllerBase - { - private readonly ITaskManager _taskManager; - - public TaskApi(ITaskManager taskManager) - { - _taskManager = taskManager; - } - - [HttpGet] - [Permission(nameof(TaskApi), Kind.Read)] - public ActionResult> GetTasks() - { - return Ok(_taskManager.GetAllTasks()); - } - - [HttpGet("{taskSlug}")] - [HttpPut("{taskSlug}")] - [Permission(nameof(TaskApi), Kind.Create)] - public IActionResult RunTask(string taskSlug, [FromQuery] Dictionary args) - { - try - { - _taskManager.StartTask(taskSlug, new Progress(), args); - return Ok(); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - } -} diff --git a/src/Kyoo.Core/Views/SubtitleApi.cs b/src/Kyoo.Core/Views/Watch/SubtitleApi.cs similarity index 100% rename from src/Kyoo.Core/Views/SubtitleApi.cs rename to src/Kyoo.Core/Views/Watch/SubtitleApi.cs From 479173601928d25e17f6bacf453c2f555bfa9c7f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 29 Sep 2021 17:10:49 +0200 Subject: [PATCH 25/36] Swagger: Creating a basic security definition --- src/Kyoo.Swagger/SwaggerModule.cs | 48 ++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 4cbff4a1..1bf05ec3 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -52,41 +52,61 @@ namespace Kyoo.Swagger public void Configure(IServiceCollection services) { services.AddTransient(); - services.AddOpenApiDocument(options => + services.AddOpenApiDocument(document => { - options.Title = "Kyoo API"; + document.Title = "Kyoo API"; // TODO use a real multi-line description in markdown. - options.Description = "The Kyoo's public API"; - options.Version = "1.0.0"; - options.DocumentName = "v1"; - options.UseControllerSummaryAsTagDescription = true; - options.GenerateExamples = true; - options.PostProcess = postProcess => + document.Description = "The Kyoo's public API"; + document.Version = "1.0.0"; + document.DocumentName = "v1"; + document.UseControllerSummaryAsTagDescription = true; + document.GenerateExamples = true; + document.PostProcess = options => { - postProcess.Info.Contact = new OpenApiContact + options.Info.Contact = new OpenApiContact { Name = "Kyoo's github", Url = "https://github.com/AnonymusRaccoon/Kyoo" }; - postProcess.Info.License = new OpenApiLicense + options.Info.License = new OpenApiLicense { Name = "GPL-3.0-or-later", Url = "https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE" }; }; - options.UseApiTags(); - options.SortApis(); - options.AddOperationFilter(x => + document.UseApiTags(); + document.SortApis(); + document.AddOperationFilter(x => { if (x is AspNetCoreOperationProcessorContext ctx) return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute; return true; }); - options.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x => + document.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x => { x.IsNullableRaw = false; x.Type = JsonObjectType.String | JsonObjectType.Integer; })); + + document.AddSecurity("Kyoo", new OpenApiSecurityScheme() + { + Type = OpenApiSecuritySchemeType.OpenIdConnect, + Description = "Kyoo's OpenID Authentication", + Flow = OpenApiOAuth2Flow.AccessCode, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + Scopes = new Dictionary + { + { "read", "Read access to protected resources" }, + { "write", "Write access to protected resources" } + }, + AuthorizationUrl = "https://localhost:44333/core/connect/authorize", + TokenUrl = "https://localhost:44333/core/connect/token" + }, + } + }); }); } From cb6ea80adb9675cf752655a407e1f5e18281ec13 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 29 Sep 2021 21:24:56 +0200 Subject: [PATCH 26/36] Swagger: Handling PermissionsAttribute for the swagger's document --- .../OperationPermissionProcessor.cs | 80 +++++++++++++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 19 ++++- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/Kyoo.Swagger/OperationPermissionProcessor.cs diff --git a/src/Kyoo.Swagger/OperationPermissionProcessor.cs b/src/Kyoo.Swagger/OperationPermissionProcessor.cs new file mode 100644 index 00000000..b0511b24 --- /dev/null +++ b/src/Kyoo.Swagger/OperationPermissionProcessor.cs @@ -0,0 +1,80 @@ +// 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.Permissions; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + +namespace Kyoo.Swagger +{ + /// + /// An operation processor that adds permissions information from the and the + /// . + /// + public class OperationPermissionProcessor : IOperationProcessor + { + /// + public bool Process(OperationProcessorContext context) + { + context.OperationDescription.Operation.Security ??= new List(); + OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes() + .Aggregate(new OpenApiSecurityRequirement(), (agg, cur) => + { + ICollection permissions = _GetPermissionsList(agg, cur.Group); + permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}"); + agg[cur.Group.ToString()] = permissions; + return agg; + }); + + PartialPermissionAttribute controller = context.ControllerType + .GetCustomAttribute(); + if (controller != null) + { + perms = context.MethodInfo.GetCustomAttributes() + .Aggregate(perms, (agg, cur) => + { + Group group = controller.Group != Group.Overall + ? controller.Group + : cur.Group; + string type = controller.Type ?? cur.Type; + Kind kind = controller.Type == null + ? controller.Kind + : cur.Kind; + ICollection permissions = _GetPermissionsList(agg, group); + permissions.Add($"{type}.{kind.ToString().ToLower()}"); + agg[group.ToString()] = permissions; + return agg; + }); + } + + context.OperationDescription.Operation.Security.Add(perms); + return true; + } + + private ICollection _GetPermissionsList(OpenApiSecurityRequirement security, Group group) + { + return security.TryGetValue(group.ToString(), out IEnumerable perms) + ? perms.ToList() + : new List(); + } + } +} diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 1bf05ec3..1d8e56e1 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -27,6 +28,7 @@ using NJsonSchema; using NJsonSchema.Generation.TypeMappers; using NSwag; using NSwag.Generation.AspNetCore; +using NSwag.Generation.Processors.Security; using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Swagger @@ -104,9 +106,24 @@ namespace Kyoo.Swagger }, AuthorizationUrl = "https://localhost:44333/core/connect/authorize", TokenUrl = "https://localhost:44333/core/connect/token" - }, + } } }); + document.OperationProcessors.Add(new OperationPermissionProcessor()); + document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Overall.ToString(), new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate." + })); + document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Admin.ToString(), new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate." + })); }); } From 40e32a16899ad4d83a8c9e5458d14aa245faa565 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 2 Oct 2021 18:31:25 +0200 Subject: [PATCH 27/36] API: Documenting the search API --- .../Attributes/ApiDefinitionAttribute.cs | 4 +- .../Permission/PartialPermissionAttribute.cs | 7 +- .../Permission/PermissionAttribute.cs | 7 +- src/Kyoo.Core/Views/Admin/TaskApi.cs | 2 +- src/Kyoo.Core/Views/Metadata/GenreApi.cs | 2 +- src/Kyoo.Core/Views/Metadata/ProviderApi.cs | 2 +- src/Kyoo.Core/Views/Metadata/StaffApi.cs | 2 +- src/Kyoo.Core/Views/Metadata/StudioApi.cs | 2 +- .../Views/Resources/CollectionApi.cs | 2 +- src/Kyoo.Core/Views/Resources/EpisodeApi.cs | 2 +- src/Kyoo.Core/Views/Resources/LibraryApi.cs | 2 +- .../Views/Resources/LibraryItemApi.cs | 3 +- src/Kyoo.Core/Views/Resources/SearchApi.cs | 194 ++++++++++++++++++ src/Kyoo.Core/Views/Resources/SeasonApi.cs | 2 +- src/Kyoo.Core/Views/Resources/ShowApi.cs | 2 +- src/Kyoo.Core/Views/SearchApi.cs | 107 ---------- src/Kyoo.Swagger/ApiSorter.cs | 14 +- src/Kyoo.Swagger/ApiTagsFilter.cs | 31 +-- src/Kyoo.Swagger/Models/TagGroups.cs | 42 ++++ 19 files changed, 282 insertions(+), 147 deletions(-) create mode 100644 src/Kyoo.Core/Views/Resources/SearchApi.cs delete mode 100644 src/Kyoo.Core/Views/SearchApi.cs create mode 100644 src/Kyoo.Swagger/Models/TagGroups.cs diff --git a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs index b8923eef..cd946714 100644 --- a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs +++ b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs @@ -23,8 +23,10 @@ namespace Kyoo.Abstractions.Models.Attributes { /// /// 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. /// - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class ApiDefinitionAttribute : Attribute { /// diff --git a/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs index 3cad4b6d..bac1edec 100644 --- a/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs +++ b/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs @@ -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. /// - /// - /// The type of the action - /// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)). - /// + /// The type of the action public PartialPermissionAttribute(string type) { - if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase)) - type = type[..^3]; Type = type.ToLower(); } diff --git a/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs index 2cf85d2f..cb17020c 100644 --- a/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs +++ b/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs @@ -91,17 +91,16 @@ namespace Kyoo.Abstractions.Models.Permissions /// /// /// The type of the action - /// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)). /// - /// The kind of permission needed. + /// + /// The kind of permission needed. + /// /// /// The group of this permission (allow grouped permission like overall.read /// for all read permissions of this group). /// 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; diff --git a/src/Kyoo.Core/Views/Admin/TaskApi.cs b/src/Kyoo.Core/Views/Admin/TaskApi.cs index 310cad81..dc338c24 100644 --- a/src/Kyoo.Core/Views/Admin/TaskApi.cs +++ b/src/Kyoo.Core/Views/Admin/TaskApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Metadata/GenreApi.cs b/src/Kyoo.Core/Views/Metadata/GenreApi.cs index 57336138..5d3cbf7f 100644 --- a/src/Kyoo.Core/Views/Metadata/GenreApi.cs +++ b/src/Kyoo.Core/Views/Metadata/GenreApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Metadata/ProviderApi.cs b/src/Kyoo.Core/Views/Metadata/ProviderApi.cs index 1150214d..6f9894d5 100644 --- a/src/Kyoo.Core/Views/Metadata/ProviderApi.cs +++ b/src/Kyoo.Core/Views/Metadata/ProviderApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Metadata/StaffApi.cs b/src/Kyoo.Core/Views/Metadata/StaffApi.cs index fc20365a..aea11a4e 100644 --- a/src/Kyoo.Core/Views/Metadata/StaffApi.cs +++ b/src/Kyoo.Core/Views/Metadata/StaffApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Metadata/StudioApi.cs b/src/Kyoo.Core/Views/Metadata/StudioApi.cs index ae138be3..d5274f18 100644 --- a/src/Kyoo.Core/Views/Metadata/StudioApi.cs +++ b/src/Kyoo.Core/Views/Metadata/StudioApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/src/Kyoo.Core/Views/Resources/CollectionApi.cs index 89091a7d..c004fc43 100644 --- a/src/Kyoo.Core/Views/Resources/CollectionApi.cs +++ b/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/src/Kyoo.Core/Views/Resources/EpisodeApi.cs index d716dbe7..ba11597a 100644 --- a/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Resources/LibraryApi.cs b/src/Kyoo.Core/Views/Resources/LibraryApi.cs index d440fae3..4f361705 100644 --- a/src/Kyoo.Core/Views/Resources/LibraryApi.cs +++ b/src/Kyoo.Core/Views/Resources/LibraryApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs b/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs index 030c60e0..5318d0b5 100644 --- a/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs +++ b/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs @@ -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 /// The filters or the sort parameters are invalid. /// No library with the given ID or slug could be found. [HttpGet] - [Permission(nameof(LibraryItemApi), Kind.Read)] + [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/src/Kyoo.Core/Views/Resources/SearchApi.cs b/src/Kyoo.Core/Views/Resources/SearchApi.cs new file mode 100644 index 00000000..a22aa85c --- /dev/null +++ b/src/Kyoo.Core/Views/Resources/SearchApi.cs @@ -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 . + +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 +{ + /// + /// An endpoint to search for every resources of kyoo. Searching for only a specific type of resource + /// is available on the said endpoint. + /// + [Route("api/search/{query}")] + [ApiController] + [ResourceView] + [ApiDefinition("Search", Group = ResourcesGroup)] + public class SearchApi : ControllerBase + { + /// + /// The library manager used to modify or retrieve information in the data store. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// Create a new . + /// + /// The library manager used to interact with the data store. + public SearchApi(ILibraryManager libraryManager) + { + _libraryManager = libraryManager; + } + + /// + /// Global search + /// + /// + /// Search for collections, shows, episodes, staff, genre and studios at the same time + /// + /// The query to search for. + /// A list of every resources found for the specified query. + [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> Search(string query) + { + return new SearchResult + { + Query = query, + Collections = await _libraryManager.Search(query), + Shows = await _libraryManager.Search(query), + Episodes = await _libraryManager.Search(query), + People = await _libraryManager.Search(query), + Genres = await _libraryManager.Search(query), + Studios = await _libraryManager.Search(query) + }; + } + + /// + /// Search collections + /// + /// + /// Search for collections + /// + /// The query to search for. + /// A list of collections found for the specified query. + [HttpGet("collections")] + [HttpGet("collection", Order = AlternativeRoute)] + [Permission(nameof(Collection), Kind.Read)] + [ApiDefinition("Collections")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> SearchCollections(string query) + { + return _libraryManager.Search(query); + } + + /// + /// Search shows + /// + /// + /// Search for shows + /// + /// The query to search for. + /// A list of shows found for the specified query. + [HttpGet("shows")] + [HttpGet("show", Order = AlternativeRoute)] + [Permission(nameof(Show), Kind.Read)] + [ApiDefinition("Shows")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> SearchShows(string query) + { + return _libraryManager.Search(query); + } + + /// + /// Search episodes + /// + /// + /// Search for episodes + /// + /// The query to search for. + /// A list of episodes found for the specified query. + [HttpGet("episodes")] + [HttpGet("episode", Order = AlternativeRoute)] + [Permission(nameof(Episode), Kind.Read)] + [ApiDefinition("Episodes")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> SearchEpisodes(string query) + { + return _libraryManager.Search(query); + } + + /// + /// Search staff + /// + /// + /// Search for staff + /// + /// The query to search for. + /// A list of staff members found for the specified query. + [HttpGet("staff")] + [HttpGet("person", Order = AlternativeRoute)] + [HttpGet("people", Order = AlternativeRoute)] + [Permission(nameof(People), Kind.Read)] + [ApiDefinition("Staff")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> SearchPeople(string query) + { + return _libraryManager.Search(query); + } + + /// + /// Search genres + /// + /// + /// Search for genres + /// + /// The query to search for. + /// A list of genres found for the specified query. + [HttpGet("genres")] + [HttpGet("genre", Order = AlternativeRoute)] + [Permission(nameof(Genre), Kind.Read)] + [ApiDefinition("Genres")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> SearchGenres(string query) + { + return _libraryManager.Search(query); + } + + /// + /// Search studios + /// + /// + /// Search for studios + /// + /// The query to search for. + /// A list of studios found for the specified query. + [HttpGet("studios")] + [HttpGet("studio", Order = AlternativeRoute)] + [Permission(nameof(Studio), Kind.Read)] + [ApiDefinition("Studios")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> SearchStudios(string query) + { + return _libraryManager.Search(query); + } + } +} diff --git a/src/Kyoo.Core/Views/Resources/SeasonApi.cs b/src/Kyoo.Core/Views/Resources/SeasonApi.cs index a92cc256..8f74a08c 100644 --- a/src/Kyoo.Core/Views/Resources/SeasonApi.cs +++ b/src/Kyoo.Core/Views/Resources/SeasonApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/Resources/ShowApi.cs b/src/Kyoo.Core/Views/Resources/ShowApi.cs index 37f3ccd1..46712294 100644 --- a/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -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 { diff --git a/src/Kyoo.Core/Views/SearchApi.cs b/src/Kyoo.Core/Views/SearchApi.cs deleted file mode 100644 index fc180483..00000000 --- a/src/Kyoo.Core/Views/SearchApi.cs +++ /dev/null @@ -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 . - -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> Search(string query) - { - return new SearchResult - { - Query = query, - Collections = await _libraryManager.Search(query), - Shows = await _libraryManager.Search(query), - Episodes = await _libraryManager.Search(query), - People = await _libraryManager.Search(query), - Genres = await _libraryManager.Search(query), - Studios = await _libraryManager.Search(query) - }; - } - - [HttpGet("collection")] - [HttpGet("collections")] - [Permission(nameof(Collection), Kind.Read)] - public Task> SearchCollections(string query) - { - return _libraryManager.Search(query); - } - - [HttpGet("show")] - [HttpGet("shows")] - [Permission(nameof(Show), Kind.Read)] - public Task> SearchShows(string query) - { - return _libraryManager.Search(query); - } - - [HttpGet("episode")] - [HttpGet("episodes")] - [Permission(nameof(Episode), Kind.Read)] - public Task> SearchEpisodes(string query) - { - return _libraryManager.Search(query); - } - - [HttpGet("people")] - [Permission(nameof(People), Kind.Read)] - public Task> SearchPeople(string query) - { - return _libraryManager.Search(query); - } - - [HttpGet("genre")] - [HttpGet("genres")] - [Permission(nameof(Genre), Kind.Read)] - public Task> SearchGenres(string query) - { - return _libraryManager.Search(query); - } - - [HttpGet("studio")] - [HttpGet("studios")] - [Permission(nameof(Studio), Kind.Read)] - public Task> SearchStudios(string query) - { - return _libraryManager.Search(query); - } - } -} diff --git a/src/Kyoo.Swagger/ApiSorter.cs b/src/Kyoo.Swagger/ApiSorter.cs index f328d354..233151a6 100644 --- a/src/Kyoo.Swagger/ApiSorter.cs +++ b/src/Kyoo.Swagger/ApiSorter.cs @@ -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 tagGroups = (List)list; + List tagGroups = (List)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(); }; diff --git a/src/Kyoo.Swagger/ApiTagsFilter.cs b/src/Kyoo.Swagger/ApiTagsFilter.cs index 9ee21a01..10177e71 100644 --- a/src/Kyoo.Swagger/ApiTagsFilter.cs +++ b/src/Kyoo.Swagger/ApiTagsFilter.cs @@ -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(); string name = def?.Name ?? context.ControllerType.Name; + ApiDefinitionAttribute methodOverride = context.MethodInfo.GetCustomAttribute(); + 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(); - 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); + context.Document.ExtensionData.TryAdd("x-tagGroups", new List()); + List obj = (List)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 { def.Name } + Name = def.Group, + Tags = new List { def.Name } }); } @@ -88,19 +93,19 @@ namespace Kyoo.Swagger /// public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess) { - List tagGroups = (List)postProcess.ExtensionData["x-tagGroups"]; + List tagGroups = (List)postProcess.ExtensionData["x-tagGroups"]; List tagsWithoutGroup = postProcess.Tags .Select(x => x.Name) .Where(x => tagGroups - .SelectMany(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 }); } } diff --git a/src/Kyoo.Swagger/Models/TagGroups.cs b/src/Kyoo.Swagger/Models/TagGroups.cs new file mode 100644 index 00000000..d04df266 --- /dev/null +++ b/src/Kyoo.Swagger/Models/TagGroups.cs @@ -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 . + +using System.Collections.Generic; +using Newtonsoft.Json; +using NSwag; + +namespace Kyoo.Swagger.Models +{ + /// + /// A class representing a group of tags in the + /// + public class TagGroups + { + /// + /// The name of the tag group. + /// + [JsonProperty(PropertyName = "name")] + public string Name { get; set; } + + /// + /// The list of tags in this group. + /// + [JsonProperty(PropertyName = "tags")] + public List Tags { get; set; } + } +} From cd5b953e0fab04ec86b03170693c9c2872ca88fe Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 3 Oct 2021 18:56:57 +0200 Subject: [PATCH 28/36] Transcoder: Including transcode methods in the filesystem interface --- Kyoo.sln | 18 ++ .../Controllers/IFileSystem.cs | 39 ++- .../Controllers/ITranscoder.cs | 32 --- .../Models/Utils/Identifier.cs | 6 +- .../FileSystems/FileSystemComposite.cs | 14 ++ .../Controllers/FileSystems/HttpFileSystem.cs | 12 + .../FileSystems/LocalFileSystem.cs | 21 +- src/Kyoo.Core/Controllers/Transcoder.cs | 233 +++++++++++++++--- src/Kyoo.Core/Tasks/RegisterEpisode.cs | 6 +- src/Kyoo.Core/Views/VideoApi.cs | 133 ---------- src/Kyoo.Core/Views/Watch/VideoApi.cs | 134 ++++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 52 ++-- 12 files changed, 448 insertions(+), 252 deletions(-) delete mode 100644 src/Kyoo.Abstractions/Controllers/ITranscoder.cs delete mode 100644 src/Kyoo.Core/Views/VideoApi.cs create mode 100644 src/Kyoo.Core/Views/Watch/VideoApi.cs diff --git a/Kyoo.sln b/Kyoo.sln index 61d5219f..c240e473 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -25,6 +25,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.Console", "src\Ky EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Swagger", "src\Kyoo.Swagger\Kyoo.Swagger.csproj", "{7D1A7596-73F6-4D35-842E-A5AD9C620596}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{FEAE1B0E-D797-470F-9030-0EF743575ECC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{8D28F5EF-0CD7-4697-A2A7-24EC31A48F21}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Databases", "Databases", "{865461CA-EC06-4B42-91CF-8723B0A9BB67}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{C569FF25-7E01-484C-9F72-5B99845AD94B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,4 +96,14 @@ Global {7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0C8AA7EA-E723-4532-852F-35AA4E8AFED5} = {FEAE1B0E-D797-470F-9030-0EF743575ECC} + {BAB270D4-E0EA-4329-BA65-512FDAB01001} = {8D28F5EF-0CD7-4697-A2A7-24EC31A48F21} + {D06BF829-23F5-40F3-A62D-627D9F4B4D6C} = {8D28F5EF-0CD7-4697-A2A7-24EC31A48F21} + {6F91B645-F785-46BB-9C4F-1EFC83E489B6} = {865461CA-EC06-4B42-91CF-8723B0A9BB67} + {3213C96D-0BF3-460B-A8B5-B9977229408A} = {865461CA-EC06-4B42-91CF-8723B0A9BB67} + {6515380E-1E57-42DA-B6E3-E1C8A848818A} = {865461CA-EC06-4B42-91CF-8723B0A9BB67} + {D8658BEA-8949-45AC-BEBB-A4FFC4F800F5} = {C569FF25-7E01-484C-9F72-5B99845AD94B} + {98851001-40DD-46A6-94B3-2F8D90722076} = {C569FF25-7E01-484C-9F72-5B99845AD94B} + EndGlobalSection EndGlobal diff --git a/src/Kyoo.Abstractions/Controllers/IFileSystem.cs b/src/Kyoo.Abstractions/Controllers/IFileSystem.cs index a8cfd96c..f70a577b 100644 --- a/src/Kyoo.Abstractions/Controllers/IFileSystem.cs +++ b/src/Kyoo.Abstractions/Controllers/IFileSystem.cs @@ -31,8 +31,6 @@ namespace Kyoo.Abstractions.Controllers /// public interface IFileSystem { - // TODO find a way to handle Transmux/Transcode with this system. - /// /// Used for http queries returning a file. This should be used to return local files /// or proxy them from a distant server. @@ -51,7 +49,7 @@ namespace Kyoo.Abstractions.Controllers /// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file). /// /// An representing the file returned. - public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null); + IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null); /// /// Read a file present at . The reader can be used in an arbitrary context. @@ -60,7 +58,7 @@ namespace Kyoo.Abstractions.Controllers /// The path of the file /// If the file could not be found. /// A reader to read the file. - public Task GetReader([NotNull] string path); + Task GetReader([NotNull] string path); /// /// Read a file present at . The reader can be used in an arbitrary context. @@ -70,28 +68,28 @@ namespace Kyoo.Abstractions.Controllers /// The mime type of the opened file. /// If the file could not be found. /// A reader to read the file. - public Task GetReader([NotNull] string path, AsyncRef mime); + Task GetReader([NotNull] string path, AsyncRef mime); /// /// Create a new file at . /// /// The path of the new file. /// A writer to write to the new file. - public Task NewFile([NotNull] string path); + Task NewFile([NotNull] string path); /// /// Create a new directory at the given path /// /// The path of the directory /// The path of the newly created directory is returned. - public Task CreateDirectory([NotNull] string path); + Task CreateDirectory([NotNull] string path); /// /// Combine multiple paths. /// /// The paths to combine /// The combined path. - public string Combine(params string[] paths); + string Combine(params string[] paths); /// /// List files in a directory. @@ -99,7 +97,7 @@ namespace Kyoo.Abstractions.Controllers /// The path of the directory /// Should the search be recursive or not. /// A list of files's path. - public Task> ListFiles([NotNull] string path, + Task> ListFiles([NotNull] string path, SearchOption options = SearchOption.TopDirectoryOnly); /// @@ -107,7 +105,7 @@ namespace Kyoo.Abstractions.Controllers /// /// The path to check /// True if the path exists, false otherwise - public Task Exists([NotNull] string path); + Task Exists([NotNull] string path); /// /// Get the extra directory of a resource . @@ -117,6 +115,25 @@ namespace Kyoo.Abstractions.Controllers /// The resource to proceed /// The type of the resource. /// The extra directory of the resource. - public Task GetExtraDirectory([NotNull] T resource); + Task GetExtraDirectory([NotNull] T resource); + + /// + /// Retrieve tracks for a specific episode. + /// Subtitles, chapters and fonts should also be extracted and cached when calling this method. + /// + /// The episode to retrieve tracks for. + /// Should the cache be invalidated and subtitles and others be re-extracted? + /// The list of tracks available for this episode. + Task> ExtractInfos([NotNull] Episode episode, bool reExtract); + + /// + /// Transmux the selected episode to hls. + /// + /// The episode to transmux. + /// The master file (m3u8) of the transmuxed hls file. + IActionResult Transmux([NotNull] Episode episode); + + // Maybe add options for to select the codec. + // IActionResult Transcode(Episode episode); } } diff --git a/src/Kyoo.Abstractions/Controllers/ITranscoder.cs b/src/Kyoo.Abstractions/Controllers/ITranscoder.cs deleted file mode 100644 index 20213526..00000000 --- a/src/Kyoo.Abstractions/Controllers/ITranscoder.cs +++ /dev/null @@ -1,32 +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.Threading.Tasks; -using Kyoo.Abstractions.Models; - -namespace Kyoo.Abstractions.Controllers -{ - public interface ITranscoder - { - Task ExtractInfos(Episode episode, bool reextract); - - Task Transmux(Episode episode); - - Task Transcode(Episode episode); - } -} diff --git a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs index 228a6a6e..7ded5348 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -107,7 +107,8 @@ namespace Kyoo.Abstractions.Models.Utils { ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug); BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self); - return Expression.Lambda>(equal); + ICollection parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; + return Expression.Lambda>(equal, parameters); } /// @@ -124,7 +125,8 @@ namespace Kyoo.Abstractions.Models.Utils { ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug); BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self); - return Expression.Lambda>(equal); + ICollection parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; + return Expression.Lambda>(equal, parameters); } /// diff --git a/src/Kyoo.Core/Controllers/FileSystems/FileSystemComposite.cs b/src/Kyoo.Core/Controllers/FileSystems/FileSystemComposite.cs index 343b4277..8973d26b 100644 --- a/src/Kyoo.Core/Controllers/FileSystems/FileSystemComposite.cs +++ b/src/Kyoo.Core/Controllers/FileSystems/FileSystemComposite.cs @@ -214,5 +214,19 @@ namespace Kyoo.Core.Controllers }; return await CreateDirectory(path); } + + /// + public Task> ExtractInfos(Episode episode, bool reExtract) + { + IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _); + return fs.ExtractInfos(episode, reExtract); + } + + /// + public IActionResult Transmux(Episode episode) + { + IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _); + return fs.Transmux(episode); + } } } diff --git a/src/Kyoo.Core/Controllers/FileSystems/HttpFileSystem.cs b/src/Kyoo.Core/Controllers/FileSystems/HttpFileSystem.cs index 662d1825..e004b796 100644 --- a/src/Kyoo.Core/Controllers/FileSystems/HttpFileSystem.cs +++ b/src/Kyoo.Core/Controllers/FileSystems/HttpFileSystem.cs @@ -110,6 +110,18 @@ namespace Kyoo.Core.Controllers throw new NotSupportedException("Extras can not be stored inside an http filesystem."); } + /// + public Task> ExtractInfos(Episode episode, bool reExtract) + { + throw new NotSupportedException("Extracting infos is not supported on an http filesystem."); + } + + /// + public IActionResult Transmux(Episode episode) + { + throw new NotSupportedException("Transmuxing is not supported on an http filesystem."); + } + /// /// An to proxy an http request. /// diff --git a/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs b/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs index 67bcc97c..74d3a7c6 100644 --- a/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs +++ b/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs @@ -41,6 +41,11 @@ namespace Kyoo.Core.Controllers /// private readonly IContentTypeProvider _provider; + /// + /// The transcoder of local files. + /// + private readonly ITranscoder _transcoder; + /// /// Options to check if the metadata should be kept in the show directory or in a kyoo's directory. /// @@ -51,10 +56,14 @@ namespace Kyoo.Core.Controllers /// /// The options to use. /// An extension provider to get content types from files extensions. - public LocalFileSystem(IOptionsMonitor options, IContentTypeProvider provider) + /// The transcoder of local files. + public LocalFileSystem(IOptionsMonitor options, + IContentTypeProvider provider, + ITranscoder transcoder) { _options = options; _provider = provider; + _transcoder = transcoder; } /// @@ -155,5 +164,15 @@ namespace Kyoo.Core.Controllers _ => null }); } + + public Task> ExtractInfos(Episode episode, bool reExtract) + { + return _transcoder.ExtractInfos(episode, reExtract); + } + + public IActionResult Transmux(Episode episode) + { + return _transcoder.Transmux(episode); + } } } diff --git a/src/Kyoo.Core/Controllers/Transcoder.cs b/src/Kyoo.Core/Controllers/Transcoder.cs index 04510a3d..42135a86 100644 --- a/src/Kyoo.Core/Controllers/Transcoder.cs +++ b/src/Kyoo.Core/Controllers/Transcoder.cs @@ -17,36 +17,70 @@ // along with Kyoo. If not, see . using System; +using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Core.Models.Options; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -// We use threads so tasks are not always awaited. -#pragma warning disable 4014 -// Private items that are external can't start with an _ -#pragma warning disable IDE1006 - namespace Kyoo.Core.Controllers { + /// + /// The transcoder used by the . + /// public class Transcoder : ITranscoder { + /// + /// The class that interact with the transcoder written in C. + /// private static class TranscoderAPI { + /// + /// The name of the library. For windows '.dll' should be appended, on linux or macos it should be prefixed + /// by 'lib' and '.so' or '.dylib' should be appended. + /// private const string TranscoderPath = "transcoder"; + /// + /// Initialize the C library, setup the logger and return the size of a . + /// + /// The size of a [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] private static extern int init(); + /// + /// Initialize the C library, setup the logger and return the size of a . + /// + /// The size of a public static int Init() => init(); + /// + /// Transmux the file at the specified path. The path must be a local one with '/' as a separator. + /// + /// The path of a local file with '/' as a separators. + /// The path of the hls output file. + /// + /// The number of seconds currently playable. This is incremented as the file gets transmuxed. + /// + /// 0 on success, non 0 on failure. [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] - private static extern int transmux(string path, string outpath, out float playableDuration); + private static extern int transmux(string path, string outPath, out float playableDuration); + /// + /// Transmux the file at the specified path. The path must be a local one. + /// + /// The path of a local file. + /// The path of the hls output file. + /// + /// The number of seconds currently playable. This is incremented as the file gets transmuxed. + /// + /// 0 on success, non 0 on failure. public static int Transmux(string path, string outPath, out float playableDuration) { path = path.Replace('\\', '/'); @@ -54,24 +88,47 @@ namespace Kyoo.Core.Controllers return transmux(path, outPath, out playableDuration); } + /// + /// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file. + /// + /// + /// The path of the video file to analyse. This must be a local path with '/' as a separator. + /// + /// The directory that will be used to store extracted files. + /// The size of the returned array. + /// The number of tracks in the returned array. + /// Should the cache be invalidated and information re-extracted or not? + /// A pointer to an array of [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] private static extern IntPtr extract_infos(string path, - string outpath, + string outPath, out uint length, out uint trackCount, - bool reextracct); + bool reExtract); + /// + /// An helper method to free an array of . + /// + /// A pointer to the first element of the array + /// The number of items in the array. [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] private static extern void free_streams(IntPtr streams, uint count); - public static Track[] ExtractInfos(string path, string outPath, bool reextract) + /// + /// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file. + /// + /// The path of the video file to analyse. This must be a local path. + /// The directory that will be used to store extracted files. + /// Should the cache be invalidated and information re-extracted or not? + /// An array of . + public static Track[] ExtractInfos(string path, string outPath, bool reExtract) { path = path.Replace('\\', '/'); outPath = outPath.Replace('\\', '/'); int size = Marshal.SizeOf(); - IntPtr ptr = extract_infos(path, outPath, out uint arrayLength, out uint trackCount, reextract); + IntPtr ptr = extract_infos(path, outPath, out uint arrayLength, out uint trackCount, reExtract); IntPtr streamsPtr = ptr; Track[] tracks; @@ -100,67 +157,161 @@ namespace Kyoo.Core.Controllers } } - public class BadTranscoderException : Exception { } - + /// + /// The file system used to retrieve the extra directory of shows to know where to extract information. + /// private readonly IFileSystem _files; - private readonly IOptions _options; - private readonly Lazy _library; - public Transcoder(IFileSystem files, IOptions options, Lazy library) + /// + /// Options to know where to cache transmuxed/transcoded episodes. + /// + private readonly IOptions _options; + + /// + /// The logger to use. This is also used by the wrapped C library. + /// + private readonly ILogger _logger; + + /// + /// Create a new . + /// + /// + /// The file system used to retrieve the extra directory of shows to know where to extract information. + /// + /// Options to know where to cache transmuxed/transcoded episodes. + /// The logger to use. This is also used by the wrapped C library. + public Transcoder(IFileSystem files, IOptions options, ILogger logger) { _files = files; _options = options; - _library = library; + _logger = logger; if (TranscoderAPI.Init() != Marshal.SizeOf()) - throw new BadTranscoderException(); + _logger.LogCritical("The transcoder library could not be initialized correctly"); } - public async Task ExtractInfos(Episode episode, bool reextract) + /// + public async Task> ExtractInfos(Episode episode, bool reExtract) { - await _library.Value.Load(episode, x => x.Show); - string dir = await _files.GetExtraDirectory(episode.Show); + string dir = await _files.GetExtraDirectory(episode); if (dir == null) throw new ArgumentException("Invalid path."); return await Task.Factory.StartNew( - () => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract), - TaskCreationOptions.LongRunning); + () => TranscoderAPI.ExtractInfos(episode.Path, dir, reExtract), + TaskCreationOptions.LongRunning + ); } - public async Task Transmux(Episode episode) + /// + public IActionResult Transmux(Episode episode) { - if (!File.Exists(episode.Path)) - throw new ArgumentException("Path does not exists. Can't transcode."); - string folder = Path.Combine(_options.Value.TransmuxPath, episode.Slug); - string manifest = Path.Combine(folder, episode.Slug + ".m3u8"); - float playableDuration = 0; - bool transmuxFailed = false; + string manifest = Path.GetFullPath(Path.Combine(folder, episode.Slug + ".m3u8")); try { Directory.CreateDirectory(folder); if (File.Exists(manifest)) - return manifest; + return new PhysicalFileResult(manifest, "application/x-mpegurl"); } catch (UnauthorizedAccessException) { - await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config."); - return null; + _logger.LogCritical("Access to the path {Manifest} is denied. " + + "Please change your transmux path in the config", manifest); + return new StatusCodeResult(500); } - Task.Factory.StartNew(() => - { - transmuxFailed = TranscoderAPI.Transmux(episode.Path, manifest, out playableDuration) != 0; - }, TaskCreationOptions.LongRunning); - while (playableDuration < 10 || (!File.Exists(manifest) && !transmuxFailed)) - await Task.Delay(10); - return transmuxFailed ? null : manifest; + return new TransmuxResult(episode.Path, manifest, _logger); } - public Task Transcode(Episode episode) + /// + /// An action result that runs the transcoder and return the created manifest file after a few seconds of + /// the video has been proceeded. If the transcoder fails, it returns a 500 error code. + /// + private class TransmuxResult : IActionResult { - return Task.FromResult(null); // Not implemented yet. + /// + /// The path of the episode to transmux. It must be a local one. + /// + private readonly string _path; + + /// + /// The path of the manifest file to create. It must be a local one. + /// + private readonly string _manifest; + + /// + /// The logger to use in case of issue. + /// + private readonly ILogger _logger; + + /// + /// Create a new . + /// + /// The path of the episode to transmux. It must be a local one. + /// The path of the manifest file to create. It must be a local one. + /// The logger to use in case of issue. + public TransmuxResult(string path, string manifest, ILogger logger) + { + _path = path; + _manifest = Path.GetFullPath(manifest); + _logger = logger; + } + +// We use threads so tasks are not always awaited. +#pragma warning disable 4014 + + /// + public async Task ExecuteResultAsync(ActionContext context) + { + float playableDuration = 0; + bool transmuxFailed = false; + + Task.Factory.StartNew(() => + { + transmuxFailed = TranscoderAPI.Transmux(_path, _manifest, out playableDuration) != 0; + }, TaskCreationOptions.LongRunning); + + while (playableDuration < 10 || (!File.Exists(_manifest) && !transmuxFailed)) + await Task.Delay(10); + + if (!transmuxFailed) + { + new PhysicalFileResult(_manifest, "application/x-mpegurl") + .ExecuteResultAsync(context); + } + else + { + _logger.LogCritical("The transmuxing failed on the C library"); + new StatusCodeResult(500) + .ExecuteResultAsync(context); + } + } + +#pragma warning restore 4014 } } + + /// + /// The transcoder used by the . This is on a different interface than the file system + /// to offset the work. + /// + public interface ITranscoder + { + /// + /// Retrieve tracks for a specific episode. + /// Subtitles, chapters and fonts should also be extracted and cached when calling this method. + /// + /// The episode to retrieve tracks for. + /// Should the cache be invalidated and subtitles and others be re-extracted? + /// The list of tracks available for this episode. + Task> ExtractInfos(Episode episode, bool reExtract); + + /// + /// Transmux the selected episode to hls. + /// + /// The episode to transmux. + /// The master file (m3u8) of the transmuxed hls file. + IActionResult Transmux(Episode episode); + } } diff --git a/src/Kyoo.Core/Tasks/RegisterEpisode.cs b/src/Kyoo.Core/Tasks/RegisterEpisode.cs index 5ad39046..0a4d6085 100644 --- a/src/Kyoo.Core/Tasks/RegisterEpisode.cs +++ b/src/Kyoo.Core/Tasks/RegisterEpisode.cs @@ -56,7 +56,7 @@ namespace Kyoo.Core.Tasks /// /// The transcoder used to extract subtitles and metadata. /// - private readonly ITranscoder _transcoder; + private readonly IFileSystem _transcoder; /// /// Create a new task. @@ -74,13 +74,13 @@ namespace Kyoo.Core.Tasks /// The thumbnail manager used to download images. /// /// - /// The transcoder used to extract subtitles and metadata. + /// The file manager used to retrieve episodes metadata. /// public RegisterEpisode(IIdentifier identifier, ILibraryManager libraryManager, AProviderComposite metadataProvider, IThumbnailsManager thumbnailsManager, - ITranscoder transcoder) + IFileSystem transcoder) { _identifier = identifier; _libraryManager = libraryManager; diff --git a/src/Kyoo.Core/Views/VideoApi.cs b/src/Kyoo.Core/Views/VideoApi.cs deleted file mode 100644 index e5c205e4..00000000 --- a/src/Kyoo.Core/Views/VideoApi.cs +++ /dev/null @@ -1,133 +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.IO; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; -using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("video")] - [ApiController] - public class VideoApi : Controller - { - private readonly ILibraryManager _libraryManager; - private readonly ITranscoder _transcoder; - private readonly IOptions _options; - private readonly IFileSystem _files; - - public VideoApi(ILibraryManager libraryManager, - ITranscoder transcoder, - IOptions options, - IFileSystem files) - { - _libraryManager = libraryManager; - _transcoder = transcoder; - _options = options; - _files = files; - } - - public override void OnActionExecuted(ActionExecutedContext ctx) - { - base.OnActionExecuted(ctx); - // Disabling the cache prevent an issue on firefox that skip the last 30 seconds of HLS files. - ctx.HttpContext.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); - ctx.HttpContext.Response.Headers.Add("Pragma", "no-cache"); - ctx.HttpContext.Response.Headers.Add("Expires", "0"); - } - - // TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)] - [HttpGet("{slug}")] - [HttpGet("direct/{slug}")] - public async Task Direct(string slug) - { - try - { - Episode episode = await _libraryManager.Get(slug); - return _files.FileResult(episode.Path, true); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("transmux/{slug}/master.m3u8")] - [Permission("video", Kind.Read)] - public async Task Transmux(string slug) - { - try - { - Episode episode = await _libraryManager.Get(slug); - string path = await _transcoder.Transmux(episode); - - if (path == null) - return StatusCode(500); - return _files.FileResult(path, true); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("transcode/{slug}/master.m3u8")] - [Permission("video", Kind.Read)] - public async Task Transcode(string slug) - { - try - { - Episode episode = await _libraryManager.Get(slug); - string path = await _transcoder.Transcode(episode); - - if (path == null) - return StatusCode(500); - return _files.FileResult(path, true); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("transmux/{episodeLink}/segments/{chunk}")] - [Permission("video", Kind.Read)] - public IActionResult GetTransmuxedChunk(string episodeLink, string chunk) - { - string path = Path.GetFullPath(Path.Combine(_options.Value.TransmuxPath, episodeLink)); - path = Path.Combine(path, "segments", chunk); - return PhysicalFile(path, "video/MP2T"); - } - - [HttpGet("transcode/{episodeLink}/segments/{chunk}")] - [Permission("video", Kind.Read)] - public IActionResult GetTranscodedChunk(string episodeLink, string chunk) - { - string path = Path.GetFullPath(Path.Combine(_options.Value.TranscodePath, episodeLink)); - path = Path.Combine(path, "segments", chunk); - return PhysicalFile(path, "video/MP2T"); - } - } -} diff --git a/src/Kyoo.Core/Views/Watch/VideoApi.cs b/src/Kyoo.Core/Views/Watch/VideoApi.cs new file mode 100644 index 00000000..03d5ed31 --- /dev/null +++ b/src/Kyoo.Core/Views/Watch/VideoApi.cs @@ -0,0 +1,134 @@ +// 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.IO; +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 Kyoo.Core.Models.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using static Kyoo.Abstractions.Models.Utils.Constants; + +namespace Kyoo.Core.Api +{ + /// + /// Get the video in a raw format or transcoded in the codec you want. + /// + [Route("videos")] + [Route("video", Order = AlternativeRoute)] + [ApiController] + [ApiDefinition("Videos", Group = WatchGroup)] + public class VideoApi : Controller + { + private readonly ILibraryManager _libraryManager; + private readonly IFileSystem _files; + + public VideoApi(ILibraryManager libraryManager, + IFileSystem files) + { + _libraryManager = libraryManager; + _files = files; + } + + /// + /// + /// Disabling the cache prevent an issue on firefox that skip the last 30 seconds of HLS files + /// + public override void OnActionExecuted(ActionExecutedContext ctx) + { + base.OnActionExecuted(ctx); + ctx.HttpContext.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); + ctx.HttpContext.Response.Headers.Add("Pragma", "no-cache"); + ctx.HttpContext.Response.Headers.Add("Expires", "0"); + } + + /// + /// Direct video + /// + /// + /// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or + /// transmuxing is done. + /// + /// The identifier of the episode to retrieve. + /// The raw video stream + /// No episode exists for the given identifier. + // TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)] + [HttpGet("direct/{identifier:id}")] + [HttpGet("{identifier:id}", Order = AlternativeRoute)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Direct(Identifier identifier) + { + Episode episode = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => _libraryManager.GetOrDefault(slug) + ); + return _files.FileResult(episode?.Path, true); + } + + /// + /// Transmux video + /// + /// + /// Change the container of the video to hls but don't re-encode the video or audio. This doesn't require mutch + /// resources from the server. + /// + /// The identifier of the episode to retrieve. + /// The transmuxed video stream + /// No episode exists for the given identifier. + [HttpGet("transmux/{identifier:id}/master.m3u8")] + [Permission("video", Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Transmux(Identifier identifier) + { + Episode episode = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => _libraryManager.GetOrDefault(slug) + ); + return _files.Transmux(episode); + } + + /// + /// Transmuxed chunk + /// + /// + /// Retrieve a chunk of a transmuxed video. + /// + /// The identifier of the episode. + /// The identifier of the chunk to retrieve. + /// The options used to retrieve the path of the segments. + /// A transmuxed video chunk. + [HttpGet("transmux/{episodeLink}/segments/{chunk}", Order = AlternativeRoute)] + [Permission("video", Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult GetTransmuxedChunk(string episodeLink, string chunk, + [FromServices] IOptions options) + { + string path = Path.GetFullPath(Path.Combine(options.Value.TransmuxPath, episodeLink)); + path = Path.Combine(path, "segments", chunk); + return PhysicalFile(path, "video/MP2T"); + } + } +} diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 1d8e56e1..b2da0668 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -28,7 +28,6 @@ using NJsonSchema; using NJsonSchema.Generation.TypeMappers; using NSwag; using NSwag.Generation.AspNetCore; -using NSwag.Generation.Processors.Security; using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Swagger @@ -90,40 +89,35 @@ namespace Kyoo.Swagger x.Type = JsonObjectType.String | JsonObjectType.Integer; })); - document.AddSecurity("Kyoo", new OpenApiSecurityScheme() + document.AddSecurity("Kyoo", new OpenApiSecurityScheme { Type = OpenApiSecuritySchemeType.OpenIdConnect, - Description = "Kyoo's OpenID Authentication", - Flow = OpenApiOAuth2Flow.AccessCode, - Flows = new OpenApiOAuthFlows() - { - Implicit = new OpenApiOAuthFlow() - { - Scopes = new Dictionary - { - { "read", "Read access to protected resources" }, - { "write", "Write access to protected resources" } - }, - AuthorizationUrl = "https://localhost:44333/core/connect/authorize", - TokenUrl = "https://localhost:44333/core/connect/token" - } - } + OpenIdConnectUrl = "/.well-known/openid-configuration", + Description = "You can login via an OIDC client, clients must be first registered in kyoo. " + + "Documentation coming soon." }); document.OperationProcessors.Add(new OperationPermissionProcessor()); - document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Overall.ToString(), new OpenApiSecurityScheme + // This does not respect the swagger's specification but it works for swaggerUi and ReDoc so honestly this will do. + document.AddSecurity(Group.Overall.ToString(), new OpenApiSecurityScheme { - Type = OpenApiSecuritySchemeType.ApiKey, - Name = "Authorization", - In = OpenApiSecurityApiKeyLocation.Header, - Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate." - })); - document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Admin.ToString(), new OpenApiSecurityScheme + ExtensionData = new Dictionary + { + ["type"] = "OpenID Connect or Api Key" + }, + Description = "Kyoo's permissions work by groups. Permissions are attributed to " + + "a specific group and if a user has a group permission, it will be the same as having every " + + "permission in the group. For example, having overall.read gives you collections.read, " + + "shows.read and so on." + }); + document.AddSecurity(Group.Admin.ToString(), new OpenApiSecurityScheme { - Type = OpenApiSecuritySchemeType.ApiKey, - Name = "Authorization", - In = OpenApiSecurityApiKeyLocation.Header, - Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate." - })); + ExtensionData = new Dictionary + { + ["type"] = "OpenID Connect or Api Key" + }, + Description = "The permission group used for administrative items like tasks, account management " + + "and library creation." + }); }); } From a7a994ab41165a5a5b180e1bf679858328f86971 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 3 Oct 2021 19:19:19 +0200 Subject: [PATCH 29/36] API: Cleaning up the configuration's documentation --- src/Kyoo.Authentication/Views/AccountApi.cs | 15 ++++++++--- .../Views/{ => Admin}/ConfigurationApi.cs | 25 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) rename src/Kyoo.Core/Views/{ => Admin}/ConfigurationApi.cs (79%) diff --git a/src/Kyoo.Authentication/Views/AccountApi.cs b/src/Kyoo.Authentication/Views/AccountApi.cs index 92af8127..3c424efd 100644 --- a/src/Kyoo.Authentication/Views/AccountApi.cs +++ b/src/Kyoo.Authentication/Views/AccountApi.cs @@ -29,6 +29,7 @@ using IdentityServer4.Services; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; +using Kyoo.Abstractions.Models.Utils; using Kyoo.Authentication.Models; using Kyoo.Authentication.Models.DTO; using Microsoft.AspNetCore.Authentication; @@ -36,14 +37,15 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Authentication.Views { /// - /// The class responsible for login, logout, permissions and claims of a user. + /// The endpoint responsible for login, logout, permissions and claims of a user. /// - [Route("api/account")] [Route("api/accounts")] + [Route("api/account", Order = AlternativeRoute)] [ApiController] public class AccountApi : Controller, IProfileService { @@ -78,11 +80,16 @@ namespace Kyoo.Authentication.Views } /// - /// Register a new user and return a OTAC to connect to it. + /// Register /// + /// + /// Register a new user and return a OTAC to connect to it. + /// /// The DTO register request /// A OTAC to connect to this new account [HttpPost("register")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))] public async Task Register([FromBody] RegisterRequest request) { User user = request.ToUser(); @@ -96,7 +103,7 @@ namespace Kyoo.Authentication.Views } catch (DuplicatedItemException) { - return Conflict(new { Errors = new { Duplicate = new[] { "A user with this name already exists" } } }); + return Conflict(new RequestError("A user with this name already exists")); } return Ok(new { Otac = user.ExtraData["otac"] }); diff --git a/src/Kyoo.Core/Views/ConfigurationApi.cs b/src/Kyoo.Core/Views/Admin/ConfigurationApi.cs similarity index 79% rename from src/Kyoo.Core/Views/ConfigurationApi.cs rename to src/Kyoo.Core/Views/Admin/ConfigurationApi.cs index 204634cd..a7a42bab 100644 --- a/src/Kyoo.Core/Views/ConfigurationApi.cs +++ b/src/Kyoo.Core/Views/Admin/ConfigurationApi.cs @@ -19,18 +19,23 @@ using System; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { /// /// An API to retrieve or edit configuration settings /// - [Route("api/config")] [Route("api/configuration")] + [Route("api/config", Order = AlternativeRoute)] [ApiController] + [PartialPermission("Configuration", Group = Group.Admin)] + [ApiDefinition("Configuration", Group = AdminGroup)] public class ConfigurationApi : Controller { /// @@ -48,14 +53,19 @@ namespace Kyoo.Core.Api } /// - /// Get a permission from it's slug. + /// Get config value /// + /// + /// Retrieve a configuration's value from it's slug. + /// /// The permission to retrieve. You can use ':' or "__" to get a child value. /// The associate value or list of values. /// Return the configuration value or the list of configurations /// No configuration exists for the given slug [HttpGet("{slug}")] - [Permission(nameof(ConfigurationApi), Kind.Read, Group.Admin)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetConfiguration(string slug) { try @@ -69,15 +79,20 @@ namespace Kyoo.Core.Api } /// - /// Edit a permission from it's slug. + /// Edit config /// + /// + /// Edit a configuration's value from it's slug. + /// /// The permission to edit. You can use ':' or "__" to get a child value. /// The new value of the configuration /// The edited value. /// Return the edited value /// No configuration exists for the given slug [HttpPut("{slug}")] - [Permission(nameof(ConfigurationApi), Kind.Write, Group.Admin)] + [PartialPermission(Kind.Write)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> EditConfiguration(string slug, [FromBody] object newValue) { try From 6ee5f3ef2469650c7841817bd7dffcab108db238 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 4 Oct 2021 22:07:20 +0200 Subject: [PATCH 30/36] Swagger: Updating icons, cleaning up the openAPI spec and documenting the account endpoint --- deployment/kyoo-windows.iss | 6 +-- icons/banner.png | Bin 0 -> 13642 bytes icons/icon-128x128.png | Bin 0 -> 4041 bytes icons/icon-16x16.png | Bin 0 -> 597 bytes icons/icon-256x256.ico | Bin 0 -> 107761 bytes icons/icon-256x256.png | Bin 0 -> 7692 bytes icons/icon-32x32.png | Bin 0 -> 1144 bytes icons/icon-64x64.png | Bin 0 -> 2194 bytes src/Directory.Build.props | 2 + .../Models/DTO/OtacResponse.cs | 41 ++++++++++++++++++ src/Kyoo.Authentication/Views/AccountApi.cs | 13 ++++-- src/Kyoo.Swagger/ApiTagsFilter.cs | 2 +- src/Kyoo.Swagger/SwaggerModule.cs | 15 ++++++- src/Kyoo.WebApp/Front | 2 +- src/Kyoo.WebApp/Kyoo.WebApp.csproj | 6 +++ 15 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 icons/banner.png create mode 100644 icons/icon-128x128.png create mode 100644 icons/icon-16x16.png create mode 100644 icons/icon-256x256.ico create mode 100644 icons/icon-256x256.png create mode 100644 icons/icon-32x32.png create mode 100644 icons/icon-64x64.png create mode 100644 src/Kyoo.Authentication/Models/DTO/OtacResponse.cs diff --git a/deployment/kyoo-windows.iss b/deployment/kyoo-windows.iss index 276fe810..f6bb8aa2 100644 --- a/deployment/kyoo-windows.iss +++ b/deployment/kyoo-windows.iss @@ -9,7 +9,7 @@ AppUpdatesURL=https://github.com/AnonymusRaccoon/Kyoo DefaultDirName={commonpf}\Kyoo DisableProgramGroupPage=yes LicenseFile={#kyoo}\LICENSE -SetupIconFile={#kyoo}\wwwroot\favicon.ico +SetupIconFile={#kyoo}\wwwroot\icon-256x256.png Compression=lzma SolidCompression=yes WizardStyle=modern @@ -54,7 +54,7 @@ procedure InitializeWizard; begin DataDirPage := CreateInputDirPage(wpSelectDir, 'Choose Data Location', 'Choose the folder in which to install the Kyoo data', - 'The installer will set the following folder for Kyoo. To install in a different folder, click Browse and select another folder.' + + 'The installer will set the following folder for Kyoo. To install in a different folder, click Browse and select another folder.' + 'Please make sure the folder exists and is accessible. Do not choose the server install folder. Click Next to continue.', False, ''); DataDirPage.Add(''); @@ -64,4 +64,4 @@ end; function GetDataDir(Param: String): String; begin Result := DataDirPage.Values[0]; -end; \ No newline at end of file +end; diff --git a/icons/banner.png b/icons/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..db5e97cd4f664b329d03d720591a241ec62a5bb1 GIT binary patch literal 13642 zcmeIZS5#A5^e-HmM7oNA2!x`5(m^_eq8vbJ!l9P{ktV&j(4?s-pr}X(5fJIU6M96X z1_&J@C3J$cKqz4|fmWq`M1Om}&YCO^h zfyg*PAd&&_6=1|V$F&Ohy5*y0;$z@$@8fUdWd~Au=5A}psp)FtV5e_q^DNM_*G>Th zda$YaNaf#vsm$UbsSZ#gRag{k^G(b$6P_m^H z$yLoi6KlW(KK17RHv`s`XA2CV8seY6s+L1c_1R>?MVTGZ`|R|n3=>ZW0&{{IUZ{iF z)x{7qeB|%q4sq4AJks$jmlMqPP_|C)K-E&<1)0|1io)uOEi-rMPANg;3@p9`8K;q~R>)g? z{V&#%>mbp`N^<0)8WD=GC=N%5`FRn7>>;4lOeTg^WE)+TMF{S*CTcx4^w$^i9!M=a%`wEf0rK=kp{}f`d1zjQ>@4(6| z3qhRW%Ka1vR)XIi;y?Qk^!sW9T8mFjXm3GP@iM;Mq~Rda8YXm3+L*hxbdldYuKrJ= z-s_G62-(y9{$*gB&SH#yT%Ij)WFSIo!@HEcJ;>rqasS?)8aMQ4cSJks}R6w z+mJU?cWm+ep32AHgAMhcpm_9~U~dCA)j#`5qaLru{@ujT0jc-BjvIS=qc9)SdV5v` zbbLI)G|AywZ{$1r0ZQO#jPnZ{TRK0!y;$)wC%5c{;RdR+eba&f0(R$hGg?THY@FajKb)brS_nyheGDyW!3hEnBX2mY}$c^CNvFhNq z0u>KlUAh%)ZWO-aWjkC9Pt@27^_A*45cHyvqsMmM#{s+GM9^?Rh2g`k{0jkE*QvL} zYD1gdM<_F>FSp2vK-}K*6!*Z(YCWE71O<*<#8}dn)1XL`i(3KAk`bY$PdeFLKA!#oe$5O8{Th>Sh|ANx z4HyK&>G+~^w6G@X=C9BXL0bP*;H}vxljIi1>hNM6?F8Vkb$p^&sV+#SUR0oaEb>T# zEvZkeI<#xePYDbGW>s}Uq7u?r6odt2d3z0Z;_o1$J|%oGZHuA@O>l*IgH6z17q?=E zb`8|9JfH8LZhaZ`6uNyK3JQPgNMhrdgI8elpx7&U#)1faN_^#<(B1%u7R)(%hcP>+ zZ_GD(N6`*{Tv^+8piKCj$3_O26FI{w`pa3M0h}b&1|K*ER}u%-Hycz0B&Js|W!#{1 z3&HVvJFpwOc;_J@z|^UT8I%~IvRdrSg*fdK^t>oS{DGianr}dXG$snV#LrshOb@wr zB`eFYc^(It4QKdJ9AOli7jQbh6Gk4(4XoeX6HOUS4+zzUA9CaTu=vmrDM158e)Gus zZ&Y+I3aQB6eqI0!mbzk904^DnS^jltpUD4g@#fjjG1HEDBgw;ufA#j*vo=mXLxymg zpZ~w9=G;k^Z=AmuIaqYn)s-;8{FgJ^uF`+^ET?cZk%PF8n+{-z@n7EbQnejAr^*mv z23N9-#SXSJ@D;w&mkxqMLB@ZVd|v+4W|pY|&FU}i zi4WX0*rEEXIUy-a3|xOLG~N&YpVa?_$F;92VQLdzMGkiEHV6jH>vsZCSp?9fGAKtk zO#xfx$$0`$#^njOvNU}R(V>BeII~#L0~&jyfjDU~*~`)unCTw&MgA=Rv2Y(TLm&qM z2j!tj(@?x+>i3~m45t5Z(TM48E!}^{b0C%ezAoR*>TCnaXT01BJHX7o{xa~u(NtsI}TENx~Ip`dheN%DogrZ z7u(_U=3R=J-i8##YvT_Yjz?zvOlN|BOZZM?=AvIGbcZT15i3@S6EiwCEi><&u_43_ z*S#zQ+t@QD;Szb=%Se=__XSyHpaT61D43HG!9y>NFXB6&ROy9%-+}^uV~da=?Wq^K zh2>!krz~An=}@o1%rc>DM--Yl`W0e|(%xB7+RQ6G&{Zlo?OF6#L!(VW3TC)WnU#%e zeD3gAsAux@bq+2gb0nw=b*e`6{cMzW)$@9jBc?o+vLN+#0lW7PWEi;-bR# z>xG(8g+@RzgGj^6Z(hsZ*jy^hYQ&5onpM6?6*0SbjjC#}f;d$gbD8}aBJ?VK+iz!{ zqVUYG@uqu(7ez(z3%5sE-Fv*Qsvk@h=!sWxHcTk3nAq!ly3JW0p*kH%X*dV`3q@3E zt4k;C!Ro-<174|lvBGrHElhYqEQVfbz#-Jl0`dhOUdg{_r3g%Rsy>Z za{Aj7BN}()U8cD)rLR9kq1&Zz&A!?O1l)#&iC`sRsds6CJj~!uJ`EJ|%sM3h%SxM; zeD{@UG0McF{=-@JqEwEcw{C^n%qXtfH?48)@b_zyMYLS(P;kIL2P>qf#dV}-k zcwrO%>jDfB9dnW~oY&eoE1)@8Yic-)a?L`N8k@9g3wm;3u& zNe9pAnh+)jZU2%)+&m=7V-7A|G}OI*A5L&gMK=E4YPzW>L48g2_lFuXL`savNK#in z7&5!@9nThtBQ3*9_mqd*C$U_|r-?Y9_;ZbnP@(V$_z;+VMuBgqMI_Gb|uZKcV zJ4toyVTVYH=|f4_XV%YVP9zc4?Koz|u#`-`m;}7tfhuA3RH4QF{%w}IDmK)LAr{lt z#5*4lS@3#zP>RhwgH$$wKmGbMG`o$piEU(_tVU zVi}bHciFyjPxozdhq{tV#qPhAbqjj7N(6c87x9+E#9QXsRy%EhGU6QMB;@P&nfPA8 z^pX{uQBE=VYpN(}Y;Klq_N|py&f2&`*A?#iE@x|Fst(NE(reduwwgw%oGPiw8NOXQ zCY&Yxc^quB5WQ(6Y3H;YAr&>L^19-6{?`mCp6%;5j>=-xh^-}kqvvd0#D|~XUlgqP zF&y+n6t$I_;I!FqC|jW59K6>E_cCLp%@y<}FgN3-K2yY5ei}JjhsUaE5q>K;jZl`G z%{C)DywThIIR~2ubR6I7c;=BKS&u~E?ZL+(j;9c9zy`-s#X+u_CX8th|*%#m=W@ z{D#40kk;^yWNnkTr@VgbgrTqgSO(}x&Gv_;3o}Jo&)vQzrjLjfUO1iQ?$!OTLv_;b zVZ!4U#eb?Nn~D0LV-5pWe@jEb{52bfGExrxtR=YJCeHLKZF6+|b7vZ131Z8woZ7>yv8F|#tNgiiu z!I-_+qdNS}G}_agY1#^e4xMg_%*kOq1IBDH&Ls&c%UN%~BQ>B~dy z7N`6o8wJVPb%Puak;P*X#c#f1gwq67KhGcIfbR_OWNU-^*_?4RpphF|E2TtOpXPyr4+Hy|VV?~G$vAIVZtA+RcAMyvF4X){(G=yP> zaqy>Yz6^)1;>zu}p+^qvR9)bBQ+Z zFsvU^(uA1~M9NE&f? zN@heflD7Qz2Gp`3&3QGa0*Xbj(cyi9p5iO0vTAb0w*6N+j#hNAw?oU!FwjE_{G4RT z5>JW*IY*h<*wkJy!_E#@fCUZX`QdG#MrW)nIeW)qUmHrl#f-gO;5h`6Z^DI{N!2YX zN(t$ESt#A592O=n(<_1$9k9XW>jO7BV+I2*xJ0X!TVQAEh(uBU4>|Hc{yV`{6@@$t zJuF%S?OgZ@zPQyBfaQs8*M|O*L%iA`IT8X$nNBq-dR};i={%;3 z$ct&kxi!2=VPf!8O|NmB7Aaa*q*&1-8+>^5hIwF>lWG3!Xm_?CpX{Emg>te1dX7ec zgBzfsx6m*>cq)5Ok^EDm7okfiSqx3uQL14|Tx{k05C(Bn>R{X9({Z@49lo9UfkV2k zgahb>yb&_(_HAFcv1zTS4rS?+M1Y&6k~nD|QiF4u<6=xZcn2O6|F|BlEe@d*w;jO^ z2+l;e=OSq0y6H3J!4w;!rLFjE^{3?CtV%n^e(mU^Y5tMS5d$ST1AmQUJ%7E1jQ?|r z#1|dvFadoSeRWus7H+E^uePb*l; zu`{<}!uT|c4gL~(vsRZ+`$%`NjFL_I9gjuyk=4oP$gjdCmO2cHV&s`Oq36855M+Hf zcX(TyT99o;l_26mM_fTDw~|}r{>s!FvOyxa6GOH=FU;7gZQ#dTT$cisE*KyIa}MQ| zHo`uNpE(h}oGd)p`TD-Y6DCnbJGP#bdjL0WtM0dqT@*G~%1?Pg^rYY_Cln?EE-4mS zet5%uDfuH~n9cLKqk#a!{KDJ(b|6=_fH}-B%{Y=n_mQQPcdPX;g6-Fnj)bX+Y&SrH zi}!JPRbVbs@V|n~)EmRw#uZuKnp{gi?yt$`y>u=^Md0RNPe(L9T6uoTf{Ec6sH?N4761&`oJDDF&%=0*e@geRnNz`h3 zW(J*Ih_$)34ltBIzmG5=(=g@|Q$H$Nd<~tyhnevuBL&msS8W>$cyccGski!cUMT&N zyf7fT!+OG|eZg0Yz9Rij;-vb^-!3Q0GnikIGL5x=my-yiDKC<>w`}~jI(1r8y&;Ys zZIZ5uYNHYQN(we440Bn3ZQKq$aOb*n_%w6oxU#7gH*ZyUAsm%hlt1kzfZN&-y#IME zIwJ!&V$-*`&86PQtxRup0GcZfw zuri9M`>QXySy~=91o|tYbp$z@w&Nw=qI2~#M{NG5oK{Rpd_4hYGnHTuS$`O-g z8slnV#6)@|fK&pC?CoAXnf(!%d`zM=JMv9o;7yhWCD@KM;vzQXuwh5#NcKt=jP8Le zUJql(oJ77kwXiq3S3Y{cL_!`j9DGz1#Zc)O#_mJ7o<_%*Pb3X}50@ZoJ<@Kxn=yB* z?y33e&*8K6;>y0NZ${rIK1v%*h`;kyYP4`*mH~ z+hF)aKtRd((3FIPJau0sH~C@9?qS@Y?8S~hGxyijepGz3EDLFd$F=cRSw&aSC*O+u z&eaq4^?M0ks3dMiI5U#X#*$eOXrC%SPCE1YQK+3O?9fA{_H~!D^GU`F|Ci1_Nu|cv z6Gi^!`Vc`E?X-P2ujHF_?(;n^+OgOebF6n5wO68!Qb!KH_v&dJTK=0ir~c`PEs|C1#!;Z@ zkSut>Z zmE?`=QGmW zvAh{svFKZ&i76_dn08_&&KUPSi-~0^;x6woefc=(PiF4HB0L&rZ3c8(0U@4@+^=ka zLc4>}*e9#6$I8s|b7z+t1HNq-#*?guFd51=!VIOiv)lM~3c2w4BVsaT_Zy^vj_xk? z14bG22kwHUl@r3FgY4Y?Si=!r3$>P%JLhM<^jQT*x0$|DCNO3f3PJxPGiE}_XF830 zgP9AB8T?_FcNF7v4@UxM07qR-SUHj551AWYnFkD7TFt-xHAZE<*jP61JG4mxkbbGa zR%DjkncF~OebQ{1IC+`#q7rZt!g4o5eG@wAs4MA#o#*|~7+wSI2b@>=wEQ%g5e|}* zmdy`o`L;iTupE+|8rwETvqJd*$s?Jy<;A=;HhKMqe5y2Z3Im&@HBX79=43ijhy+S! zAIoApLtMsY=tr9=tc&g(+<2hc zL_TSXy)cO>QK->M2t(6uNTWPhO;|uZF6(ttA!bZb={X-2EeU(nlmwCjPP9+VJuDAq zqK%#nV6d!cOk&-XyM0k*LlRnU-)_)NNTAt~_*OwQTyMjrSvXg31_&rW$A6bReF0Lf z@%s*BXYGZ3yPRm5n*xEIUe7D)HWxx#oTXq}h*?$lRQ&iSMd;YU+@TK#E;p6Hz(zJ;`6k{dm;%`oq2LqX-wPP4DfsN3G>i_~4|bBl&C zD1S4E-!|?CXR`S!a4gM?Hg}UJ>art3y>uIbG1Xtsw(uboCZe%f6bs$WT@MOt}A1gGlRTEMkoA1_x|3T?a$frsbi-E;xukb0X@2>Kb>?m zaU%I-Mr%Ng*gr{S{hZnHIrWckOm6FdS~Oz}n<)~jUnpSEE1GCmOc7gj zh=`hD{@9>%bHdWZ&ih7~3M zdLms4UXrZ34p;TpC%C?D8U>9QtE8Mji-P}fn2Q^fq2k^Ts2Dg1jFt*F0x{5V`gpR8 zy@21pE@9~x@uNVw#FCu)=y_DN87 zE$!)H2?;W06x#*&E8qZ>woE7K!e1+l$uI+!8V956-z| zU!dwQOI=xa*y!Db#lxihDsJAdA%5(p1d6h3kzq?>$COdP>)GyrL_Hac21blHd*u(; zxaK|DxMp-$IJ9JGuOW_XC_&tX2iKXFK^?J$^Bn(54@Eix4FW6oxk6pzNZlr{=HCXJ zfB-MRF!mT=jp|V6@f`^iK&1S}IPa=toSR_LZX@{+WSU1-EK<)$NWV z=fz#%+qJJjqzALj%uPORQ!ls>roLm#E=+M2jrV>aqhCaohgP;{Eu}VFo1HjFpjq@YU*K$j#sM5^{2LBmLTl%_xp8~59a0I43l$6QPde}vP^34R`+eY{i5IsaXa^RQ&z zX9i{S+@=b?sPx@Qv;V=yNM&TO!=J!!E`pCc~=17tw zG|egGN{wQH)w_aw zb$rfxO$dHs+Y5m)uABPc<_82e67rKHxlSp;nbA|{=X1iS$X~Y7*_QPE=q_zRselsK zD;W?6tHi3hGwlz#L_;C;&VV>$Yg|0XjFul(!SM`uE6~{Tv;1vzC!vhtI@Mqx5XGMb z%`Lq^Br!!4(NZ_6ZN?`X2rVjkF}Bty{NfKB&jT!3GtMu5JRxs97-0RMc&0>Mx<#by zW__aE{4T?L)>O=WnOc)I1;!J1xrzA?ZN8I>fxXw1&rMIdF;>hw;+q?louM;f0s$PKF2zqFAt11Tl&E#dL>oG--J*E;&5YvLw1J<;6xLfkH_Op%Sh5-+(LNMQJOKt@7-L! zahJpI=W*N@*+XXs9-L*lG|}$hr~@p<#4`~_hTz*^AxDYbB_IraSq9MAYvHQ1^? z=!2j#JpT&1luFHn$`pvm&&-bwev2gwq@MSerJAQQ%X=Ie#&@eW=kKB);0JqYuZ@H_ z#G@>vXG`fT1-QN@jSbdWZ14n3dWy+vH*X7voK*H!fF1b}&W>k<+#v=QD7`GN&hSYD zP~yz~s45Pfb3B`b|^V=vm) z;t7^`q&lBGPS2n`v8(E)#+=Zv`ol&y8oN4v^_AMf9Z!ikEi8P__gWH`D`L>TbuTd& z-%|PwKP!S)Xdc{erL!0VbBds$@8C1`SU_Z09<~d%ssy3ppm-2zXC(-$YLi}!y9!pr#A_p(|W!($=F6*|! z#5n_v+ON>S&X$`J7xpo?N+;E92?Qp#;N}(Q^w6UxF|Lf)J}vsbhc6s!XGQN!Z1-*j zf=N75Z43%{wSdyrrsZcfyYybQs?FSf9Yk92u31GUD`_{I}NtzwJP&yw|m_wt{6OWOlwIJWaq0vG1$F$k#>pIE^$M?CnoxpbX zt*W4ih~*Qn~cOn>%)G z8|7#aW(~5wYy^ibS12oPt)lvUGn@cSOvrH2xymlURzQaJY0R& zwuf)+m3aD?u$kt^OhQhT=zKDJTYJisIz~^hc~ds3_;pK>j6J+WRjD>0YGKzQsUYnq zJYGDy#Fgx@ueB_iCg~B)?AeO=uH0N)yZ1}H=f^@k9cEw%PA5!tTspC@@Jl>oo6NFo z!U+JLFg{Clk~dTO6Pvhk*eC5XDFaz;{M_UFkg0!*klCGap~z^bBweR_S6XOj#hi(o zs7RhpN#=*%d1LIKr*pl6g&^1JG-xS<+qqtb3LJDl``U{+(O1RF>k2LvgdnkmltJGM z^ug0T|`1!wNavRNbBUpmt=uWj!&Yq&ds3)sxw~)3!4_5E$yZ(Cj2ddq==1_ z>fK)7vo#MtBbjumGo`bwj>^f(+mCx1F6ul*|0PMWdH*sY+M*M{7u|LF?F)-@u&0A0 zlWCdY9a$dJyg(lK&>3NQFtTB}8#jR$ok^e(R}z2G7Yvo2*BDUAQ5AO|)`YUr%(&+M zT=R$2$ISE1@ikgi@xGC4_{jq8Xk|JWRSZ4X@A>kpk?w`(FZBVur(?ScgED{LoyFRj zg(iQKUC+5%aQ*X`v=AJpU&L+w_5G$iOA+_$=ZCJw-R>?;hiT!`USB1o< zW7b=QBdyc7))nA{&&=bb6r|uKL3>H}BYhdf?KnLf@Z@*!4^g7QdT=JQ$ELoSG82VW zCw6MK)^K6^-(hyf7Ay-cQ?7BpVEdjkZA-tRz+}dFLGGJ0TB831@S0VPiA?tOhv?jU zQ3^h;E=_Jddd|FEc(DLXiU~^r!DLmE?Zs>@)~$AXf5@3_Ui6z>@$Nh4jD(MisvR2! zNAEn|y6-KlPEYYSnC1Q^ln(WKrFt?O-%t+_y+R6({uA|WfMMiFHF~<;1O$guiCND|hFjSt!kh>Hf?e#1cPgeA>BL@M9`S`-8iP4_4vb@D|rL8m?!# z1^9|?9>ylD6Y*&hGI;(=`9?tk!=J#UPmL_+_F@4RlC#BL7bDr;_4D*}BzcCAxHl9O{o-Pj5b-KW8%9H!ZbE-U!*nLPY=k1+;m-g3wDsOwsco7(n04MBVC+Hm zh+>vmnHors^l;W9VhmAd$d@?l-PziL`HWa$B@|_ze;5!c&-b42E3nVl$J`)!1a7zu ztyGG5qxQ>CeW#)NQ^J^w@;x#y*yuhZVc<1}cr%o|hz^p{O7@r&(RRb9{fov{zdVp3 z=Q9K^uhIt1={G)QgaAwlr@D>*`BGP=(a%< z6I?(>UOs`RpUb-CFyk5LJ53{&(WUwULDuPX(yG-|wnxr4G; zml`}aku7r4M}l!(m>)D<4WGJ(;4&P;Pb(cM8%f%$v zVR2iKdlX(xVebEgTE==DIycuAD7NhT(s0DZjNx{+16O$xqlmvuy{8Yf{een!B1a_S zXsfKQ|4cViGCJ9`INH?J9!>@EKNQ)-G@|Q_#0<=1?m78yt0=Qccpc$(v|FA0I}Hvc z+TkU8Mrr8K0Nm+bxL;`%w_7RCh>9_&Oya%GIEsG#4~`G=?X1vgxJH!PS7 zv40(^kEt6vA!*A6kQ(8nsv6*1JDr36k*e;$6wpocK(8w4A-E0h(@wgOMpFfgZX>|r z?|>2j*j)I{*)jm%0$SBx-!z7>s{#-pP$pv&EUz|PkWKUQYW&^I*?k>e5a7H(x+7;) zKqD%A+ipG74kULuTYVyf`CDb0|8rkq>rHr2s{kK{T0M|&ar~TiHJOLQ> z{_l5yetwP8|5>821R^~ccJZBf^FOxkp10sH0gr9y*3&)|FR8;kF*X-|cUOO+ZcXPpmP`#Fe=I`i$Uee4d*WQ(_~Iw&2PE1E zH6%;kNwoxPe%{c^6IoCppgfi^!i4*XF%-!Nz0{eBI#Aq>pMyB5Ez3dw2ha)mKHzcuIQ@UO23WY9edcxJeM9^#pL$70VY!ZGy8Msqr9cznsObm9 zT^=6^{578s@J-b3O9(9cuX4*=NvnnWQguo3;;y)f{~2BZ6x`_Z=r3w;Yy&@w@eJ-RR8VH_oI!04Yoat?J4Nw3k7+$j(sa>_G2?;(EYvT zWQVe87zEhG1|_HkO-iZ<0Uo|Q8FR_7%og`BZE=_T)uF%GpZ?tw6(kt&S1D2j2Y;of z9$ji2e5*G1zpL18Cx3_knOkNJDE&VZ5jf)CiEtHDO7iSYychswb^G)7tbCgi6gdK{ zTDzx(<&p7`x!hwt23Wd(3WnB7Ggr7c+sMnMx|B)?mZv%>{n^adiofDh;CH|j^W_ZP zZu7`UZV6wKyubnnx+4f8iwa>0pu1$X1q&ZPPqmvXj=+H!`#F8WZ2%l)ul%ZlNi}vc zFPB=m_tIfWQm+hY^9v?2Q{hCdu#IQO6EpGB_^yyWcuMe4pbRBtyf7#aP)V_YXl&cA zgZG8m)t!s@|8FC$<4+AU=*6da4R4bZ9?=hzFGWPtM4wNA!7N}h= zr2zkP`U=RbMvO?I@6Q<r4cq3GK7HG2^2oO*PPtNR`WGRk;)gf zq(Mn#cAVtzsΞPz%@W>rxL1l{Q(h)ZNQqvaoBbh@vr_H?LM=-Ws%nQ3TE;V8jBn zf}n2zR3`9mgcu4$eEZrwHN`QY^ooY9OdoVRkuGP^kOmS=#wG{gGfBv;`)r})iNci5 zB2!tqyGIB4YQZkcU?EBLAr6y2FKqOek}@UN9Yz99TZydB-S!$J^kTOSc`SS7%bERW zCc3|5vKN7_)S2j~-x<08+06dUM<-^I&AhmC{x4J|*%Qe9&7S$JP+!5JnY$ECuoi;9~-ynEmbtYfzdMpU&Zo}zpLZ9${n}j*Nm#Eq6TE3=f`k$w$jR&^^vM{ z5uB|55)Qvk&0SOTGQ9_HQ+(;zmAh5$RL4Y#aVM$HuP9te$dkANJSdteD()I~HvVB) zQJ~r@y+*0POyQEB9>_nc;6O=bmIjwF;i(}2wF3fH-?V3$Oi)?E96F>1?g5$TKSc@H zx^cQocDJeTB(3;GbY`VZGriZ`Be1-;JH1M>m~hCal@;u2K?(Z5@JU2pFk6EvwzC4t TSK01fE~u%d^QctS`o;eO?;l}T literal 0 HcmV?d00001 diff --git a/icons/icon-128x128.png b/icons/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..f602aaaa40bb037117dd189e0c33cec32f068545 GIT binary patch literal 4041 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 literal 0 HcmV?d00001 diff --git a/icons/icon-16x16.png b/icons/icon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..f0ecbd819daa84075fa1a47fa177b21b5f134036 GIT binary patch literal 597 zcmV-b0;>IqP)DlS^n5Q51&%`^-!-c}J_YYBfbrL0tG)W)mM^E7Glk zxaiWI;37LwQE;c5uFT3^iwlbdgNRVDAY$Bfp~l3bAYp1UO)|-3;*}Xmq0vIk2S1#1 z;r!NDHS! zT`V^ZaX#M0*$+52$)ve-(&$v3U(-|XYNahFxVLv5dt?t6KgO_PGtB?gV6r409NCe` zmFezF#m0gVdY%vx`=>#nE-}BR`EfSt_<3%$iGmeC(M=*APX{$Ec9k)?%9vavgp6-m z!RzR8Sa2_PF z*H>M~u_}zoDWz2YFN5iL8!s%06`SMEcLTZ626mEl)azf{tyXo(b?q~*>x{c@=h>ze zJiM{LIn0;GLbZ)FY4;u-=-Dq#dg1ox=IbqQ!6VERtR6w9W1&_nH*9-t?ry0uk=E&x znKAuoC6mey8bHeMt7P(!N+c$ft|t{CY@Ks0417Sfg9T`+%TY2 jEP92*{zl@v3=4qY26X(q5>8yD2N`Y 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 literal 0 HcmV?d00001 diff --git a/icons/icon-256x256.png b/icons/icon-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..a395f0fe942ef7826d580fac8ded0386598a41c4 GIT binary patch literal 7692 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
G*eaI>pNT;POF@@ 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%0DKML zKx}Gxx{(~f=hkJX69K#o2@!cM21D`Y6=oH>o=0zh;7HVm-H%Ev#&Pv!4 zat*+=Q}Rs(D1_jXxEr7Kr0{YehfmYp*#B1q@65R2p)zKw3NFtqvZX-)?*W*9|Jhwv zRR~W4a7@n73Fu%g%1iwgEdcVjFfcg)l}sHTmUf2D#ek2mjWeB zYhm11>+WYBc=uj6KR+8@LR9u_NLb>etrTMSh<@;s) zSn1{h&kyLx=jI!o&s=}pB2F~j>{P(8U*-9}m9I3-)v7Aa{JDsmH3ct78GJR|i@tE^ z=G#y8pGzL>xI?LNrQP#4Y{iWjlnO{1YmHW=Wxhiidc8%T{;h^@o(`=Hxr>pX_hJah zGJ_mw_iTo?5nvrLTNC&>>;0u-3Z>$`RdN8AR8@Wa==D^WwHqechGcKk?ckshnK1MM zbUkOg8CEG>1khxv|B4nVV5O9Wrse@9^mn$aBq0000< KMNUMnLSTYhxE}-n literal 0 HcmV?d00001 diff --git a/icons/icon-64x64.png b/icons/icon-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..ad7d28e990527a6623ee685e3f2289f216412048 GIT binary patch literal 2194 zcmV;D2yOR?P)6)eR7;kX9zKhj6Q@ zJjNfL&<03sG$6ESAVCoodkCrPga(yBD*FdE@kk^Dte~oukT$WR%^ff{vGJ%V3|N*p zk0p&;CypK4_g>rR+*dx^_i5j>Nwc36>E=G{@BHrh&hMP_eeS5$YS_&1ZEEY^C4eD- z66;~(cZzUTT5>Doe%0UXC`!C(o%Spbhqk~py$31BgHqZ45l1C9WAo8$PGIF3JX zjmC6MVdnvjNC7w{N#fbfN`Qt*b!v>R<>0ObS|vb(Oaui@NfH+~p#9babol(JRVz+D zhm_-#Iz2WN5tanRdUit;mLUjBs7Rud&m$s6RU$Toa@Y{CM?aRtJj&&)QzBp{q3LAe zFY4jQU@G&vlMhNK6Z-+220#OPWH6OE>2wfwa7=)PinIbj$UziVoPNhS@`&*}y3?u= z`_=Qm9!zCEZTE0GnV>RJ!p$dwns}Re}`n9`n8o&p-e0Zpf;$g1o z*|%z;O9wAZowxe@bTV-tfTsbM)>dJ34~!@WW54VQ7OJ7atAJrg1PV`k1Y zmr#vAZSZIoz=sA?nQ_;anxHKL_QgWDH3ED!K5cmYhlL)bOUm(g`V@71UX0<(uT8PH z8A@n6nRrn302YS^QyELoaf_Cwz(*5dJi288ao?4uYoodCcs1|GE7<}BiVJ6Yb{^NHl$V$%{j>7|L zP#l+{cr7c*$`Offmx(aK$i5+bHmDf?=#?HE`SbLHYgvxFB4AoreVgR+uVk&?0BWPMap7k=OwEKvX~fnfDFh;g}K!$@pjgJ*izv0(em+V%W`??cj=lKP?K# zR)g~Kn??KL@nbQ?czTB7LnC}X9-+_{#JY!Y|NHmqO3`3E3nJoY9LG<1MT9E?$kor~ zd*$QzhB_?o-4^WBgC#vlsCV# z)4ELlWUpU8{{52q*HB>nRzeA^G_B6fRS=C0A{dIx<+u%oR0(aF2-gJ6mjpbO-H8F- zx+>Iy-|OXGT+9^>?`bstH}hId=|E>7j%aKM;jXRlbG+TKv|J*~ar`L9@w=Sb(^4Yz z(^+GKG~5;s>q*jc%UuAPgXPhI0ES~m#y1R*kH~*ij*G$)M6ql)JZj_~;W&Ow%`X3D zb@Q663>}KCdPigMBWa{$YB)A4|NqT-bK_YM38AhXmJ-?kBI2)h654V#Xa+LyQ;|9O z>Y02$@{%aK`fXh$?B+=o1nl9I%JG4zow%5D+*gpHNvKkeFF_Ovc8_QhYS_{CoPdw_ zb?V12j)8kvbUytz@2YyYx01`Qkr7H=idrjW{HNVIiB|#OuJ8>c>wGFO7w3w>Ui2$ z=Lw_+UwV&iwx^?7DWSMBkKEFvQ{yS}ACx3m*hMl zfOY@hE_XY6hUy769#HmpxZf{FbQgx7P$Rl)!!>w90HwJa{#CT@6R@CoaJ3rH&z$JH zjN1qM?H;3u{&iN$+8KM>1W@kJO540(KL6`bt6(BKgRi|)A23ht5U_hopV1SthN}_X zvt{_U3D_514PF%8?dHE6(TfYZl>KpH1}Af?eT+T`IDNB^B_YKa?)i?Cwh3TMUrK+? zP$y*#8vgXfN&Kp`8as%DGdFig-}vCrF*TyE*;bsk2{;&D^+Y56r-)uG%-BlV6aT!7 zSE~{I|89ufS0anwOD7ZG)y~m!Svr}xs_qx?1Z@+bwY>>BCKYF;%t03irBZ3^PClkYq$nW0=(KAxSrAOUSTkMd`36o?+V&k7uc}-gITmfz=q!;&cY1=H_UGY zSU4eI-Ah?Jup;1`I=^p7!!;NYaKrynj0H=;4>r;d9lXN3Kb~F(W4Nx^+ynslKXvXt U5Gfy@#{d8T07*qoM6N<$g5NhZ7ytkO literal 0 HcmV?d00001 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5efb3ebc..8983bf4e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,6 +15,8 @@ 1.0.0 true snupkg + + $(MSBuildThisFileDirectory)../icons/icon-256x256.ico diff --git a/src/Kyoo.Authentication/Models/DTO/OtacResponse.cs b/src/Kyoo.Authentication/Models/DTO/OtacResponse.cs new file mode 100644 index 00000000..13ff8aac --- /dev/null +++ b/src/Kyoo.Authentication/Models/DTO/OtacResponse.cs @@ -0,0 +1,41 @@ +// 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 . + +namespace Kyoo.Authentication.Models.DTO +{ + /// + /// A one time access token + /// + public class OtacResponse + { + /// + /// The One Time Access Token that allow one to connect to an account without typing a password or without + /// any kind of verification. This is valid only one time and only for a short period of time. + /// + public string OTAC { get; set; } + + /// + /// Create a new . + /// + /// The one time access token. + public OtacResponse(string otac) + { + OTAC = otac; + } + } +} diff --git a/src/Kyoo.Authentication/Views/AccountApi.cs b/src/Kyoo.Authentication/Views/AccountApi.cs index 3c424efd..a2cbf2f2 100644 --- a/src/Kyoo.Authentication/Views/AccountApi.cs +++ b/src/Kyoo.Authentication/Views/AccountApi.cs @@ -28,6 +28,7 @@ using IdentityServer4.Models; using IdentityServer4.Services; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Authentication.Models; @@ -43,10 +44,13 @@ namespace Kyoo.Authentication.Views { /// /// The endpoint responsible for login, logout, permissions and claims of a user. + /// Documentation of this endpoint is a work in progress. /// + /// TODO document this well. [Route("api/accounts")] [Route("api/account", Order = AlternativeRoute)] [ApiController] + [ApiDefinition("Account")] public class AccountApi : Controller, IProfileService { /// @@ -90,7 +94,7 @@ namespace Kyoo.Authentication.Views [HttpPost("register")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))] - public async Task Register([FromBody] RegisterRequest request) + public async Task> Register([FromBody] RegisterRequest request) { User user = request.ToUser(); user.Permissions = _options.Value.Permissions.NewUser; @@ -106,7 +110,7 @@ namespace Kyoo.Authentication.Views return Conflict(new RequestError("A user with this name already exists")); } - return Ok(new { Otac = user.ExtraData["otac"] }); + return Ok(new OtacResponse(user.ExtraData["otac"])); } /// @@ -126,8 +130,11 @@ namespace Kyoo.Authentication.Views } /// - /// Login the user. + /// Login /// + /// + /// Login the current session. + /// /// The DTO login request /// TODO [HttpPost("login")] diff --git a/src/Kyoo.Swagger/ApiTagsFilter.cs b/src/Kyoo.Swagger/ApiTagsFilter.cs index 10177e71..f3537541 100644 --- a/src/Kyoo.Swagger/ApiTagsFilter.cs +++ b/src/Kyoo.Swagger/ApiTagsFilter.cs @@ -59,7 +59,7 @@ namespace Kyoo.Swagger }); } - if (def == null) + if (def?.Group == null) return true; context.Document.ExtensionData ??= new Dictionary(); diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index b2da0668..b929ed62 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; @@ -58,7 +59,7 @@ namespace Kyoo.Swagger document.Title = "Kyoo API"; // TODO use a real multi-line description in markdown. document.Description = "The Kyoo's public API"; - document.Version = "1.0.0"; + document.Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); document.DocumentName = "v1"; document.UseControllerSummaryAsTagDescription = true; document.GenerateExamples = true; @@ -74,6 +75,14 @@ namespace Kyoo.Swagger Name = "GPL-3.0-or-later", Url = "https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE" }; + + options.Info.ExtensionData ??= new Dictionary(); + options.Info.ExtensionData["x-logo"] = new + { + url = "/banner.png", + backgroundColor = "#FFFFFF", + altText = "Kyoo's logo" + }; }; document.UseApiTags(); document.SortApis(); @@ -129,6 +138,10 @@ namespace Kyoo.Swagger SA.New(app => app.UseReDoc(x => { x.Path = "/redoc"; + x.AdditionalSettings["theme"] = new + { + colors = new { primary = new { main = "#e13e13" } } + }; }), SA.Before) }; } diff --git a/src/Kyoo.WebApp/Front b/src/Kyoo.WebApp/Front index a3da5f1e..7bf53b40 160000 --- a/src/Kyoo.WebApp/Front +++ b/src/Kyoo.WebApp/Front @@ -1 +1 @@ -Subproject commit a3da5f1e6edb982e3b71792a7ea6fcd45661c337 +Subproject commit 7bf53b40080d1d43228f1cdcad510b302ead99ff diff --git a/src/Kyoo.WebApp/Kyoo.WebApp.csproj b/src/Kyoo.WebApp/Kyoo.WebApp.csproj index eeb49df7..4d637707 100644 --- a/src/Kyoo.WebApp/Kyoo.WebApp.csproj +++ b/src/Kyoo.WebApp/Kyoo.WebApp.csproj @@ -12,6 +12,7 @@ false $(DefaultItemExcludes);$(SpaRoot)node_modules/** Front/ + ../../icons/ $(SpaRoot)node_modules/.install-stamp @@ -29,6 +30,11 @@ + + wwwroot/%(RecursiveDir)%(Filename)%(Extension) + Always + + wwwroot/%(RecursiveDir)%(Filename)%(Extension) Always From fd50a2dedcd482955bbb9152fb81ffda89d26e6b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 7 Oct 2021 21:48:40 +0200 Subject: [PATCH 31/36] Adding the missing documentation on every classes and methods --- src/Directory.Build.props | 1 - .../Models/ConfigurationReference.cs | 10 ++- src/Kyoo.Abstractions/Utility/Utility.cs | 10 +++ .../Controllers/PasswordUtils.cs | 3 + .../Controllers/ConfigurationManager.cs | 5 ++ .../FileSystems/LocalFileSystem.cs | 2 + src/Kyoo.Core/Controllers/LibraryManager.cs | 3 + .../PassthroughPermissionValidator.cs | 4 ++ src/Kyoo.Core/Helper.cs | 3 + src/Kyoo.Core/Views/Helper/ApiHelper.cs | 64 +++++++++++++------ src/Kyoo.Core/Views/Helper/BaseApi.cs | 5 +- .../Views/Helper/ResourceViewAttribute.cs | 6 ++ .../Helper/Serializers/PeopleRoleConverter.cs | 6 ++ src/Kyoo.Core/Views/Watch/VideoApi.cs | 12 ++++ .../Migrations/20210801171613_Initial.cs | 5 ++ .../Migrations/20210801171641_Triggers.cs | 17 +++-- .../Migrations/20210801171534_Initial.cs | 5 ++ .../Migrations/20210801171544_Triggers.cs | 45 +++++++------ 18 files changed, 159 insertions(+), 47 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8983bf4e..b2300460 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -45,7 +45,6 @@ true - CS1591;SA1600;SA1601 true $(MSBuildThisFileDirectory)../Kyoo.ruleset diff --git a/src/Kyoo.Abstractions/Models/ConfigurationReference.cs b/src/Kyoo.Abstractions/Models/ConfigurationReference.cs index 2272612f..635bbb97 100644 --- a/src/Kyoo.Abstractions/Models/ConfigurationReference.cs +++ b/src/Kyoo.Abstractions/Models/ConfigurationReference.cs @@ -105,9 +105,17 @@ namespace Kyoo.Abstractions.Models return CreateReference(path, typeof(T)); } + /// + /// Return a meaning that the given path is of any type. + /// It means that the type can't be edited. + /// + /// + /// The path that will be untyped (separated by ':' or "__". If empty, it will start at root). + /// + /// A configuration reference representing a path of any type. public static ConfigurationReference CreateUntyped(string path) { - return new(path, null); + return new ConfigurationReference(path, null); } } } diff --git a/src/Kyoo.Abstractions/Utility/Utility.cs b/src/Kyoo.Abstractions/Utility/Utility.cs index 5c0dcc80..04a1f948 100644 --- a/src/Kyoo.Abstractions/Utility/Utility.cs +++ b/src/Kyoo.Abstractions/Utility/Utility.cs @@ -425,6 +425,11 @@ namespace Kyoo.Utils return (T)method.MakeGenericMethod(types).Invoke(instance, args.ToArray()); } + /// + /// Convert a dictionary to a query string. + /// + /// The list of query parameters. + /// A valid query string with all items in the dictionary. public static string ToQueryString(this Dictionary query) { if (!query.Any()) @@ -432,6 +437,11 @@ namespace Kyoo.Utils return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); } + /// + /// Rethrow the exception without modifying the stack trace. + /// This is similar to the rethrow; code but is useful when the exception is not in a catch block. + /// + /// The exception to rethrow. [System.Diagnostics.CodeAnalysis.DoesNotReturn] public static void ReThrow([NotNull] this Exception ex) { diff --git a/src/Kyoo.Authentication/Controllers/PasswordUtils.cs b/src/Kyoo.Authentication/Controllers/PasswordUtils.cs index fb2982ba..5c071a4e 100644 --- a/src/Kyoo.Authentication/Controllers/PasswordUtils.cs +++ b/src/Kyoo.Authentication/Controllers/PasswordUtils.cs @@ -23,6 +23,9 @@ using IdentityModel; namespace Kyoo.Authentication { + /// + /// Some functions to handle password management. + /// public static class PasswordUtils { /// diff --git a/src/Kyoo.Core/Controllers/ConfigurationManager.cs b/src/Kyoo.Core/Controllers/ConfigurationManager.cs index 760e3a95..10e0843b 100644 --- a/src/Kyoo.Core/Controllers/ConfigurationManager.cs +++ b/src/Kyoo.Core/Controllers/ConfigurationManager.cs @@ -32,6 +32,11 @@ using Newtonsoft.Json.Linq; namespace Kyoo.Core.Controllers { + /// + /// A class to ease configuration management. This work WITH Microsoft's package, you can still use IOptions patterns + /// to access your options, this manager ease dynamic work and editing. + /// It works with . + /// public class ConfigurationManager : IConfigurationManager { /// diff --git a/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs b/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs index 74d3a7c6..44fe5154 100644 --- a/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs +++ b/src/Kyoo.Core/Controllers/FileSystems/LocalFileSystem.cs @@ -165,11 +165,13 @@ namespace Kyoo.Core.Controllers }); } + /// public Task> ExtractInfos(Episode episode, bool reExtract) { return _transcoder.ExtractInfos(episode, reExtract); } + /// public IActionResult Transmux(Episode episode) { return _transcoder.Transmux(episode); diff --git a/src/Kyoo.Core/Controllers/LibraryManager.cs b/src/Kyoo.Core/Controllers/LibraryManager.cs index 5243ee36..d07a5da3 100644 --- a/src/Kyoo.Core/Controllers/LibraryManager.cs +++ b/src/Kyoo.Core/Controllers/LibraryManager.cs @@ -29,6 +29,9 @@ using Kyoo.Utils; namespace Kyoo.Core.Controllers { + /// + /// An class to interact with the database. Every repository is mapped through here. + /// public class LibraryManager : ILibraryManager { /// diff --git a/src/Kyoo.Core/Controllers/PassthroughPermissionValidator.cs b/src/Kyoo.Core/Controllers/PassthroughPermissionValidator.cs index 97b73ec2..2c5ae6aa 100644 --- a/src/Kyoo.Core/Controllers/PassthroughPermissionValidator.cs +++ b/src/Kyoo.Core/Controllers/PassthroughPermissionValidator.cs @@ -29,6 +29,10 @@ namespace Kyoo.Core.Controllers /// public class PassthroughPermissionValidator : IPermissionValidator { + /// + /// Create a new . + /// + /// The logger used to warn that no real permission validator exists. [SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor", Justification = "ILogger should include the typeparam for context.")] public PassthroughPermissionValidator(ILogger logger) diff --git a/src/Kyoo.Core/Helper.cs b/src/Kyoo.Core/Helper.cs index 42f767b7..ad45835d 100644 --- a/src/Kyoo.Core/Helper.cs +++ b/src/Kyoo.Core/Helper.cs @@ -22,6 +22,9 @@ using Newtonsoft.Json; namespace Kyoo.Core { + /// + /// A class containing helper methods. + /// public static class Helper { /// diff --git a/src/Kyoo.Core/Views/Helper/ApiHelper.cs b/src/Kyoo.Core/Views/Helper/ApiHelper.cs index 7365c5a7..a1e2c617 100644 --- a/src/Kyoo.Core/Views/Helper/ApiHelper.cs +++ b/src/Kyoo.Core/Views/Helper/ApiHelper.cs @@ -22,24 +22,53 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using JetBrains.Annotations; using Kyoo.Abstractions.Models; namespace Kyoo.Core.Api { + /// + /// A static class containing methods to parse the where query string. + /// public static class ApiHelper { - public static Expression StringCompatibleExpression(Func operand, - Expression left, - Expression right) + /// + /// Make an expression (like + /// + /// compatible with strings). If the expressions are not strings, the given is + /// constructed but if the expressions are strings, this method make the compatible with + /// strings. + /// + /// + /// The expression to make compatible. It should be something like + /// or + /// . + /// + /// The first parameter to compare. + /// The second parameter to compare. + /// A comparison expression compatible with strings + public static BinaryExpression StringCompatibleExpression( + [NotNull] Func operand, + [NotNull] Expression left, + [NotNull] Expression right) { - if (left is MemberExpression member && ((PropertyInfo)member.Member).PropertyType == typeof(string)) - { - MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right); - return operand(call, Expression.Constant(0)); - } - return operand(left, right); + if (left is not MemberExpression member || ((PropertyInfo)member.Member).PropertyType != typeof(string)) + return operand(left, right); + MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right); + return operand(call, Expression.Constant(0)); } + /// + /// Parse a where query for the given . Items can be filtered by any property + /// of the given type. + /// + /// The list of filters. + /// + /// A custom expression to initially filter a collection. It will be combined with the parsed expression. + /// + /// The type to create filters for. + /// A filter is invalid. + /// An expression representing the filters that can be used anywhere or compiled public static Expression> ParseWhere(Dictionary where, Expression> defaultWhere = null) { @@ -96,18 +125,17 @@ namespace Kyoo.Core.Api "not" when valueExpr == null => _ResourceEqual(propertyExpr, value, true), "eq" => Expression.Equal(propertyExpr, valueExpr), - "not" => Expression.NotEqual(propertyExpr, valueExpr!), - "lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr), - "lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr), - "gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr), - "gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr), + "not" => Expression.NotEqual(propertyExpr, valueExpr), + "lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr!), + "lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr!), + "gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr!), + "gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr!), _ => throw new ArgumentException($"Invalid operand: {operand}") }; - if (expression != null) - expression = Expression.AndAlso(expression, condition); - else - expression = condition; + expression = expression != null + ? Expression.AndAlso(expression, condition) + : condition; } return Expression.Lambda>(expression!, param); diff --git a/src/Kyoo.Core/Views/Helper/BaseApi.cs b/src/Kyoo.Core/Views/Helper/BaseApi.cs index 315e92fd..0629faa5 100644 --- a/src/Kyoo.Core/Views/Helper/BaseApi.cs +++ b/src/Kyoo.Core/Views/Helper/BaseApi.cs @@ -27,7 +27,10 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Core.Api { - public class BaseApi : ControllerBase + /// + /// A common API containing custom methods to help inheritors. + /// + public abstract class BaseApi : ControllerBase { /// /// Construct and return a page from an api. diff --git a/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs b/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs index abf215b7..6cc70187 100644 --- a/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs +++ b/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs @@ -32,8 +32,13 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Core.Api { + /// + /// An attribute to put on most controllers. It handle fields loading (only retuning fields requested and if they + /// are requested, load them) and help for the where query parameter. + /// public class ResourceViewAttribute : ActionFilterAttribute { + /// public override void OnActionExecuting(ActionExecutingContext context) { if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary where) @@ -92,6 +97,7 @@ namespace Kyoo.Core.Api base.OnActionExecuting(context); } + /// public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { if (context.Result is ObjectResult result) diff --git a/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs b/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs index 447efccd..f4db9075 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/PeopleRoleConverter.cs @@ -24,8 +24,13 @@ using Newtonsoft.Json.Linq; namespace Kyoo.Core.Api { + /// + /// A custom role's convertor to inline the person or the show depending on the value of + /// . + /// public class PeopleRoleConverter : JsonConverter { + /// public override void WriteJson(JsonWriter writer, PeopleRole value, JsonSerializer serializer) { ICollection oldPeople = value.Show?.People; @@ -46,6 +51,7 @@ namespace Kyoo.Core.Api value.People.Roles = oldRoles; } + /// public override PeopleRole ReadJson(JsonReader reader, Type objectType, PeopleRole existingValue, diff --git a/src/Kyoo.Core/Views/Watch/VideoApi.cs b/src/Kyoo.Core/Views/Watch/VideoApi.cs index 03d5ed31..40f8c881 100644 --- a/src/Kyoo.Core/Views/Watch/VideoApi.cs +++ b/src/Kyoo.Core/Views/Watch/VideoApi.cs @@ -41,9 +41,21 @@ namespace Kyoo.Core.Api [ApiDefinition("Videos", Group = WatchGroup)] public class VideoApi : Controller { + /// + /// The library manager used to modify or retrieve information in the data store. + /// private readonly ILibraryManager _libraryManager; + + /// + /// The file system used to send video files. + /// private readonly IFileSystem _files; + /// + /// Create a new . + /// + /// The library manager used to retrieve episodes. + /// The file manager used to send video files. public VideoApi(ILibraryManager libraryManager, IFileSystem files) { diff --git a/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.cs b/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.cs index 47e1e42e..0724cfd8 100644 --- a/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.cs +++ b/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.cs @@ -24,8 +24,12 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { + /// + /// The initial migration that build most of the database. + /// public partial class Initial : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AlterDatabase() @@ -783,6 +787,7 @@ namespace Kyoo.Postgresql.Migrations column: "episode_id"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.cs b/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.cs index c1723610..19cf8ce5 100644 --- a/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.cs +++ b/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.cs @@ -20,8 +20,12 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Kyoo.Postgresql.Migrations { + /// + /// A migration that adds postgres triggers to update slugs. + /// public partial class Triggers : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { // language=PostgreSQL @@ -42,7 +46,7 @@ namespace Kyoo.Postgresql.Migrations // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF season_number, show_id ON seasons + CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF season_number, show_id ON seasons FOR EACH ROW EXECUTE PROCEDURE season_slug_update();"); // language=PostgreSQL @@ -66,7 +70,7 @@ namespace Kyoo.Postgresql.Migrations // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER episode_slug_trigger + CREATE TRIGGER episode_slug_trigger BEFORE INSERT OR UPDATE OF absolute_number, episode_number, season_number, show_id ON episodes FOR EACH ROW EXECUTE PROCEDURE episode_slug_update();"); @@ -80,7 +84,7 @@ namespace Kyoo.Postgresql.Migrations UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id; UPDATE episodes SET slug = CASE WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug - WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number) + WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number) ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) END WHERE show_id = NEW.id; RETURN NEW; @@ -128,7 +132,7 @@ namespace Kyoo.Postgresql.Migrations BEGIN IF NEW.track_index = 0 THEN NEW.track_index := (SELECT COUNT(*) FROM tracks - WHERE episode_id = NEW.episode_id AND type = NEW.type + WHERE episode_id = NEW.episode_id AND type = NEW.type AND language = NEW.language AND is_forced = NEW.is_forced); END IF; NEW.slug := CONCAT( @@ -149,7 +153,7 @@ namespace Kyoo.Postgresql.Migrations $$;"); // language=PostgreSQL migrationBuilder.Sql(@" - CREATE TRIGGER track_slug_trigger + CREATE TRIGGER track_slug_trigger BEFORE INSERT OR UPDATE OF episode_id, is_forced, language, track_index, type ON tracks FOR EACH ROW EXECUTE PROCEDURE track_slug_update();"); @@ -167,11 +171,12 @@ namespace Kyoo.Postgresql.Migrations INNER JOIN collections AS c ON l.collection_id = c.id WHERE s.id = l.show_id)) UNION ALL - SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status, + SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status, NULL AS start_air, NULL AS end_air, c0.images, 'collection'::item_type AS type FROM collections AS c0"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { // language=PostgreSQL diff --git a/src/Kyoo.SqLite/Migrations/20210801171534_Initial.cs b/src/Kyoo.SqLite/Migrations/20210801171534_Initial.cs index ee965602..985f0409 100644 --- a/src/Kyoo.SqLite/Migrations/20210801171534_Initial.cs +++ b/src/Kyoo.SqLite/Migrations/20210801171534_Initial.cs @@ -21,8 +21,12 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Kyoo.SqLite.Migrations { + /// + /// The initial migration that build most of the database. + /// public partial class Initial : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( @@ -775,6 +779,7 @@ namespace Kyoo.SqLite.Migrations column: "EpisodeID"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.cs b/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.cs index bbb1cc92..10f184c7 100644 --- a/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.cs +++ b/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.cs @@ -20,31 +20,35 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Kyoo.SqLite.Migrations { + /// + /// A migration that adds sqlite triggers to update slugs. + /// public partial class Triggers : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW - BEGIN + CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW + BEGIN UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber WHERE ID == new.ID; END"); // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW - BEGIN + CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW + BEGIN UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber WHERE ID == new.ID; END"); // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW - BEGIN - UPDATE Episodes - SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || CASE WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber @@ -54,11 +58,11 @@ namespace Kyoo.SqLite.Migrations END"); // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID - ON Episodes FOR EACH ROW - BEGIN - UPDATE Episodes - SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID + ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || CASE WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber @@ -69,7 +73,7 @@ namespace Kyoo.SqLite.Migrations // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER TrackSlugInsert + CREATE TRIGGER TrackSlugInsert AFTER INSERT ON Tracks FOR EACH ROW BEGIN @@ -98,7 +102,7 @@ namespace Kyoo.SqLite.Migrations END;"); // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER TrackSlugUpdate + CREATE TRIGGER TrackSlugUpdate AFTER UPDATE OF EpisodeID, IsForced, Language, TrackIndex, Type ON Tracks FOR EACH ROW BEGIN @@ -107,7 +111,7 @@ namespace Kyoo.SqLite.Migrations WHERE EpisodeID = new.EpisodeID AND Type = new.Type AND Language = new.Language AND IsForced = new.IsForced ) WHERE ID = new.ID AND TrackIndex = 0; - UPDATE Tracks SET Slug = + UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) || '.' || Language || CASE (TrackIndex) @@ -128,7 +132,7 @@ namespace Kyoo.SqLite.Migrations END;"); // language=SQLite migrationBuilder.Sql(@" - CREATE TRIGGER EpisodeUpdateTracksSlug + CREATE TRIGGER EpisodeUpdateTracksSlug AFTER UPDATE OF Slug ON Episodes FOR EACH ROW BEGIN @@ -157,8 +161,8 @@ namespace Kyoo.SqLite.Migrations CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW BEGIN UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; - UPDATE Episodes - SET Slug = new.Slug || + UPDATE Episodes + SET Slug = new.Slug || CASE WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber @@ -181,11 +185,12 @@ namespace Kyoo.SqLite.Migrations INNER JOIN Collections AS c ON l.CollectionID = c.ID WHERE s.ID = l.ShowID)) UNION ALL - SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 0 AS Status, + SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 0 AS Status, NULL AS StartAir, NULL AS EndAir, c0.Images, 2 AS Type FROM collections AS c0"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { // language=SQLite From 1cd88a8bfe3d39667134d500535c15a8fd47d648 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 9 Oct 2021 13:13:10 +0200 Subject: [PATCH 32/36] JsonSerializer: Cleaning up fields handling --- src/Kyoo.Core/CoreModule.cs | 26 ++----- .../Views/Helper/ResourceViewAttribute.cs | 18 ++--- .../Views/Helper/Serializers/JsonOptions.cs | 54 +++++++++++++++ ...tyIgnorer.cs => JsonSerializerContract.cs} | 67 +++++++++---------- tests/Kyoo.Tests/Kyoo.Tests.csproj | 5 -- 5 files changed, 97 insertions(+), 73 deletions(-) create mode 100644 src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs rename src/Kyoo.Core/Views/Helper/Serializers/{JsonPropertyIgnorer.cs => JsonSerializerContract.cs} (52%) diff --git a/src/Kyoo.Core/CoreModule.cs b/src/Kyoo.Core/CoreModule.cs index f601ad25..dd5f2e39 100644 --- a/src/Kyoo.Core/CoreModule.cs +++ b/src/Kyoo.Core/CoreModule.cs @@ -26,7 +26,6 @@ using Autofac.Extras.AttributeMetadata; using Kyoo.Abstractions; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Utils; -using Kyoo.Core.Api; using Kyoo.Core.Controllers; using Kyoo.Core.Models.Options; using Kyoo.Core.Tasks; @@ -35,11 +34,12 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.StaticFiles; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Serilog; using IMetadataProvider = Kyoo.Abstractions.Controllers.IMetadataProvider; +using JsonOptions = Kyoo.Core.Api.JsonOptions; namespace Kyoo.Core { @@ -67,20 +67,6 @@ namespace Kyoo.Core { "logging", null } }; - /// - /// The configuration to use. - /// - private readonly IConfiguration _configuration; - - /// - /// Create a new core module instance and use the given configuration. - /// - /// The configuration to use - public CoreModule(IConfiguration configuration) - { - _configuration = configuration; - } - /// public void Configure(ContainerBuilder builder) { @@ -140,17 +126,13 @@ namespace Kyoo.Core /// public void Configure(IServiceCollection services) { - Uri publicUrl = _configuration.GetPublicUrl(); + services.AddTransient, JsonOptions>(); services.AddMvcCore() + .AddNewtonsoftJson() .AddDataAnnotations() .AddControllersAsServices() .AddApiExplorer() - .AddNewtonsoftJson(x => - { - x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); - x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); - }) .ConfigureApiBehaviorOptions(options => { options.SuppressMapClientErrors = true; diff --git a/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs b/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs index 6cc70187..3bb12b3a 100644 --- a/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs +++ b/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs @@ -24,6 +24,7 @@ using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Utils; using Kyoo.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -64,13 +65,13 @@ namespace Kyoo.Core.Api type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type; type = Utility.GetGenericDefinition(type, typeof(Page<>))?.GetGenericArguments()[0] ?? type; + context.HttpContext.Items["ResourceType"] = type.Name; + PropertyInfo[] properties = type.GetProperties() .Where(x => x.GetCustomAttribute() != null) .ToArray(); if (fields.Count == 1 && fields.Contains("all")) - { fields = properties.Select(x => x.Name).ToList(); - } else { fields = fields @@ -82,10 +83,9 @@ namespace Kyoo.Core.Api ?.Name; if (property != null) return property; - context.Result = new BadRequestObjectResult(new - { - Error = $"{x} does not exist on {type.Name}." - }); + context.Result = new BadRequestObjectResult( + new RequestError($"{x} does not exist on {type.Name}.") + ); return null; }) .ToList(); @@ -110,7 +110,7 @@ namespace Kyoo.Core.Api if (result.DeclaredType == null) return; - ILibraryManager library = context.HttpContext.RequestServices.GetService(); + ILibraryManager library = context.HttpContext.RequestServices.GetRequiredService(); ICollection fields = (ICollection)context.HttpContext.Items["fields"]; Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>)); @@ -119,13 +119,13 @@ namespace Kyoo.Core.Api foreach (IResource resource in ((dynamic)result.Value).Items) { foreach (string field in fields!) - await library!.Load(resource, field); + await library.Load(resource, field); } } else if (result.DeclaredType.IsAssignableTo(typeof(IResource))) { foreach (string field in fields!) - await library!.Load((IResource)result.Value, field); + await library.Load((IResource)result.Value, field); } } } diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs new file mode 100644 index 00000000..c272e942 --- /dev/null +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs @@ -0,0 +1,54 @@ +// 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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Kyoo.Core.Api +{ + /// + /// The custom options of newtonsoft json. This simply add the and set + /// the . It is on a separate class to use dependency injection. + /// + public class JsonOptions : IConfigureOptions + { + /// + /// The http context accessor given to the . + /// + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Create a new . + /// + /// + /// The http context accessor given to the . + /// + public JsonOptions(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + /// + public void Configure(MvcNewtonsoftJsonOptions options) + { + options.SerializerSettings.ContractResolver = new JsonSerializerContract(_httpContextAccessor); + options.SerializerSettings.Converters.Add(new PeopleRoleConverter()); + } + } +} diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs similarity index 52% rename from src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs rename to src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index 40a682a9..0fef35fc 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -16,27 +16,39 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; -using System.Collections; +using System.Collections.Generic; using System.Reflection; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; -using Kyoo.Utils; +using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace Kyoo.Core.Api { - public class JsonPropertyIgnorer : CamelCasePropertyNamesContractResolver + /// + /// A custom json serializer that respects and + /// . It also handle via the + /// fields query parameter and items. + /// + public class JsonSerializerContract : CamelCasePropertyNamesContractResolver { - private readonly Uri _host; - private int _depth = -1; + /// + /// The http context accessor used to retrieve the fields query parameter as well as the type of + /// resource currently serializing. + /// + private readonly IHttpContextAccessor _httpContextAccessor; - public JsonPropertyIgnorer(Uri host) + /// + /// Create a new . + /// + /// The http context accessor to use. + public JsonSerializerContract(IHttpContextAccessor httpContextAccessor) { - _host = host; + _httpContextAccessor = httpContextAccessor; } + /// protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { JsonProperty property = base.CreateProperty(member, memberSerialization); @@ -44,19 +56,14 @@ namespace Kyoo.Core.Api LoadableRelationAttribute relation = member.GetCustomAttribute(); if (relation != null) { - if (relation.RelationID == null) - property.ShouldSerialize = x => _depth == 0 && member.GetValue(x) != null; - else + property.ShouldSerialize = _ => { - property.ShouldSerialize = x => - { - if (_depth != 0) - return false; - if (member.GetValue(x) != null) - return true; - return x.GetType().GetProperty(relation.RelationID)?.GetValue(x) != null; - }; - } + string resType = (string)_httpContextAccessor.HttpContext!.Items["ResourceType"]; + if (member.DeclaringType!.Name != resType) + return false; + ICollection fields = (ICollection)_httpContextAccessor.HttpContext!.Items["fields"]; + return fields!.Contains(member.Name); + }; } if (member.GetCustomAttribute() != null) @@ -66,24 +73,10 @@ namespace Kyoo.Core.Api // TODO use http context to disable serialize as. // TODO check https://stackoverflow.com/questions/53288633/net-core-api-custom-json-resolver-based-on-request-values - SerializeAsAttribute serializeAs = member.GetCustomAttribute(); - if (serializeAs != null) - property.ValueProvider = new SerializeAsProvider(serializeAs.Format, _host); + // SerializeAsAttribute serializeAs = member.GetCustomAttribute(); + // if (serializeAs != null) + // property.ValueProvider = new SerializeAsProvider(serializeAs.Format, _host); return property; } - - protected override JsonContract CreateContract(Type objectType) - { - JsonContract contract = base.CreateContract(objectType); - if (Utility.GetGenericDefinition(objectType, typeof(Page<>)) == null - && !objectType.IsAssignableTo(typeof(IEnumerable)) - && objectType.Name != "AnnotatedProblemDetails") - { - contract.OnSerializingCallbacks.Add((_, _) => _depth++); - contract.OnSerializedCallbacks.Add((_, _) => _depth--); - } - - return contract; - } } } diff --git a/tests/Kyoo.Tests/Kyoo.Tests.csproj b/tests/Kyoo.Tests/Kyoo.Tests.csproj index 330c41f7..c02609c5 100644 --- a/tests/Kyoo.Tests/Kyoo.Tests.csproj +++ b/tests/Kyoo.Tests/Kyoo.Tests.csproj @@ -1,12 +1,8 @@ - net5.0 default false - - Zoe Roux - SDG @@ -37,5 +33,4 @@ - From 96494ecf28d7584be4114d08b8da8c7c9d53d3f9 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 9 Oct 2021 21:36:23 +0200 Subject: [PATCH 33/36] Serializer: Reworking images serialization --- .../Serializer/SerializeAsAttribute.cs | 51 ------------ src/Kyoo.Abstractions/Models/LibraryItem.cs | 9 --- .../Models/Resources/Collection.cs | 10 --- .../Models/Resources/Episode.cs | 9 --- .../Resources/Interfaces/IThumbnails.cs | 14 +++- .../Models/Resources/People.cs | 10 --- .../Models/Resources/Provider.cs | 10 --- .../Models/Resources/Season.cs | 9 --- .../Models/Resources/Show.cs | 27 ------- src/Kyoo.Abstractions/Models/WatchItem.cs | 21 ----- src/Kyoo.Core/Views/Helper/ApiHelper.cs | 2 +- .../Serializers/JsonSerializerContract.cs | 74 ++++++++++++++++-- .../Helper/Serializers/SerializeAsProvider.cs | 77 ------------------- .../20210801171613_Initial.Designer.cs | 3 +- .../20210801171641_Triggers.Designer.cs | 1 + .../20210801171534_Initial.Designer.cs | 1 + .../20210801171544_Triggers.Designer.cs | 1 + 17 files changed, 86 insertions(+), 243 deletions(-) delete mode 100644 src/Kyoo.Abstractions/Models/Attributes/Serializer/SerializeAsAttribute.cs delete mode 100644 src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs diff --git a/src/Kyoo.Abstractions/Models/Attributes/Serializer/SerializeAsAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/Serializer/SerializeAsAttribute.cs deleted file mode 100644 index 6e2a8983..00000000 --- a/src/Kyoo.Abstractions/Models/Attributes/Serializer/SerializeAsAttribute.cs +++ /dev/null @@ -1,51 +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; - -namespace Kyoo.Abstractions.Models.Attributes -{ - /// - /// Change the way the field is serialized. It allow one to use a string format like formatting instead of the default value. - /// This can be disabled for a request by setting the "internal" query string parameter to true. - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] - public class SerializeAsAttribute : Attribute - { - /// - /// The format string to use. - /// - public string Format { get; } - - /// - /// Create a new with the selected format. - /// - /// - /// The format string can contains any property within {}. It will be replaced by the actual value of the property. - /// You can also use the special value {HOST} that will put the webhost address. - /// - /// - /// The show's poster serialized uses this format string: {HOST}/api/shows/{Slug}/poster - /// - /// The format to use - public SerializeAsAttribute(string format) - { - Format = format; - } - } -} diff --git a/src/Kyoo.Abstractions/Models/LibraryItem.cs b/src/Kyoo.Abstractions/Models/LibraryItem.cs index c572f6a4..7dfbb4ea 100644 --- a/src/Kyoo.Abstractions/Models/LibraryItem.cs +++ b/src/Kyoo.Abstractions/Models/LibraryItem.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; -using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models { @@ -86,14 +85,6 @@ namespace Kyoo.Abstractions.Models /// public Dictionary Images { get; set; } - /// - /// The path of this item's poster. - /// By default, the http path for this poster is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/{Type:l}/{Slug}/poster")] - public string Poster => Images?.GetValueOrDefault(Models.Images.Poster); - /// /// The type of this item (ether a collection, a show or a movie). /// diff --git a/src/Kyoo.Abstractions/Models/Resources/Collection.cs b/src/Kyoo.Abstractions/Models/Resources/Collection.cs index 5f48fee1..101781f4 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Collection.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Collection.cs @@ -16,7 +16,6 @@ // 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 Kyoo.Abstractions.Models.Attributes; @@ -42,15 +41,6 @@ namespace Kyoo.Abstractions.Models /// public Dictionary Images { get; set; } - /// - /// The path of this poster. - /// By default, the http path for this poster is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/collection/{Slug}/poster")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Poster => Images?.GetValueOrDefault(Models.Images.Poster); - /// /// The description of this collection. /// diff --git a/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/src/Kyoo.Abstractions/Models/Resources/Episode.cs index 83f82b08..42e9ecee 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Episode.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Episode.cs @@ -127,15 +127,6 @@ namespace Kyoo.Abstractions.Models /// public Dictionary Images { get; set; } - /// - /// The path of this episode's thumbnail. - /// By default, the http path for the thumbnail is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/episodes/{Slug}/thumbnail")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Thumb => Images?.GetValueOrDefault(Models.Images.Thumbnail); - /// /// The title of this episode. /// diff --git a/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs index e2d83e78..1fdb34f0 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -34,8 +34,6 @@ namespace Kyoo.Abstractions.Models /// An arbitrary index should not be used, instead use indexes from /// public Dictionary Images { get; set; } - - // TODO remove Posters properties add them via the json serializer for every IThumbnails } /// @@ -63,5 +61,17 @@ namespace Kyoo.Abstractions.Models /// A video of a few minutes that tease the content. /// public const int Trailer = 3; + + /// + /// Retrieve the name of an image using it's ID. It is also used by the serializer to retrieve all named images. + /// If a plugin adds a new image type, it should add it's value and name here to allow the serializer to add it. + /// + public static Dictionary ImageName { get; } = new() + { + [Poster] = nameof(Poster), + [Thumbnail] = nameof(Thumbnail), + [Logo] = nameof(Logo), + [Trailer] = nameof(Trailer) + }; } } diff --git a/src/Kyoo.Abstractions/Models/Resources/People.cs b/src/Kyoo.Abstractions/Models/Resources/People.cs index e1070545..71f9012d 100644 --- a/src/Kyoo.Abstractions/Models/Resources/People.cs +++ b/src/Kyoo.Abstractions/Models/Resources/People.cs @@ -16,7 +16,6 @@ // 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 Kyoo.Abstractions.Models.Attributes; @@ -41,15 +40,6 @@ namespace Kyoo.Abstractions.Models /// public Dictionary Images { get; set; } - /// - /// The path of this poster. - /// By default, the http path for this poster is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/people/{Slug}/poster")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Poster => Images?.GetValueOrDefault(Models.Images.Poster); - /// [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } diff --git a/src/Kyoo.Abstractions/Models/Resources/Provider.cs b/src/Kyoo.Abstractions/Models/Resources/Provider.cs index 74401045..e1c10de3 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Provider.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Provider.cs @@ -16,7 +16,6 @@ // 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 Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; @@ -44,15 +43,6 @@ namespace Kyoo.Abstractions.Models /// public Dictionary Images { get; set; } - /// - /// The path of this provider's logo. - /// By default, the http path for this logo is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/providers/{Slug}/logo")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Logo => Images?.GetValueOrDefault(Models.Images.Logo); - /// /// The list of libraries that uses this provider. /// diff --git a/src/Kyoo.Abstractions/Models/Resources/Season.cs b/src/Kyoo.Abstractions/Models/Resources/Season.cs index f219f483..29d22d44 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Season.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Season.cs @@ -98,15 +98,6 @@ namespace Kyoo.Abstractions.Models /// public Dictionary Images { get; set; } - /// - /// The path of this poster. - /// By default, the http path for this poster is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Poster => Images?.GetValueOrDefault(Models.Images.Poster); - /// [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } diff --git a/src/Kyoo.Abstractions/Models/Resources/Show.cs b/src/Kyoo.Abstractions/Models/Resources/Show.cs index 87e00052..97061446 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -82,33 +82,6 @@ namespace Kyoo.Abstractions.Models /// public Dictionary Images { get; set; } - /// - /// The path of this show's poster. - /// By default, the http path for this poster is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/shows/{Slug}/poster")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Poster => Images?.GetValueOrDefault(Models.Images.Poster); - - /// - /// The path of this show's logo. - /// By default, the http path for this logo is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/shows/{Slug}/logo")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Logo => Images?.GetValueOrDefault(Models.Images.Logo); - - /// - /// The path of this show's backdrop. - /// By default, the http path for this backdrop is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] - [Obsolete("Use Images instead of this, this is only kept for the API response.")] - public string Backdrop => Images?.GetValueOrDefault(Models.Images.Thumbnail); - /// /// True if this show represent a movie, false otherwise. /// diff --git a/src/Kyoo.Abstractions/Models/WatchItem.cs b/src/Kyoo.Abstractions/Models/WatchItem.cs index 5bbf0fb8..3af919b9 100644 --- a/src/Kyoo.Abstractions/Models/WatchItem.cs +++ b/src/Kyoo.Abstractions/Models/WatchItem.cs @@ -101,27 +101,6 @@ namespace Kyoo.Abstractions.Models /// public bool IsMovie { get; set; } - /// - /// The path of this item's poster. - /// By default, the http path for the poster is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; } - - /// - /// The path of this item's logo. - /// By default, the http path for the logo is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; } - - /// - /// The path of this item's backdrop. - /// By default, the http path for the backdrop is returned from the public API. - /// This can be disabled using the internal query flag. - /// - [SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; } - /// /// The container of the video file of this episode. /// Common containers are mp4, mkv, avi and so on. diff --git a/src/Kyoo.Core/Views/Helper/ApiHelper.cs b/src/Kyoo.Core/Views/Helper/ApiHelper.cs index a1e2c617..60adae50 100644 --- a/src/Kyoo.Core/Views/Helper/ApiHelper.cs +++ b/src/Kyoo.Core/Views/Helper/ApiHelper.cs @@ -36,7 +36,7 @@ namespace Kyoo.Core.Api /// Make an expression (like /// /// compatible with strings). If the expressions are not strings, the given is - /// constructed but if the expressions are strings, this method make the compatible with + /// constructed but if the expressions are strings, this method make the compatible with /// strings. /// /// diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index 0fef35fc..0fb6ee72 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -16,6 +16,7 @@ // 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.Reflection; using Kyoo.Abstractions.Models; @@ -70,13 +71,74 @@ namespace Kyoo.Core.Api property.ShouldSerialize = _ => false; if (member.GetCustomAttribute() != null) property.ShouldDeserialize = _ => false; - - // TODO use http context to disable serialize as. - // TODO check https://stackoverflow.com/questions/53288633/net-core-api-custom-json-resolver-based-on-request-values - // SerializeAsAttribute serializeAs = member.GetCustomAttribute(); - // if (serializeAs != null) - // property.ValueProvider = new SerializeAsProvider(serializeAs.Format, _host); return property; } + + /// + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + IList properties = base.CreateProperties(type, memberSerialization); + if (!type.IsAssignableTo(typeof(IThumbnails))) + return properties; + foreach ((int id, string image) in Images.ImageName) + { + properties.Add(new JsonProperty + { + DeclaringType = type, + PropertyName = image.ToLower(), + UnderlyingName = image, + PropertyType = typeof(string), + Readable = true, + Writable = false, + ItemIsReference = false, + TypeNameHandling = TypeNameHandling.None, + ShouldSerialize = x => + { + IThumbnails thumb = (IThumbnails)x; + return thumb.Images?.ContainsKey(id) == true; + }, + ValueProvider = new ThumbnailProvider(id) + }); + } + + return properties; + } + + /// + /// A custom that uses the + /// . as a value. + /// + private class ThumbnailProvider : IValueProvider + { + /// + /// The index/ID of the image to retrieve/set. + /// + private readonly int _imageIndex; + + /// + /// Create a new . + /// + /// The index/ID of the image to retrieve/set. + public ThumbnailProvider(int imageIndex) + { + _imageIndex = imageIndex; + } + + /// + public void SetValue(object target, object value) + { + if (target is not IThumbnails thumb) + throw new ArgumentException($"The given object is not an Thumbnail."); + thumb.Images[_imageIndex] = value as string; + } + + /// + public object GetValue(object target) + { + if (target is IThumbnails thumb) + return thumb.Images?.GetValueOrDefault(_imageIndex); + return null; + } + } } } diff --git a/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs b/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs deleted file mode 100644 index ff104fa7..00000000 --- a/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs +++ /dev/null @@ -1,77 +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.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using Newtonsoft.Json.Serialization; - -namespace Kyoo.Core.Api -{ - public class SerializeAsProvider : IValueProvider - { - private readonly string _format; - private readonly Uri _host; - - public SerializeAsProvider(string format, Uri host) - { - _format = format; - _host = host; - } - - public object GetValue(object target) - { - return Regex.Replace(_format, @"(? - { - string value = x.Groups[1].Value; - string modifier = x.Groups[3].Value; - - if (value == "HOST") - return _host.ToString().TrimEnd('/'); - - PropertyInfo properties = target.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .FirstOrDefault(y => y.Name == value); - if (properties == null) - return null; - object objValue = properties.GetValue(target); - if (objValue is not string ret) - ret = objValue?.ToString(); - if (ret == null) - throw new ArgumentException($"Invalid serializer replacement {value}"); - - foreach (char modification in modifier) - { - ret = modification switch - { - 'l' => ret.ToLowerInvariant(), - 'u' => ret.ToUpperInvariant(), - _ => throw new ArgumentException($"Invalid serializer modificator {modification}.") - }; - } - return ret; - }); - } - - public void SetValue(object target, object value) - { - // Values are ignored and should not be editable, except if the internal value is set. - } - } -} diff --git a/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.Designer.cs b/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.Designer.cs index 0f7db480..02a66b0d 100644 --- a/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.Designer.cs +++ b/src/Kyoo.Postgresql/Migrations/20210801171613_Initial.Designer.cs @@ -15,7 +15,8 @@ namespace Kyoo.Postgresql.Migrations [Migration("20210801171613_Initial")] partial class Initial { - protected override void BuildTargetModel(ModelBuilder modelBuilder) + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder diff --git a/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.Designer.cs b/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.Designer.cs index 51d369e9..de520a04 100644 --- a/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.Designer.cs +++ b/src/Kyoo.Postgresql/Migrations/20210801171641_Triggers.Designer.cs @@ -15,6 +15,7 @@ namespace Kyoo.Postgresql.Migrations [Migration("20210801171641_Triggers")] partial class Triggers { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/src/Kyoo.SqLite/Migrations/20210801171534_Initial.Designer.cs b/src/Kyoo.SqLite/Migrations/20210801171534_Initial.Designer.cs index d01507a7..e912d60a 100644 --- a/src/Kyoo.SqLite/Migrations/20210801171534_Initial.Designer.cs +++ b/src/Kyoo.SqLite/Migrations/20210801171534_Initial.Designer.cs @@ -12,6 +12,7 @@ namespace Kyoo.SqLite.Migrations [Migration("20210801171534_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.Designer.cs b/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.Designer.cs index 0ca18ed6..1576e9e3 100644 --- a/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.Designer.cs +++ b/src/Kyoo.SqLite/Migrations/20210801171544_Triggers.Designer.cs @@ -12,6 +12,7 @@ namespace Kyoo.SqLite.Migrations [Migration("20210801171544_Triggers")] partial class Triggers { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 From 504bd5bca84b39c7d9d61d4332f577c8c29c7932 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 9 Oct 2021 22:32:11 +0200 Subject: [PATCH 34/36] Serialier: Using local url for images --- src/Kyoo.Abstractions/Models/LibraryItem.cs | 12 +++++-- src/Kyoo.Core/Tasks/MetadataProviderLoader.cs | 2 +- .../Views/Helper/Serializers/JsonOptions.cs | 14 ++++++-- .../Serializers/JsonSerializerContract.cs | 34 +++++++++++++++---- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/Kyoo.Abstractions/Models/LibraryItem.cs b/src/Kyoo.Abstractions/Models/LibraryItem.cs index 7dfbb4ea..fd9a5b85 100644 --- a/src/Kyoo.Abstractions/Models/LibraryItem.cs +++ b/src/Kyoo.Abstractions/Models/LibraryItem.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq.Expressions; namespace Kyoo.Abstractions.Models @@ -33,7 +34,8 @@ namespace Kyoo.Abstractions.Models Show, /// - /// The is a Movie (a with equals to true). + /// The is a Movie (a with + /// equals to true). /// Movie, @@ -47,7 +49,7 @@ namespace Kyoo.Abstractions.Models /// A type union between and . /// This is used to list content put inside a library. /// - public class LibraryItem : IResource, IThumbnails + public class LibraryItem : CustomTypeDescriptor, IResource, IThumbnails { /// public int ID { get; set; } @@ -160,5 +162,11 @@ namespace Kyoo.Abstractions.Models Images = x.Images, Type = ItemType.Collection }; + + /// + public override string GetClassName() + { + return Type.ToString(); + } } } diff --git a/src/Kyoo.Core/Tasks/MetadataProviderLoader.cs b/src/Kyoo.Core/Tasks/MetadataProviderLoader.cs index 7c004268..c9160fda 100644 --- a/src/Kyoo.Core/Tasks/MetadataProviderLoader.cs +++ b/src/Kyoo.Core/Tasks/MetadataProviderLoader.cs @@ -72,7 +72,7 @@ namespace Kyoo.Core.Tasks /// public TaskParameters GetParameters() { - return new(); + return new TaskParameters(); } /// diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs index c272e942..0372b568 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.cs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -33,21 +34,30 @@ namespace Kyoo.Core.Api /// private readonly IHttpContextAccessor _httpContextAccessor; + /// + /// The options containing the public URL of kyoo, given to . + /// + private readonly IOptions _options; + /// /// Create a new . /// /// /// The http context accessor given to the . /// - public JsonOptions(IHttpContextAccessor httpContextAccessor) + /// + /// The options containing the public URL of kyoo, given to . + /// + public JsonOptions(IHttpContextAccessor httpContextAccessor, IOptions options) { _httpContextAccessor = httpContextAccessor; + _options = options; } /// public void Configure(MvcNewtonsoftJsonOptions options) { - options.SerializerSettings.ContractResolver = new JsonSerializerContract(_httpContextAccessor); + options.SerializerSettings.ContractResolver = new JsonSerializerContract(_httpContextAccessor, _options); options.SerializerSettings.Converters.Add(new PeopleRoleConverter()); } } diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index 0fb6ee72..5e90ffdf 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -18,10 +18,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Reflection; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -40,13 +43,20 @@ namespace Kyoo.Core.Api /// private readonly IHttpContextAccessor _httpContextAccessor; + /// + /// The options containing the public URL of kyoo. + /// + private readonly IOptions _options; + /// /// Create a new . /// /// The http context accessor to use. - public JsonSerializerContract(IHttpContextAccessor httpContextAccessor) + /// The options containing the public URL of kyoo. + public JsonSerializerContract(IHttpContextAccessor httpContextAccessor, IOptions options) { _httpContextAccessor = httpContextAccessor; + _options = options; } /// @@ -97,7 +107,7 @@ namespace Kyoo.Core.Api IThumbnails thumb = (IThumbnails)x; return thumb.Images?.ContainsKey(id) == true; }, - ValueProvider = new ThumbnailProvider(id) + ValueProvider = new ThumbnailProvider(_options.Value.PublicUrl, id) }); } @@ -110,6 +120,11 @@ namespace Kyoo.Core.Api /// private class ThumbnailProvider : IValueProvider { + /// + /// The public address of kyoo. + /// + private readonly Uri _host; + /// /// The index/ID of the image to retrieve/set. /// @@ -118,9 +133,11 @@ namespace Kyoo.Core.Api /// /// Create a new . /// + /// The public address of kyoo. /// The index/ID of the image to retrieve/set. - public ThumbnailProvider(int imageIndex) + public ThumbnailProvider(Uri host, int imageIndex) { + _host = host; _imageIndex = imageIndex; } @@ -135,9 +152,14 @@ namespace Kyoo.Core.Api /// public object GetValue(object target) { - if (target is IThumbnails thumb) - return thumb.Images?.GetValueOrDefault(_imageIndex); - return null; + if (target is not (IThumbnails thumb and IResource res) + || string.IsNullOrEmpty(thumb.Images?.GetValueOrDefault(_imageIndex))) + return null; + string type = target is ICustomTypeDescriptor descriptor + ? descriptor.GetClassName() + : target.GetType().Name; + return new Uri(_host, $"/api/{type}/{res.Slug}/{Images.ImageName[_imageIndex]}".ToLower()) + .ToString(); } } } From 6a3e48a1d13da332354e64175a87b8b53c02ab91 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 10 Oct 2021 21:25:00 +0200 Subject: [PATCH 35/36] API: Fixing watch items images and previous/next handling --- src/Kyoo.Abstractions/Models/WatchItem.cs | 23 ++++++++++++++++--- .../Serializers/JsonSerializerContract.cs | 6 +++-- src/Kyoo.Core/Views/Watch/SubtitleApi.cs | 5 +++- src/Kyoo.WebApp/Front | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Kyoo.Abstractions/Models/WatchItem.cs b/src/Kyoo.Abstractions/Models/WatchItem.cs index 3af919b9..bedd5df5 100644 --- a/src/Kyoo.Abstractions/Models/WatchItem.cs +++ b/src/Kyoo.Abstractions/Models/WatchItem.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -32,7 +33,7 @@ namespace Kyoo.Abstractions.Models /// Information about tracks and display information that could be used by the player. /// This contains mostly data from an with another form. /// - public class WatchItem + public class WatchItem : CustomTypeDescriptor, IThumbnails { /// /// The ID of the episode associated with this item. @@ -101,6 +102,9 @@ namespace Kyoo.Abstractions.Models /// public bool IsMovie { get; set; } + /// + public Dictionary Images { get; set; } + /// /// The container of the video file of this episode. /// Common containers are mp4, mkv, avi and so on. @@ -147,11 +151,11 @@ namespace Kyoo.Abstractions.Models if (ep.AbsoluteNumber != null) { previous = await library.GetOrDefault( - x => x.ShowID == ep.ShowID && x.AbsoluteNumber <= ep.AbsoluteNumber, + x => x.ShowID == ep.ShowID && x.AbsoluteNumber < ep.AbsoluteNumber, new Sort(x => x.AbsoluteNumber, true) ); next = await library.GetOrDefault( - x => x.ShowID == ep.ShowID && x.AbsoluteNumber >= ep.AbsoluteNumber, + x => x.ShowID == ep.ShowID && x.AbsoluteNumber > ep.AbsoluteNumber, new Sort(x => x.AbsoluteNumber) ); } @@ -195,6 +199,7 @@ namespace Kyoo.Abstractions.Models Title = ep.Title, ReleaseDate = ep.ReleaseDate, Path = ep.Path, + Images = ep.Show.Images, Container = PathIO.GetExtension(ep.Path)![1..], Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), @@ -232,5 +237,17 @@ namespace Kyoo.Abstractions.Models return Array.Empty(); } } + + /// + public override string GetClassName() + { + return nameof(Show); + } + + /// + public override string GetComponentName() + { + return ShowSlug; + } } } diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index 5e90ffdf..429c7c60 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -152,13 +152,15 @@ namespace Kyoo.Core.Api /// public object GetValue(object target) { - if (target is not (IThumbnails thumb and IResource res) + string slug = (target as IResource)?.Slug ?? (target as ICustomTypeDescriptor)?.GetComponentName(); + if (target is not IThumbnails thumb + || slug == null || string.IsNullOrEmpty(thumb.Images?.GetValueOrDefault(_imageIndex))) return null; string type = target is ICustomTypeDescriptor descriptor ? descriptor.GetClassName() : target.GetType().Name; - return new Uri(_host, $"/api/{type}/{res.Slug}/{Images.ImageName[_imageIndex]}".ToLower()) + return new Uri(_host, $"/api/{type}/{slug}/{Images.ImageName[_imageIndex]}".ToLower()) .ToString(); } } diff --git a/src/Kyoo.Core/Views/Watch/SubtitleApi.cs b/src/Kyoo.Core/Views/Watch/SubtitleApi.cs index 41b25063..3266af8d 100644 --- a/src/Kyoo.Core/Views/Watch/SubtitleApi.cs +++ b/src/Kyoo.Core/Views/Watch/SubtitleApi.cs @@ -17,6 +17,7 @@ // along with Kyoo. If not, see . using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -75,11 +76,13 @@ namespace Kyoo.Core.Api /// An optional extension for the subtitle file. /// The subtitle file /// No subtitle exist with the given ID or slug. - [HttpGet("{identifier:id}", Order = AlternativeRoute)] + [HttpGet("{identifier:int}", Order = AlternativeRoute)] [HttpGet("{identifier:id}.{extension}")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("ReSharper", "RouteTemplates.ParameterTypeAndConstraintsMismatch", + Justification = "An indentifier can be constructed with an int.")] public async Task GetSubtitle(Identifier identifier, string extension) { Track subtitle = await identifier.Match( diff --git a/src/Kyoo.WebApp/Front b/src/Kyoo.WebApp/Front index 7bf53b40..846dbcb2 160000 --- a/src/Kyoo.WebApp/Front +++ b/src/Kyoo.WebApp/Front @@ -1 +1 @@ -Subproject commit 7bf53b40080d1d43228f1cdcad510b302ead99ff +Subproject commit 846dbcb22ed29244a2384d240180c821ec18df2b From b57fa17cedfadb1de11960807b334010510a0ac4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 18 Oct 2021 21:15:12 +0200 Subject: [PATCH 36/36] Documenting thumbnails --- .../Resources/Interfaces/IThumbnails.cs | 1 + .../Serializers/JsonSerializerContract.cs | 2 +- .../Views/Resources/CollectionApi.cs | 4 +- src/Kyoo.Swagger/SwaggerModule.cs | 1 + src/Kyoo.Swagger/ThumbnailProcessor.cs | 49 +++++++++++++++++++ src/Kyoo.WebApp/Front | 2 +- 6 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/Kyoo.Swagger/ThumbnailProcessor.cs diff --git a/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs index 1fdb34f0..6a84977f 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -33,6 +33,7 @@ namespace Kyoo.Abstractions.Models /// /// An arbitrary index should not be used, instead use indexes from /// + /// {"0": "example.com/dune/poster"} public Dictionary Images { get; set; } } diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index 429c7c60..b1e58c1b 100644 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -105,7 +105,7 @@ namespace Kyoo.Core.Api ShouldSerialize = x => { IThumbnails thumb = (IThumbnails)x; - return thumb.Images?.ContainsKey(id) == true; + return thumb?.Images?.ContainsKey(id) == true; }, ValueProvider = new ThumbnailProvider(_options.Value.PublicUrl, id) }); diff --git a/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/src/Kyoo.Core/Views/Resources/CollectionApi.cs index c004fc43..3cee88b6 100644 --- a/src/Kyoo.Core/Views/Resources/CollectionApi.cs +++ b/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -91,7 +91,7 @@ namespace Kyoo.Core.Api try { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Collections.Any(identifier.IsSame)), + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Collections)), new Sort(sortBy), new Pagination(limit, afterID) ); @@ -135,7 +135,7 @@ namespace Kyoo.Core.Api try { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Collections.Any(identifier.IsSame)), + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Collections)), new Sort(sortBy), new Pagination(limit, afterID) ); diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index b929ed62..88f1d7e1 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -97,6 +97,7 @@ namespace Kyoo.Swagger x.IsNullableRaw = false; x.Type = JsonObjectType.String | JsonObjectType.Integer; })); + document.SchemaProcessors.Add(new ThumbnailProcessor()); document.AddSecurity("Kyoo", new OpenApiSecurityScheme { diff --git a/src/Kyoo.Swagger/ThumbnailProcessor.cs b/src/Kyoo.Swagger/ThumbnailProcessor.cs new file mode 100644 index 00000000..dba8d682 --- /dev/null +++ b/src/Kyoo.Swagger/ThumbnailProcessor.cs @@ -0,0 +1,49 @@ +// 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 Kyoo.Abstractions.Models; +using NJsonSchema; +using NJsonSchema.Generation; + +namespace Kyoo.Swagger +{ + /// + /// An operation processor to add computed fields of . + /// + public class ThumbnailProcessor : ISchemaProcessor + { + /// + public void Process(SchemaProcessorContext context) + { + if (!context.Type.IsAssignableTo(typeof(IThumbnails))) + return; + foreach ((int _, string imageP) in Images.ImageName) + { + string image = imageP.ToLower()[0] + imageP[1..]; + context.Schema.Properties.Add(image, new JsonSchemaProperty + { + Type = JsonObjectType.String, + IsNullableRaw = true, + Description = $"An url to the {image} of this resource. If this resource does not have an image, " + + $"the link will be null. If the kyoo's instance is not capable of handling this kind of image " + + $"for the specific resource, this field won't be present." + }); + } + } + } +} diff --git a/src/Kyoo.WebApp/Front b/src/Kyoo.WebApp/Front index 846dbcb2..0170abcf 160000 --- a/src/Kyoo.WebApp/Front +++ b/src/Kyoo.WebApp/Front @@ -1 +1 @@ -Subproject commit 846dbcb22ed29244a2384d240180c821ec18df2b +Subproject commit 0170abcffd4da197ccbf1155623311b07777314b