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/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/Kyoo.sln b/Kyoo.sln index faab841f..c240e473 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -23,6 +23,16 @@ 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 +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 @@ -81,5 +91,19 @@ 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 + 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/icons/banner.png b/icons/banner.png new file mode 100644 index 00000000..db5e97cd Binary files /dev/null and b/icons/banner.png differ diff --git a/icons/icon-128x128.png b/icons/icon-128x128.png new file mode 100644 index 00000000..f602aaaa Binary files /dev/null and b/icons/icon-128x128.png differ diff --git a/icons/icon-16x16.png b/icons/icon-16x16.png new file mode 100644 index 00000000..f0ecbd81 Binary files /dev/null and b/icons/icon-16x16.png differ diff --git a/icons/icon-256x256.ico b/icons/icon-256x256.ico new file mode 100644 index 00000000..8a6ef232 Binary files /dev/null and b/icons/icon-256x256.ico differ diff --git a/icons/icon-256x256.png b/icons/icon-256x256.png new file mode 100644 index 00000000..a395f0fe Binary files /dev/null and b/icons/icon-256x256.png differ diff --git a/icons/icon-32x32.png b/icons/icon-32x32.png new file mode 100644 index 00000000..b5944473 Binary files /dev/null and b/icons/icon-32x32.png differ diff --git a/icons/icon-64x64.png b/icons/icon-64x64.png new file mode 100644 index 00000000..ad7d28e9 Binary files /dev/null and b/icons/icon-64x64.png differ diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b9d5b35c..b2300460 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,4 +1,24 @@ + + 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 + + $(MSBuildThisFileDirectory)../icons/icon-256x256.ico + + true true @@ -11,6 +31,10 @@ true + + + + @@ -21,7 +45,6 @@ true - CS1591;SA1600;SA1601 true $(MSBuildThisFileDirectory)../Kyoo.ruleset 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/ILibraryManager.cs b/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs index f4218760..3b1a1b7f 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; /// @@ -317,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, @@ -330,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, @@ -344,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, @@ -357,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, @@ -371,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, @@ -384,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, @@ -398,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, @@ -411,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, @@ -425,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, @@ -438,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, @@ -452,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, @@ -465,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 805de8ec..1d996e97 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. @@ -179,7 +180,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); } @@ -264,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. /// @@ -343,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, @@ -356,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, @@ -370,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, @@ -383,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, @@ -418,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, @@ -431,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, @@ -445,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, @@ -458,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, @@ -472,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, @@ -485,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, @@ -499,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, @@ -512,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.Abstractions/Kyoo.Abstractions.csproj b/src/Kyoo.Abstractions/Kyoo.Abstractions.csproj index 60af7d84..3bc50076 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 @@ -24,8 +13,6 @@ - - diff --git a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs new file mode 100644 index 00000000..cd946714 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs @@ -0,0 +1,55 @@ +// 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. + /// 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 | AttributeTargets.Method)] + 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. 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; } + + /// + /// 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/Attributes/Permission/PartialPermissionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/Permission/PartialPermissionAttribute.cs index 58e6366a..bac1edec 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. /// @@ -49,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.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/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/Models/LibraryItem.cs b/src/Kyoo.Abstractions/Models/LibraryItem.cs index c572f6a4..fd9a5b85 100644 --- a/src/Kyoo.Abstractions/Models/LibraryItem.cs +++ b/src/Kyoo.Abstractions/Models/LibraryItem.cs @@ -18,8 +18,8 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq.Expressions; -using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models { @@ -34,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, @@ -48,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; } @@ -86,14 +87,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). /// @@ -169,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.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..6a84977f 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -33,9 +33,8 @@ 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; } - - // TODO remove Posters properties add them via the json serializer for every IThumbnails } /// @@ -63,5 +62,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/Resources/Track.cs b/src/Kyoo.Abstractions/Models/Resources/Track.cs index e6d5d7e1..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 @@ -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 new file mode 100644 index 00000000..7f857df1 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -0,0 +1,55 @@ +// 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.Attributes; + +namespace Kyoo.Abstractions.Models.Utils +{ + /// + /// A class containing constant numbers. + /// + public static class Constants + { + /// + /// 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. + /// + public const int AlternativeRoute = 1; + + /// + /// A group name for . It should be used for main resources of kyoo. + /// + 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 = "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.Abstractions/Models/Utils/Identifier.cs b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs new file mode 100644 index 00000000..7ded5348 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -0,0 +1,215 @@ +// 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.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +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); + } + + /// + /// 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); + ICollection parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; + return Expression.Lambda>(equal, parameters); + } + + /// + /// 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); + ICollection parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; + return Expression.Lambda>(equal, parameters); + } + + /// + /// 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 + ); + } + + /// + /// 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.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 + { + /// + 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.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.Abstractions/Models/Utils/RequestError.cs b/src/Kyoo.Abstractions/Models/Utils/RequestError.cs new file mode 100644 index 00000000..fa40fc64 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Utils/RequestError.cs @@ -0,0 +1,58 @@ +// 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. + /// + /// ["InvalidFilter: no field 'startYear' on a collection"] + [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.Abstractions/Models/WatchItem.cs b/src/Kyoo.Abstractions/Models/WatchItem.cs index 8c4daab9..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,26 +102,8 @@ 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; } + /// + public Dictionary Images { get; set; } /// /// The container of the video file of this episode. @@ -158,36 +141,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 @@ -202,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(), @@ -239,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.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.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/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.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.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.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 af569655..a2cbf2f2 100644 --- a/src/Kyoo.Authentication/Views/AccountApi.cs +++ b/src/Kyoo.Authentication/Views/AccountApi.cs @@ -28,7 +28,9 @@ 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; using Kyoo.Authentication.Models.DTO; using Microsoft.AspNetCore.Authentication; @@ -36,15 +38,19 @@ 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. + /// Documentation of this endpoint is a work in progress. /// - [Route("api/account")] + /// TODO document this well. [Route("api/accounts")] + [Route("api/account", Order = AlternativeRoute)] [ApiController] + [ApiDefinition("Account")] public class AccountApi : Controller, IProfileService { /// @@ -78,12 +84,17 @@ 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")] - public async Task Register([FromBody] RegisterRequest request) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))] + public async Task> Register([FromBody] RegisterRequest request) { User user = request.ToUser(); user.Permissions = _options.Value.Permissions.NewUser; @@ -96,10 +107,10 @@ 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"] }); + return Ok(new OtacResponse(user.ExtraData["otac"])); } /// @@ -119,8 +130,11 @@ namespace Kyoo.Authentication.Views } /// - /// Login the user. + /// Login /// + /// + /// Login the current session. + /// /// The DTO login request /// TODO [HttpPost("login")] @@ -177,6 +191,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 +202,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/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/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/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..44fe5154 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,17 @@ 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.Abstractions/Controllers/ITranscoder.cs b/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs similarity index 60% rename from src/Kyoo.Abstractions/Controllers/ITranscoder.cs rename to src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs index 20213526..cda79146 100644 --- a/src/Kyoo.Abstractions/Controllers/ITranscoder.cs +++ b/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs @@ -16,17 +16,25 @@ // 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; +using Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; -namespace Kyoo.Abstractions.Controllers +namespace Kyoo.Core.Controllers { - public interface ITranscoder + /// + /// The route constraint that goes with the . + /// + public class IdentifierRouteConstraint : IRouteConstraint { - Task ExtractInfos(Episode episode, bool reextract); - - Task Transmux(Episode episode); - - Task Transcode(Episode episode); + /// + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + return values.ContainsKey(routeKey); + } } } diff --git a/src/Kyoo.Core/Controllers/LibraryManager.cs b/src/Kyoo.Core/Controllers/LibraryManager.cs index a6cd9f44..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 { /// @@ -163,10 +166,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/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/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/Controllers/Repositories/LocalRepository.cs b/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index efc017d7..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); } /// @@ -179,9 +182,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/Controllers/Transcoder.cs b/src/Kyoo.Core/Controllers/Transcoder.cs index a29966ba..42135a86 100644 --- a/src/Kyoo.Core/Controllers/Transcoder.cs +++ b/src/Kyoo.Core/Controllers/Transcoder.cs @@ -17,34 +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 - 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('\\', '/'); @@ -52,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; @@ -98,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/CoreModule.cs b/src/Kyoo.Core/CoreModule.cs index fa7f9819..dd5f2e39 100644 --- a/src/Kyoo.Core/CoreModule.cs +++ b/src/Kyoo.Core/CoreModule.cs @@ -18,24 +18,28 @@ 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.Core.Api; +using Kyoo.Abstractions.Models.Utils; 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.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 { @@ -63,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) { @@ -136,16 +126,31 @@ namespace Kyoo.Core /// public void Configure(IServiceCollection services) { - string publicUrl = _configuration.GetPublicUrl(); + services.AddTransient, JsonOptions>(); - services.AddMvc().AddControllersAsServices(); - services.AddControllers() - .AddNewtonsoftJson(x => + services.AddMvcCore() + .AddNewtonsoftJson() + .AddDataAnnotations() + .AddControllersAsServices() + .AddApiExplorer() + .ConfigureApiBehaviorOptions(options => { - x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); - x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); + 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.Configure(x => + { + x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint)); + }); + services.AddResponseCompression(x => { x.EnableForHttps = true; 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/Kyoo.Core.csproj b/src/Kyoo.Core/Kyoo.Core.csproj index 75db8e82..03e8785e 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/ @@ -41,6 +38,7 @@ + @@ -62,6 +60,7 @@ + diff --git a/src/Kyoo.Core/PluginsStartup.cs b/src/Kyoo.Core/PluginsStartup.cs index 1a554a8b..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; @@ -28,6 +29,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 +77,8 @@ namespace Kyoo.Core typeof(PostgresModule), typeof(SqLiteModule), typeof(PluginTvdb), - typeof(PluginTmdb) + typeof(PluginTmdb), + typeof(SwaggerModule) ); } @@ -106,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.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/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/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 diff --git a/src/Kyoo.Core/Views/Admin/TaskApi.cs b/src/Kyoo.Core/Views/Admin/TaskApi.cs new file mode 100644 index 00000000..dc338c24 --- /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("Task", 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/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs deleted file mode 100644 index db4ac5bc..00000000 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ /dev/null @@ -1,201 +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.Exceptions; -using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/collection")] - [Route("api/collections")] - [ApiController] - [PartialPermission(nameof(CollectionApi))] - public class CollectionApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _files; - private readonly IThumbnailsManager _thumbs; - - public CollectionApi(ILibraryManager libraryManager, - IFileSystem files, - IThumbnailsManager thumbs, - IOptions options) - : base(libraryManager.CollectionRepository, options.Value.PublicUrl) - { - _libraryManager = libraryManager; - _files = files; - _thumbs = thumbs; - } - - [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 = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Collections.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 = 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("{id:int}/library")] - [HttpGet("{id:int}/libraries")] - [PartialPermission(Kind.Read)] - public async Task>> GetLibraries(int id, - [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.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}/library")] - [HttpGet("{slug}/libraries")] - [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(); - } - } - } -} diff --git a/src/Kyoo.Core/Views/EpisodeApi.cs b/src/Kyoo.Core/Views/EpisodeApi.cs deleted file mode 100644 index 37591b99..00000000 --- a/src/Kyoo.Core/Views/EpisodeApi.cs +++ /dev/null @@ -1,238 +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.Exceptions; -using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/episode")] - [Route("api/episodes")] - [ApiController] - [PartialPermission(nameof(EpisodeApi))] - public class EpisodeApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - private readonly IThumbnailsManager _thumbnails; - private readonly IFileSystem _files; - - public EpisodeApi(ILibraryManager libraryManager, - IOptions options, - IFileSystem files, - IThumbnailsManager thumbnails) - : base(libraryManager.EpisodeRepository, options.Value.PublicUrl) - { - _libraryManager = libraryManager; - _files = files; - _thumbnails = thumbnails; - } - - [HttpGet("{episodeID:int}/show")] - [PartialPermission(Kind.Read)] - public async Task> GetShow(int episodeID) - { - Show 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}/show")] - [PartialPermission(Kind.Read)] - public async Task> GetShow(string showSlug, int seasonNumber, int episodeNumber) - { - Show ret = await _libraryManager.GetOrDefault(showSlug); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")] - [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, - [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.ID == episodeID), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(episodeID) == 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(); - } - } - } -} diff --git a/src/Kyoo.Core/Views/GenreApi.cs b/src/Kyoo.Core/Views/GenreApi.cs deleted file mode 100644 index 3ec1c7ed..00000000 --- a/src/Kyoo.Core/Views/GenreApi.cs +++ /dev/null @@ -1,98 +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 Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -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, IOptions options) - : base(libraryManager.GenreRepository, options.Value.PublicUrl) - { - _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/Helper/ApiHelper.cs b/src/Kyoo.Core/Views/Helper/ApiHelper.cs index 7365c5a7..60adae50 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 new file mode 100644 index 00000000..0629faa5 --- /dev/null +++ b/src/Kyoo.Core/Views/Helper/BaseApi.cs @@ -0,0 +1,63 @@ +// 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 +{ + /// + /// A common API containing custom methods to help inheritors. + /// + public abstract 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 c621b993..16e8beb1 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -18,126 +18,190 @@ 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.Permissions; +using Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; 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 + public class CrudApi : BaseApi where T : class, IResource { - private readonly IRepository _repository; + /// + /// The repository of the resource, used to retrieve, save and do operations on the baking store. + /// + protected IRepository Repository { get; } - protected Uri BaseURL { get; } - - public CrudApi(IRepository repository, Uri baseURL) + /// + /// Create a new using the given repository and base url. + /// + /// + /// The repository to use as a baking store for the type . + /// + public CrudApi(IRepository repository) { - _repository = repository; - BaseURL = baseURL; + Repository = repository; } - [HttpGet("{id:int}")] + /// + /// Get item + /// + /// + /// Get a specific resource via it's ID or it's slug. + /// + /// The ID or slug of the resource to retrieve. + /// The retrieved resource. + /// A resource with the given ID or slug does not exist. + [HttpGet("{identifier:id}")] [PartialPermission(Kind.Read)] - public virtual async Task> Get(int id) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(Identifier identifier) { - T ret = await _repository.GetOrDefault(id); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{slug}")] - [PartialPermission(Kind.Read)] - public virtual 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; } + /// + /// 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 { - return await _repository.GetCount(ApiHelper.ParseWhere(where)); + return await Repository.GetCount(ApiHelper.ParseWhere(where)); } 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). + /// 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] [PartialPermission(Kind.Read)] - public virtual async Task>> GetAll([FromQuery] string sortBy, - [FromQuery] int afterID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + public async Task>> GetAll( + [FromQuery] string sortBy, [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)); + new Pagination(limit, afterID) + ); return Page(resources, limit); } 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)] - public virtual async Task> Create([FromBody] T resource) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))] + public async Task> Create([FromBody] T resource) { try { - return await _repository.Create(resource); + return await Repository.Create(resource); } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); + return BadRequest(new RequestError(ex.Message)); } catch (DuplicatedItemException) { - T existing = await _repository.GetOrDefault(resource.Slug); + T existing = await Repository.GetOrDefault(resource.Slug); return Conflict(existing); } } + /// + /// 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 { 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) { @@ -145,44 +209,27 @@ 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}")] + /// + /// Delete an item + /// + /// + /// Delete one item via it's ID or it's slug. + /// + /// The ID or slug of the resource to delete. + /// The item has successfully been deleted. + /// No item could be found with the given id or slug. + [HttpDelete("{identifier:id}")] [PartialPermission(Kind.Delete)] - public virtual async Task Delete(int id) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(Identifier identifier) { try { - await _repository.Delete(id); + await identifier.Match( + id => Repository.Delete(id), + slug => Repository.Delete(slug) + ); } catch (ItemNotFoundException) { @@ -192,32 +239,28 @@ namespace Kyoo.Core.Api return Ok(); } - [HttpDelete("{slug}")] + /// + /// 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(string slug) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + public async Task Delete([FromQuery] Dictionary where) { try { - await _repository.Delete(slug); + await Repository.DeleteAll(ApiHelper.ParseWhere(where)); } - catch (ItemNotFoundException) + catch (ArgumentException ex) { - return NotFound(); - } - - return Ok(); - } - - [PartialPermission(Kind.Delete)] - public virtual async Task Delete(Dictionary where) - { - try - { - await _repository.DeleteAll(ApiHelper.ParseWhere(where)); - } - catch (ItemNotFoundException) - { - return NotFound(); + return BadRequest(new RequestError(ex.Message)); } return Ok(); diff --git a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs new file mode 100644 index 00000000..89e2e8c0 --- /dev/null +++ b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -0,0 +1,157 @@ +// 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.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 +{ + /// + /// 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. + public CrudThumbsApi(IRepository repository, + IFileSystem files, + IThumbnailsManager thumbs) + : base(repository) + { + _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, image); + 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.Core/Views/Helper/ResourceViewAttribute.cs b/src/Kyoo.Core/Views/Helper/ResourceViewAttribute.cs index abf215b7..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; @@ -32,8 +33,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) @@ -59,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 @@ -77,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(); @@ -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) @@ -104,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<>)); @@ -113,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..0372b568 --- /dev/null +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonOptions.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 Kyoo.Core.Models.Options; +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; + + /// + /// The options containing the public URL of kyoo, given to . + /// + private readonly IOptions _options; + + /// + /// Create a new . + /// + /// + /// The http context accessor given to the . + /// + /// + /// 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); + options.SerializerSettings.Converters.Add(new PeopleRoleConverter()); + } + } +} diff --git a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs deleted file mode 100644 index d5f23fce..00000000 --- a/src/Kyoo.Core/Views/Helper/Serializers/JsonPropertyIgnorer.cs +++ /dev/null @@ -1,89 +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; -using System.Reflection; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Attributes; -using Kyoo.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Kyoo.Core.Api -{ - public class JsonPropertyIgnorer : CamelCasePropertyNamesContractResolver - { - private int _depth = -1; - private string _host; - - public JsonPropertyIgnorer(string host) - { - _host = host; - } - - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - JsonProperty property = base.CreateProperty(member, memberSerialization); - - LoadableRelationAttribute relation = member.GetCustomAttribute(); - if (relation != null) - { - if (relation.RelationID == null) - property.ShouldSerialize = x => _depth == 0 && member.GetValue(x) != null; - else - { - property.ShouldSerialize = x => - { - if (_depth != 0) - return false; - if (member.GetValue(x) != null) - return true; - return x.GetType().GetProperty(relation.RelationID)?.GetValue(x) != null; - }; - } - } - - if (member.GetCustomAttribute() != null) - 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 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/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs new file mode 100644 index 00000000..b1e58c1b --- /dev/null +++ b/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -0,0 +1,168 @@ +// 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.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; + +namespace Kyoo.Core.Api +{ + /// + /// A custom json serializer that respects and + /// . It also handle via the + /// fields query parameter and items. + /// + public class JsonSerializerContract : CamelCasePropertyNamesContractResolver + { + /// + /// The http context accessor used to retrieve the fields query parameter as well as the type of + /// resource currently serializing. + /// + 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. + /// The options containing the public URL of kyoo. + public JsonSerializerContract(IHttpContextAccessor httpContextAccessor, IOptions options) + { + _httpContextAccessor = httpContextAccessor; + _options = options; + } + + /// + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + + LoadableRelationAttribute relation = member.GetCustomAttribute(); + if (relation != null) + { + property.ShouldSerialize = _ => + { + 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) + property.ShouldSerialize = _ => false; + if (member.GetCustomAttribute() != null) + property.ShouldDeserialize = _ => false; + 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(_options.Value.PublicUrl, id) + }); + } + + return properties; + } + + /// + /// A custom that uses the + /// . as a value. + /// + private class ThumbnailProvider : IValueProvider + { + /// + /// The public address of kyoo. + /// + private readonly Uri _host; + + /// + /// The index/ID of the image to retrieve/set. + /// + private readonly int _imageIndex; + + /// + /// Create a new . + /// + /// The public address of kyoo. + /// The index/ID of the image to retrieve/set. + public ThumbnailProvider(Uri host, int imageIndex) + { + _host = host; + _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) + { + 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}/{slug}/{Images.ImageName[_imageIndex]}".ToLower()) + .ToString(); + } + } + } +} 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/Helper/Serializers/SerializeAsProvider.cs b/src/Kyoo.Core/Views/Helper/Serializers/SerializeAsProvider.cs deleted file mode 100644 index b16e63ac..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 string _format; - private string _host; - - public SerializeAsProvider(string format, string host) - { - _format = format; - _host = host.TrimEnd('/'); - } - - 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; - - 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.Core/Views/LibraryApi.cs b/src/Kyoo.Core/Views/LibraryApi.cs deleted file mode 100644 index 0199aa3c..00000000 --- a/src/Kyoo.Core/Views/LibraryApi.cs +++ /dev/null @@ -1,217 +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 Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/library")] - [Route("api/libraries")] - [ApiController] - [PartialPermission(nameof(LibraryApi))] - public class LibraryApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - private readonly ITaskManager _taskManager; - - public LibraryApi(ILibraryManager libraryManager, ITaskManager taskManager, 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")] - [PartialPermission(Kind.Read)] - public async Task>> GetShows(int id, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 50) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Libraries.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.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) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Libraries.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}/collection")] - [HttpGet("{slug}/collections")] - [PartialPermission(Kind.Read)] - public async Task>> GetCollections(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.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}/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 }); - } - } - } -} diff --git a/src/Kyoo.Core/Views/LibraryItemApi.cs b/src/Kyoo.Core/Views/LibraryItemApi.cs deleted file mode 100644 index 1b1137fb..00000000 --- a/src/Kyoo.Core/Views/LibraryItemApi.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.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.Permissions; -using Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/item")] - [Route("api/items")] - [ApiController] - [ResourceView] - public class LibraryItemApi : ControllerBase - { - private readonly ILibraryItemRepository _libraryItems; - private readonly Uri _baseURL; - - public LibraryItemApi(ILibraryItemRepository libraryItems, IOptions options) - { - _libraryItems = libraryItems; - _baseURL = options.Value.PublicUrl; - } - - [HttpGet] - [Permission(nameof(LibraryItemApi), Kind.Read)] - public async Task>> GetAll([FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 50) - { - try - { - ICollection resources = await _libraryItems.GetAll( - ApiHelper.ParseWhere(where), - new Sort(sortBy), - 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(); - } - 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..5d3cbf7f --- /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(Genre))] + [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.Core/Views/Metadata/ProviderApi.cs b/src/Kyoo.Core/Views/Metadata/ProviderApi.cs new file mode 100644 index 00000000..6f9894d5 --- /dev/null +++ b/src/Kyoo.Core/Views/Metadata/ProviderApi.cs @@ -0,0 +1,55 @@ +// 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.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 +{ + /// + /// 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(Provider))] + [ApiDefinition("Providers", Group = MetadataGroup)] + public class ProviderApi : CrudThumbsApi + { + /// + /// 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, files, thumbnails) + { } + } +} diff --git a/src/Kyoo.Core/Views/Metadata/StaffApi.cs b/src/Kyoo.Core/Views/Metadata/StaffApi.cs new file mode 100644 index 00000000..aea11a4e --- /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(People))] + [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/Metadata/StudioApi.cs b/src/Kyoo.Core/Views/Metadata/StudioApi.cs new file mode 100644 index 00000000..d5274f18 --- /dev/null +++ b/src/Kyoo.Core/Views/Metadata/StudioApi.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/studios")] + [Route("api/studio", Order = AlternativeRoute)] + [ApiController] + [PartialPermission(nameof(Show))] + [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; + } + + /// + /// 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)] + [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.Matcher(x => x.StudioID, x => x.Studio.Slug)), + 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.Core/Views/PeopleApi.cs b/src/Kyoo.Core/Views/PeopleApi.cs deleted file mode 100644 index 2d810927..00000000 --- a/src/Kyoo.Core/Views/PeopleApi.cs +++ /dev/null @@ -1,126 +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 Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -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, - IOptions options, - IFileSystem files, - IThumbnailsManager thumbs) - : base(libraryManager.PeopleRepository, options.Value.PublicUrl) - { - _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/ProviderApi.cs b/src/Kyoo.Core/Views/ProviderApi.cs deleted file mode 100644 index 582c4d3d..00000000 --- a/src/Kyoo.Core/Views/ProviderApi.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.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 -{ - [Route("api/provider")] - [Route("api/providers")] - [ApiController] - [PartialPermission(nameof(ProviderApi))] - public class ProviderApi : CrudApi - { - private readonly IThumbnailsManager _thumbnails; - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _files; - - public ProviderApi(ILibraryManager libraryManager, - IOptions options, - IFileSystem files, - IThumbnailsManager thumbnails) - : base(libraryManager.ProviderRepository, options.Value.PublicUrl) - { - _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)); - } - } -} diff --git a/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/src/Kyoo.Core/Views/Resources/CollectionApi.cs new file mode 100644 index 00000000..3cee88b6 --- /dev/null +++ b/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -0,0 +1,153 @@ +// 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/collections")] + [Route("api/collection", Order = AlternativeRoute)] + [ApiController] + [PartialPermission(nameof(Collection))] + [ApiDefinition("Collections", Group = ResourcesGroup)] + public class CollectionApi : CrudThumbsApi + { + /// + /// 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. + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + public CollectionApi(ILibraryManager libraryManager, + IFileSystem files, + IThumbnailsManager thumbs) + : base(libraryManager.CollectionRepository, files, thumbs) + { + _libraryManager = libraryManager; + } + + /// + /// Get shows in collection + /// + /// + /// Lists the shows that are contained in the collection with the given id or slug. + /// + /// 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 collection 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 = 30, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Collections)), + 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)); + } + } + + /// + /// Get libraries containing this collection + /// + /// + /// Lists the libraries that contain the collection with the given id or slug. + /// + /// 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 collection with the given ID or slug could be found. + [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, + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Collections)), + 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.Core/Views/Resources/EpisodeApi.cs b/src/Kyoo.Core/Views/Resources/EpisodeApi.cs new file mode 100644 index 00000000..ba11597a --- /dev/null +++ b/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -0,0 +1,162 @@ +// 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/episodes")] + [Route("api/episode", Order = AlternativeRoute)] + [ApiController] + [ResourceView] + [PartialPermission(nameof(Episode))] + [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; + + /// + /// 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. + public EpisodeApi(ILibraryManager libraryManager, + IFileSystem files, + IThumbnailsManager thumbnails) + : base(libraryManager.EpisodeRepository, files, thumbnails) + { + _libraryManager = libraryManager; + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetShow(Identifier identifier) + { + Show ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Episodes)); + if (ret == null) + return NotFound(); + return ret; + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetSeason(Identifier identifier) + { + 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(); + } + + /// + /// 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 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)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetEpisode(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + 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(identifier.IsSame()) == null) + return NotFound(); + return Page(resources, limit); + } + catch (ArgumentException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + } +} diff --git a/src/Kyoo.Core/Views/Resources/LibraryApi.cs b/src/Kyoo.Core/Views/Resources/LibraryApi.cs new file mode 100644 index 00000000..4f361705 --- /dev/null +++ b/src/Kyoo.Core/Views/Resources/LibraryApi.cs @@ -0,0 +1,204 @@ +// 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.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 . + /// + [Route("api/libraries")] + [Route("api/library", Order = AlternativeRoute)] + [ApiController] + [ResourceView] + [PartialPermission(nameof(Library), 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; + + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// + public LibraryApi(ILibraryManager libraryManager) + : base(libraryManager.LibraryRepository) + { + _libraryManager = libraryManager; + } + + /// + /// 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)] + [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 = 50, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Libraries)), + 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)); + } + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetCollections(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Libraries)), + 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)); + } + } + + /// + /// 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/library 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetItems(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50, + [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.GetItemsFromLibrary(id, whereQuery, sort, pagination), + slug => _libraryManager.GetItemsFromLibrary(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/Resources/LibraryItemApi.cs b/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs new file mode 100644 index 00000000..5318d0b5 --- /dev/null +++ b/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs @@ -0,0 +1,104 @@ +// 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.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 +{ + /// + /// 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] + [PartialPermission(nameof(LibraryItem))] + [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; + + /// + /// Create a new . + /// + /// + /// The library item repository used to modify or retrieve information in the data store. + /// + public LibraryItemApi(ILibraryItemRepository libraryItems) + { + _libraryItems = libraryItems; + } + + /// + /// 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] + [PartialPermission(Kind.Read)] + [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? afterID = null) + { + try + { + ICollection resources = await _libraryItems.GetAll( + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID) + ); + + return Page(resources, limit); + } + catch (ArgumentException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + } +} 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 new file mode 100644 index 00000000..8f74a08c --- /dev/null +++ b/src/Kyoo.Core/Views/Resources/SeasonApi.cs @@ -0,0 +1,129 @@ +// 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/seasons")] + [Route("api/season", Order = AlternativeRoute)] + [ApiController] + [PartialPermission(nameof(Season))] + [ApiDefinition("Seasons", Group = ResourcesGroup)] + public class SeasonApi : 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 in the data store. + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + public SeasonApi(ILibraryManager libraryManager, + IFileSystem files, + IThumbnailsManager thumbs) + : base(libraryManager.SeasonRepository, files, thumbs) + { + _libraryManager = libraryManager; + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetEpisode(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + 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(identifier.IsSame()) == null) + return NotFound(); + return Page(resources, limit); + } + catch (ArgumentException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetShow(Identifier identifier) + { + Show ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Seasons)); + if (ret == null) + return NotFound(); + return ret; + } + } +} diff --git a/src/Kyoo.Core/Views/Resources/ShowApi.cs b/src/Kyoo.Core/Views/Resources/ShowApi.cs new file mode 100644 index 00000000..46712294 --- /dev/null +++ b/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -0,0 +1,439 @@ +// 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.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 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 . + /// + [Route("api/shows")] + [Route("api/show", Order = AlternativeRoute)] + [Route("api/movie", Order = AlternativeRoute)] + [Route("api/movies", Order = AlternativeRoute)] + [ApiController] + [PartialPermission(nameof(Show))] + [ApiDefinition("Shows", Group = ResourcesGroup)] + public class ShowApi : CrudThumbsApi + { + /// + /// The library manager used to modify or retrieve information in the data store. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// The file manager used to send images and fonts. + /// + 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 . + /// + /// + /// 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, files, thumbs) + { + _libraryManager = libraryManager; + _files = files; + _baseURL = options.Value.PublicUrl; + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetSeasons(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.Matcher(x => x.ShowID, x => x.Show.Slug)), + 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)); + } + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetEpisodes(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowID, x => x.Show.Slug)), + 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)); + } + } + + /// + /// Get staff + /// + /// + /// 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}/staff")] + [HttpGet("{identifier:id}/people", Order = AlternativeRoute)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetPeople(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30, + [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.GetPeopleFromShow(id, whereQuery, sort, pagination), + slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination) + ); + return Page(resources, limit); + } + catch (ItemNotFoundException) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetGenres(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), + 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)); + } + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetStudio(Identifier identifier) + { + Studio studio = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Shows)); + if (studio == null) + return NotFound(); + return 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetLibraries(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), + 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)); + } + } + + /// + /// 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}/collections")] + [HttpGet("{identifier:id}/collection", Order = AlternativeRoute)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetCollections(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), + 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)); + } + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetFonts(Identifier identifier) + { + 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)}" + ); + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetFont(Identifier identifier, string font) + { + 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(); + string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments", font); + return _files.FileResult(path); + } + } +} 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.Core/Views/SeasonApi.cs b/src/Kyoo.Core/Views/SeasonApi.cs deleted file mode 100644 index 126603ea..00000000 --- a/src/Kyoo.Core/Views/SeasonApi.cs +++ /dev/null @@ -1,184 +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 Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/season")] - [Route("api/seasons")] - [ApiController] - [PartialPermission(nameof(SeasonApi))] - public class SeasonApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - private readonly IThumbnailsManager _thumbs; - private readonly IFileSystem _files; - - public SeasonApi(ILibraryManager libraryManager, - IOptions options, - IThumbnailsManager thumbs, - IFileSystem files) - : base(libraryManager.SeasonRepository, options.Value.PublicUrl) - { - _libraryManager = libraryManager; - _thumbs = thumbs; - _files = files; - } - - [HttpGet("{seasonID:int}/episode")] - [HttpGet("{seasonID:int}/episodes")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(int seasonID, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.SeasonID == seasonID), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(seasonID) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{showSlug}-s{seasonNumber:int}/episode")] - [HttpGet("{showSlug}-s{seasonNumber:int}/episodes")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(string showSlug, - 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.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)); - 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 deleted file mode 100644 index 4ebe06da..00000000 --- a/src/Kyoo.Core/Views/ShowApi.cs +++ /dev/null @@ -1,474 +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.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.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/show")] - [Route("api/shows")] - [Route("api/movie")] - [Route("api/movies")] - [ApiController] - [PartialPermission(nameof(ShowApi))] - public class ShowApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _files; - private readonly IThumbnailsManager _thumbs; - - public ShowApi(ILibraryManager libraryManager, - IFileSystem files, - IThumbnailsManager thumbs, - IOptions options) - : base(libraryManager.ShowRepository, options.Value.PublicUrl) - { - _libraryManager = libraryManager; - _files = files; - _thumbs = thumbs; - } - - [HttpGet("{showID:int}/season")] - [HttpGet("{showID:int}/seasons")] - [PartialPermission(Kind.Read)] - public async Task>> GetSeasons(int showID, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 20) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.ShowID == showID), - 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}/season")] - [HttpGet("{slug}/seasons")] - [PartialPermission(Kind.Read)] - public async Task>> GetSeasons(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.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) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.ShowID == showID), - 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}/episode")] - [HttpGet("{slug}/episodes")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisodes(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 50) - { - 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}/people")] - [PartialPermission(Kind.Read)] - public async Task>> GetPeople(int showID, - [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) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.ID == showID)), - 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}/genre")] - [HttpGet("{slug}/genres")] - [PartialPermission(Kind.Read)] - public async Task>> GetGenre(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.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) - { - return NotFound(); - } - } - - [HttpGet("{slug}/studio")] - [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, - [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.ID == showID)), - 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}/library")] - [HttpGet("{slug}/libraries")] - [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.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) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.ID == showID)), - 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}/collection")] - [HttpGet("{slug}/collections")] - [PartialPermission(Kind.Read)] - public async Task>> GetCollections(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.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("{slug}/font")] - [HttpGet("{slug}/fonts")] - [PartialPermission(Kind.Read)] - public async Task>> GetFonts(string slug) - { - 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) - { - 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(); - } - } - } -} diff --git a/src/Kyoo.Core/Views/StudioApi.cs b/src/Kyoo.Core/Views/StudioApi.cs deleted file mode 100644 index f7bd9c77..00000000 --- a/src/Kyoo.Core/Views/StudioApi.cs +++ /dev/null @@ -1,98 +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 Kyoo.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/studio")] - [Route("api/studios")] - [ApiController] - [PartialPermission(nameof(ShowApi))] - public class StudioApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - - public StudioApi(ILibraryManager libraryManager, IOptions options) - : base(libraryManager.StudioRepository, options.Value.PublicUrl) - { - _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.StudioID == 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.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 }); - } - } - } -} diff --git a/src/Kyoo.Core/Views/SubtitleApi.cs b/src/Kyoo.Core/Views/SubtitleApi.cs deleted file mode 100644 index 784a9022..00000000 --- a/src/Kyoo.Core/Views/SubtitleApi.cs +++ /dev/null @@ -1,159 +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.IO; -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("subtitle")] - [ApiController] - public class SubtitleApi : ControllerBase - { - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _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) - { - Track subtitle = await _libraryManager.GetOrDefault(id); - return subtitle != null - ? _files.FileResult(subtitle.Path) - : NotFound(); - } - - [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") - return new ConvertSubripToVtt(subtitle.Path, _files); - 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 - { - private readonly string _path; - private readonly IFileSystem _files; - - public ConvertSubripToVtt(string subtitlePath, IFileSystem files) - { - _path = subtitlePath; - _files = files; - } - - public async Task ExecuteResultAsync(ActionContext context) - { - List lines = new(); - - context.HttpContext.Response.StatusCode = 200; - context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt"); - - await using (StreamWriter writer = new(context.HttpContext.Response.Body)) - { - await writer.WriteLineAsync("WEBVTT"); - await writer.WriteLineAsync(string.Empty); - await writer.WriteLineAsync(string.Empty); - - using StreamReader reader = new(await _files.GetReader(_path)); - string line; - while ((line = await reader.ReadLineAsync()) != null) - { - if (line == string.Empty) - { - lines.Add(string.Empty); - IEnumerable processedBlock = _ConvertBlock(lines); - foreach (string t in processedBlock) - await writer.WriteLineAsync(t); - lines.Clear(); - } - else - lines.Add(line); - } - } - - await context.HttpContext.Response.Body.FlushAsync(); - } - - private static IEnumerable _ConvertBlock(IList lines) - { - if (lines.Count < 3) - return lines; - lines[1] = lines[1].Replace(',', '.'); - if (lines[2].Length > 5) - { - lines[1] += lines[2].Substring(0, 6) switch - { - "{\\an1}" => " line:93% position:15%", - "{\\an2}" => " line:93%", - "{\\an3}" => " line:93% position:85%", - "{\\an4}" => " line:50% position:15%", - "{\\an5}" => " line:50%", - "{\\an6}" => " line:50% position:85%", - "{\\an7}" => " line:7% position:15%", - "{\\an8}" => " line:7%", - "{\\an9}" => " line:7% position:85%", - _ => " line:93%" - }; - } - - if (lines[2].StartsWith("{\\an")) - lines[2] = lines[2].Substring(6); - - return lines; - } - } - } -} 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/TrackApi.cs b/src/Kyoo.Core/Views/TrackApi.cs deleted file mode 100644 index 7075bcbc..00000000 --- a/src/Kyoo.Core/Views/TrackApi.cs +++ /dev/null @@ -1,73 +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.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.Core.Models.Options; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Kyoo.Core.Api -{ - [Route("api/track")] - [Route("api/tracks")] - [ApiController] - [PartialPermission(nameof(Track))] - public class TrackApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - - public TrackApi(ILibraryManager libraryManager, IOptions options) - : base(libraryManager.TrackRepository, options.Value.PublicUrl) - { - _libraryManager = libraryManager; - } - - [HttpGet("{id:int}/episode")] - [PartialPermission(Kind.Read)] - public async Task> GetEpisode(int id) - { - try - { - return await _libraryManager.Get(x => x.Tracks.Any(y => y.ID == id)); - } - catch (ItemNotFoundException) - { - 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(); - } - } - } -} 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/SubtitleApi.cs b/src/Kyoo.Core/Views/Watch/SubtitleApi.cs new file mode 100644 index 00000000..3266af8d --- /dev/null +++ b/src/Kyoo.Core/Views/Watch/SubtitleApi.cs @@ -0,0 +1,205 @@ +// 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.Diagnostics.CodeAnalysis; +using System.IO; +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 +{ + /// + /// 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; + } + + /// + /// 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: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( + 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)); + }); + + if (subtitle == null) + return NotFound(); + if (subtitle.Codec == "subrip" && extension == "vtt") + return new ConvertSubripToVtt(subtitle.Path, _files); + return _files.FileResult(subtitle.Path); + } + + /// + /// 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(); + + context.HttpContext.Response.StatusCode = 200; + context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt"); + + await using (StreamWriter writer = new(context.HttpContext.Response.Body)) + { + await writer.WriteLineAsync("WEBVTT"); + await writer.WriteLineAsync(string.Empty); + await writer.WriteLineAsync(string.Empty); + + using StreamReader reader = new(await _files.GetReader(_path)); + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + if (line == string.Empty) + { + lines.Add(string.Empty); + IEnumerable processedBlock = _ConvertBlock(lines); + foreach (string t in processedBlock) + await writer.WriteLineAsync(t); + lines.Clear(); + } + else + lines.Add(line); + } + } + + await context.HttpContext.Response.Body.FlushAsync(); + } + + /// + /// 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; + lines[1] = lines[1].Replace(',', '.'); + if (lines[2].Length > 5) + { + lines[1] += lines[2].Substring(0, 6) switch + { + "{\\an1}" => " line:93% position:15%", + "{\\an2}" => " line:93%", + "{\\an3}" => " line:93% position:85%", + "{\\an4}" => " line:50% position:15%", + "{\\an5}" => " line:50%", + "{\\an6}" => " line:50% position:85%", + "{\\an7}" => " line:7% position:15%", + "{\\an8}" => " line:7%", + "{\\an9}" => " line:7% position:85%", + _ => " line:93%" + }; + } + + if (lines[2].StartsWith("{\\an")) + lines[2] = lines[2][6..]; + return lines; + } + } + } +} diff --git a/src/Kyoo.Core/Views/Watch/TrackApi.cs b/src/Kyoo.Core/Views/Watch/TrackApi.cs new file mode 100644 index 00000000..60a5d0c2 --- /dev/null +++ b/src/Kyoo.Core/Views/Watch/TrackApi.cs @@ -0,0 +1,81 @@ +// 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 +{ + /// + /// 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 = WatchGroup)] + 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. + /// + public TrackApi(ILibraryManager libraryManager) + : base(libraryManager.TrackRepository) + { + _libraryManager = libraryManager; + } + + /// + /// 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)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetEpisode(Identifier identifier) + { + Episode ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Tracks)); + if (ret == null) + return NotFound(); + return ret; + } + } +} diff --git a/src/Kyoo.Core/Views/Watch/VideoApi.cs b/src/Kyoo.Core/Views/Watch/VideoApi.cs new file mode 100644 index 00000000..40f8c881 --- /dev/null +++ b/src/Kyoo.Core/Views/Watch/VideoApi.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.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 + { + /// + /// 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) + { + _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.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(); - } - } - } -} 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.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.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/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.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.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/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.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/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.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 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 diff --git a/src/Kyoo.Swagger/ApiSorter.cs b/src/Kyoo.Swagger/ApiSorter.cs new file mode 100644 index 00000000..233151a6 --- /dev/null +++ b/src/Kyoo.Swagger/ApiSorter.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.Collections.Generic; +using System.Linq; +using Kyoo.Swagger.Models; +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 => + { + 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 new file mode 100644 index 00000000..f3537541 --- /dev/null +++ b/src/Kyoo.Swagger/ApiTagsFilter.cs @@ -0,0 +1,124 @@ +// 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 Kyoo.Swagger.Models; +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; + + 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)) + { + context.Document.Tags.Add(new OpenApiTag + { + Name = name, + Description = context.ControllerType.GetXmlDocsSummary() + }); + } + + if (def?.Group == null) + return true; + + context.Document.ExtensionData ??= new Dictionary(); + 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); + } + else + { + obj.Add(new TagGroups + { + 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 TagGroups + { + 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/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 new file mode 100644 index 00000000..54d4e2f3 --- /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/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; } + } +} 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 new file mode 100644 index 00000000..88f1d7e1 --- /dev/null +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -0,0 +1,149 @@ +// 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.Reflection; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Permissions; +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; + +namespace Kyoo.Swagger +{ + /// + /// A module to enable a swagger interface and an OpenAPI endpoint to document Kyoo. + /// + 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.AddTransient(); + services.AddOpenApiDocument(document => + { + document.Title = "Kyoo API"; + // TODO use a real multi-line description in markdown. + document.Description = "The Kyoo's public API"; + document.Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); + document.DocumentName = "v1"; + document.UseControllerSummaryAsTagDescription = true; + document.GenerateExamples = true; + document.PostProcess = options => + { + options.Info.Contact = new OpenApiContact + { + Name = "Kyoo's github", + Url = "https://github.com/AnonymusRaccoon/Kyoo" + }; + options.Info.License = new OpenApiLicense + { + 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(); + document.AddOperationFilter(x => + { + if (x is AspNetCoreOperationProcessorContext ctx) + return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute; + return true; + }); + document.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x => + { + x.IsNullableRaw = false; + x.Type = JsonObjectType.String | JsonObjectType.Integer; + })); + document.SchemaProcessors.Add(new ThumbnailProcessor()); + + document.AddSecurity("Kyoo", new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.OpenIdConnect, + 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()); + // 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 + { + 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 + { + ExtensionData = new Dictionary + { + ["type"] = "OpenID Connect or Api Key" + }, + Description = "The permission group used for administrative items like tasks, account management " + + "and library creation." + }); + }); + } + + /// + public IEnumerable ConfigureSteps => new IStartupAction[] + { + SA.New(app => app.UseOpenApi(), SA.Before + 1), + SA.New(app => app.UseSwaggerUi3(), SA.Before), + 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.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.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/Front b/src/Kyoo.WebApp/Front index a3da5f1e..0170abcf 160000 --- a/src/Kyoo.WebApp/Front +++ b/src/Kyoo.WebApp/Front @@ -1 +1 @@ -Subproject commit a3da5f1e6edb982e3b71792a7ea6fcd45661c337 +Subproject commit 0170abcffd4da197ccbf1155623311b07777314b diff --git a/src/Kyoo.WebApp/Kyoo.WebApp.csproj b/src/Kyoo.WebApp/Kyoo.WebApp.csproj index 910d2167..4d637707 100644 --- a/src/Kyoo.WebApp/Kyoo.WebApp.csproj +++ b/src/Kyoo.WebApp/Kyoo.WebApp.csproj @@ -1,22 +1,22 @@ - + net5.0 + default + Kyoo.WebApp + Kyoo.WebApp + true Latest false $(DefaultItemExcludes);$(SpaRoot)node_modules/** Front/ + ../../icons/ $(SpaRoot)node_modules/.install-stamp false - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo - default @@ -30,6 +30,11 @@ + + wwwroot/%(RecursiveDir)%(Filename)%(Extension) + Always + + wwwroot/%(RecursiveDir)%(Filename)%(Extension) Always @@ -41,7 +46,7 @@ - + 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; } }, { 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 @@ -