Merge pull request #40 from AnonymusRaccoon/swagger

Swagger
This commit is contained in:
Zoe Roux 2021-10-18 21:25:43 +02:00 committed by GitHub
commit 540587ec79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 4811 additions and 3118 deletions

View File

@ -89,3 +89,5 @@ resharper_xmldoc_attribute_indent = align_by_first_attribute
resharper_xmldoc_indent_child_elements = RemoveIndent resharper_xmldoc_indent_child_elements = RemoveIndent
resharper_xmldoc_indent_text = 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 <https://www.gnu.org/licenses/>.

View File

@ -1,4 +1,4 @@
# Authors # Authors
Alphabetical order by first name. Ordered by the date of the first commit.
* Zoe Roux ([@AnonymusRaccoon](http://github.com/AnonymusRaccoon)) * Zoe Roux ([@AnonymusRaccoon](http://github.com/AnonymusRaccoon))

View File

@ -25,6 +25,7 @@ Here are a few things you can do that will increase the likelihood of your pull
## Resources ## 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/) - [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) - [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) - [GitHub Help](https://docs.github.com/en)

View File

@ -23,6 +23,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.WindowsTrait", "s
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.Console", "src\Kyoo.Host.Console\Kyoo.Host.Console.csproj", "{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.Console", "src\Kyoo.Host.Console\Kyoo.Host.Console.csproj", "{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@ -9,7 +9,7 @@ AppUpdatesURL=https://github.com/AnonymusRaccoon/Kyoo
DefaultDirName={commonpf}\Kyoo DefaultDirName={commonpf}\Kyoo
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
LicenseFile={#kyoo}\LICENSE LicenseFile={#kyoo}\LICENSE
SetupIconFile={#kyoo}\wwwroot\favicon.ico SetupIconFile={#kyoo}\wwwroot\icon-256x256.png
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern WizardStyle=modern
@ -54,7 +54,7 @@ procedure InitializeWizard;
begin begin
DataDirPage := CreateInputDirPage(wpSelectDir, DataDirPage := CreateInputDirPage(wpSelectDir,
'Choose Data Location', 'Choose the folder in which to install the Kyoo data', 'Choose Data Location', 'Choose the folder in which to install the Kyoo data',
'The installer will set the following folder for Kyoo. To install in a different folder, click Browse and select another folder.' + 'The installer will set the following folder for Kyoo. To install in a different folder, click Browse and select another folder.' +
'Please make sure the folder exists and is accessible. Do not choose the server install folder. Click Next to continue.', 'Please make sure the folder exists and is accessible. Do not choose the server install folder. Click Next to continue.',
False, ''); False, '');
DataDirPage.Add(''); DataDirPage.Add('');
@ -64,4 +64,4 @@ end;
function GetDataDir(Param: String): String; function GetDataDir(Param: String): String;
begin begin
Result := DataDirPage.Values[0]; Result := DataDirPage.Values[0];
end; end;

BIN
icons/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
icons/icon-128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
icons/icon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

BIN
icons/icon-256x256.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
icons/icon-256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
icons/icon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/icon-64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,4 +1,24 @@
<Project> <Project>
<PropertyGroup>
<Company>Kyoo</Company>
<Authors>Kyoo</Authors>
<Copyright>Copyright (c) Kyoo</Copyright>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
<RequireLicenseAcceptance>true</RequireLicenseAcceptance>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageProjectUrl>https://github.com/AnonymusRaccoon/Kyoo</PackageProjectUrl>
<PackageVersion>1.0.0</PackageVersion>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<ApplicationIcon>$(MSBuildThisFileDirectory)../icons/icon-256x256.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<IsWindows Condition="$([MSBuild]::IsOSPlatform('Windows'))">true</IsWindows> <IsWindows Condition="$([MSBuild]::IsOSPlatform('Windows'))">true</IsWindows>
<IsOSX Condition="$([MSBuild]::IsOSPlatform('OSX'))">true</IsOSX> <IsOSX Condition="$([MSBuild]::IsOSPlatform('OSX'))">true</IsOSX>
@ -11,6 +31,10 @@
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle> <CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="$(CheckCodingStyle) == true"> <ItemGroup Condition="$(CheckCodingStyle) == true">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.354" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.354" PrivateAssets="All" />
@ -21,7 +45,6 @@
<PropertyGroup Condition="$(CheckCodingStyle) == true"> <PropertyGroup Condition="$(CheckCodingStyle) == true">
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591;SA1600;SA1601</NoWarn>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet>
<!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode>--> <!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode>-->

View File

@ -31,8 +31,6 @@ namespace Kyoo.Abstractions.Controllers
/// </summary> /// </summary>
public interface IFileSystem public interface IFileSystem
{ {
// TODO find a way to handle Transmux/Transcode with this system.
/// <summary> /// <summary>
/// Used for http queries returning a file. This should be used to return local files /// Used for http queries returning a file. This should be used to return local files
/// or proxy them from a distant server. /// 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). /// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file).
/// </param> /// </param>
/// <returns>An <see cref="IActionResult"/> representing the file returned.</returns> /// <returns>An <see cref="IActionResult"/> representing the file returned.</returns>
public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null); IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null);
/// <summary> /// <summary>
/// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context. /// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context.
@ -60,7 +58,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="path">The path of the file</param> /// <param name="path">The path of the file</param>
/// <exception cref="FileNotFoundException">If the file could not be found.</exception> /// <exception cref="FileNotFoundException">If the file could not be found.</exception>
/// <returns>A reader to read the file.</returns> /// <returns>A reader to read the file.</returns>
public Task<Stream> GetReader([NotNull] string path); Task<Stream> GetReader([NotNull] string path);
/// <summary> /// <summary>
/// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context. /// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context.
@ -70,28 +68,28 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="mime">The mime type of the opened file.</param> /// <param name="mime">The mime type of the opened file.</param>
/// <exception cref="FileNotFoundException">If the file could not be found.</exception> /// <exception cref="FileNotFoundException">If the file could not be found.</exception>
/// <returns>A reader to read the file.</returns> /// <returns>A reader to read the file.</returns>
public Task<Stream> GetReader([NotNull] string path, AsyncRef<string> mime); Task<Stream> GetReader([NotNull] string path, AsyncRef<string> mime);
/// <summary> /// <summary>
/// Create a new file at <paramref name="path"></paramref>. /// Create a new file at <paramref name="path"></paramref>.
/// </summary> /// </summary>
/// <param name="path">The path of the new file.</param> /// <param name="path">The path of the new file.</param>
/// <returns>A writer to write to the new file.</returns> /// <returns>A writer to write to the new file.</returns>
public Task<Stream> NewFile([NotNull] string path); Task<Stream> NewFile([NotNull] string path);
/// <summary> /// <summary>
/// Create a new directory at the given path /// Create a new directory at the given path
/// </summary> /// </summary>
/// <param name="path">The path of the directory</param> /// <param name="path">The path of the directory</param>
/// <returns>The path of the newly created directory is returned.</returns> /// <returns>The path of the newly created directory is returned.</returns>
public Task<string> CreateDirectory([NotNull] string path); Task<string> CreateDirectory([NotNull] string path);
/// <summary> /// <summary>
/// Combine multiple paths. /// Combine multiple paths.
/// </summary> /// </summary>
/// <param name="paths">The paths to combine</param> /// <param name="paths">The paths to combine</param>
/// <returns>The combined path.</returns> /// <returns>The combined path.</returns>
public string Combine(params string[] paths); string Combine(params string[] paths);
/// <summary> /// <summary>
/// List files in a directory. /// List files in a directory.
@ -99,7 +97,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="path">The path of the directory</param> /// <param name="path">The path of the directory</param>
/// <param name="options">Should the search be recursive or not.</param> /// <param name="options">Should the search be recursive or not.</param>
/// <returns>A list of files's path.</returns> /// <returns>A list of files's path.</returns>
public Task<ICollection<string>> ListFiles([NotNull] string path, Task<ICollection<string>> ListFiles([NotNull] string path,
SearchOption options = SearchOption.TopDirectoryOnly); SearchOption options = SearchOption.TopDirectoryOnly);
/// <summary> /// <summary>
@ -107,7 +105,7 @@ namespace Kyoo.Abstractions.Controllers
/// </summary> /// </summary>
/// <param name="path">The path to check</param> /// <param name="path">The path to check</param>
/// <returns>True if the path exists, false otherwise</returns> /// <returns>True if the path exists, false otherwise</returns>
public Task<bool> Exists([NotNull] string path); Task<bool> Exists([NotNull] string path);
/// <summary> /// <summary>
/// Get the extra directory of a resource <typeparamref name="T"/>. /// Get the extra directory of a resource <typeparamref name="T"/>.
@ -117,6 +115,25 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="resource">The resource to proceed</param> /// <param name="resource">The resource to proceed</param>
/// <typeparam name="T">The type of the resource.</typeparam> /// <typeparam name="T">The type of the resource.</typeparam>
/// <returns>The extra directory of the resource.</returns> /// <returns>The extra directory of the resource.</returns>
public Task<string> GetExtraDirectory<T>([NotNull] T resource); Task<string> GetExtraDirectory<T>([NotNull] T resource);
/// <summary>
/// Retrieve tracks for a specific episode.
/// Subtitles, chapters and fonts should also be extracted and cached when calling this method.
/// </summary>
/// <param name="episode">The episode to retrieve tracks for.</param>
/// <param name="reExtract">Should the cache be invalidated and subtitles and others be re-extracted?</param>
/// <returns>The list of tracks available for this episode.</returns>
Task<ICollection<Track>> ExtractInfos([NotNull] Episode episode, bool reExtract);
/// <summary>
/// Transmux the selected episode to hls.
/// </summary>
/// <param name="episode">The episode to transmux.</param>
/// <returns>The master file (m3u8) of the transmuxed hls file.</returns>
IActionResult Transmux([NotNull] Episode episode);
// Maybe add options for to select the codec.
// IActionResult Transcode(Episode episode);
} }
} }

View File

@ -200,10 +200,11 @@ namespace Kyoo.Abstractions.Controllers
/// Get the resource by a filter function or null if it is not found. /// Get the resource by a filter function or null if it is not found.
/// </summary> /// </summary>
/// <param name="where">The filter function.</param> /// <param name="where">The filter function.</param>
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The first resource found that match the where function</returns> /// <returns>The first resource found that match the where function</returns>
[ItemCanBeNull] [ItemCanBeNull]
Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where) Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T> sortBy = default)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -317,6 +318,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id, Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
@ -330,6 +332,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id, Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
@ -344,6 +347,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug, Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
@ -357,6 +361,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug, Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
@ -371,6 +376,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID, Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -384,6 +390,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID, Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
@ -398,6 +405,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug, Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -411,6 +419,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug, Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
@ -425,6 +434,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id, Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -438,6 +448,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id, Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
@ -452,6 +463,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug, Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -465,6 +477,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug, Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,

View File

@ -81,9 +81,10 @@ namespace Kyoo.Abstractions.Controllers
/// Get the first resource that match the predicate or null if it is not found. /// Get the first resource that match the predicate or null if it is not found.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter the resource.</param> /// <param name="where">A predicate to filter the resource.</param>
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemCanBeNull] [ItemCanBeNull]
Task<T> GetOrDefault(Expression<Func<T, bool>> where); Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default);
/// <summary> /// <summary>
/// Search for resources. /// Search for resources.
@ -179,7 +180,6 @@ namespace Kyoo.Abstractions.Controllers
/// Delete all resources that match the predicate. /// Delete all resources that match the predicate.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param> /// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DeleteAll([NotNull] Expression<Func<T, bool>> where); Task DeleteAll([NotNull] Expression<Func<T, bool>> where);
} }
@ -264,6 +264,8 @@ namespace Kyoo.Abstractions.Controllers
/// </summary> /// </summary>
public interface IEpisodeRepository : IRepository<Episode> public interface IEpisodeRepository : IRepository<Episode>
{ {
// TODO replace the next methods with extension methods.
/// <summary> /// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number. /// Get a episode from it's showID, it's seasonNumber and it's episode number.
/// </summary> /// </summary>
@ -343,6 +345,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id, public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
@ -356,6 +359,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id, public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
@ -370,6 +374,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug, public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
@ -383,6 +388,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug, public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
@ -418,6 +424,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID, Task<ICollection<PeopleRole>> GetFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -431,6 +438,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID, Task<ICollection<PeopleRole>> GetFromShow(int showID,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
@ -445,6 +453,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug, Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -458,6 +467,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug, Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
@ -472,6 +482,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id, Task<ICollection<PeopleRole>> GetFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -485,6 +496,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id, Task<ICollection<PeopleRole>> GetFromPeople(int id,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
@ -499,6 +511,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param> /// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug, Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -512,6 +525,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param> /// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug, Task<ICollection<PeopleRole>> GetFromPeople(string slug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,

View File

@ -1,20 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<Title>Kyoo.Abstractions</Title>
<Authors>Zoe Roux</Authors>
<Description>Base package to create plugins for Kyoo.</Description>
<PackageProjectUrl>https://github.com/AnonymusRaccoon/Kyoo</PackageProjectUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<Company>SDG</Company>
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageVersion>1.0.0</PackageVersion>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
<Title>Kyoo.Abstractions</Title>
<Description>Base package to create plugins for Kyoo.</Description>
<RootNamespace>Kyoo.Abstractions</RootNamespace> <RootNamespace>Kyoo.Abstractions</RootNamespace>
</PropertyGroup> </PropertyGroup>
@ -24,8 +13,6 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
<PackageReference Include="System.ComponentModel.Composition" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Composition" Version="5.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using JetBrains.Annotations;
namespace Kyoo.Abstractions.Models.Attributes
{
/// <summary>
/// An attribute to specify on apis to specify it's documentation's name and category.
/// If this is applied on a method, the specified method will be exploded from the controller's page and be
/// included on the specified tag page.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiDefinitionAttribute : Attribute
{
/// <summary>
/// The public name of this api.
/// </summary>
[NotNull] public string Name { get; }
/// <summary>
/// The name of the group in witch this API is. You can also specify a custom sort order using the following
/// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
/// th alphabetical ordering.
/// </summary>
public string Group { get; set; }
/// <summary>
/// Create a new <see cref="ApiDefinitionAttribute"/>.
/// </summary>
/// <param name="name">The name of the api that will be used on the documentation page.</param>
public ApiDefinitionAttribute([NotNull] string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
Name = name;
}
}
}

View File

@ -39,6 +39,11 @@ namespace Kyoo.Abstractions.Models.Permissions
/// </summary> /// </summary>
public Kind Kind { get; } public Kind Kind { get; }
/// <summary>
/// The group of this permission.
/// </summary>
public Group Group { get; set; }
/// <summary> /// <summary>
/// Ask a permission to run an action. /// Ask a permission to run an action.
/// </summary> /// </summary>
@ -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 /// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors. /// lead to unspecified behaviors.
/// </remarks> /// </remarks>
/// <param name="type"> /// <param name="type">The type of the action</param>
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
public PartialPermissionAttribute(string type) public PartialPermissionAttribute(string type)
{ {
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower(); Type = type.ToLower();
} }

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
namespace Kyoo.Abstractions.Models.Attributes
{
/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SerializeAsAttribute : Attribute
{
/// <summary>
/// The format string to use.
/// </summary>
public string Format { get; }
/// <summary>
/// Create a new <see cref="SerializeAsAttribute"/> with the selected format.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <example>
/// The show's poster serialized uses this format string: <code>{HOST}/api/shows/{Slug}/poster</code>
/// </example>
/// <param name="format">The format to use</param>
public SerializeAsAttribute(string format)
{
Format = format;
}
}
}

View File

@ -105,9 +105,17 @@ namespace Kyoo.Abstractions.Models
return CreateReference(path, typeof(T)); return CreateReference(path, typeof(T));
} }
/// <summary>
/// Return a <see cref="ConfigurationReference"/> meaning that the given path is of any type.
/// It means that the type can't be edited.
/// </summary>
/// <param name="path">
/// The path that will be untyped (separated by ':' or "__". If empty, it will start at root).
/// </param>
/// <returns>A configuration reference representing a path of any type.</returns>
public static ConfigurationReference CreateUntyped(string path) public static ConfigurationReference CreateUntyped(string path)
{ {
return new(path, null); return new ConfigurationReference(path, null);
} }
} }
} }

View File

@ -18,8 +18,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions; using System.Linq.Expressions;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
@ -34,7 +34,8 @@ namespace Kyoo.Abstractions.Models
Show, Show,
/// <summary> /// <summary>
/// The <see cref="LibraryItem"/> is a Movie (a <see cref="Show"/> with <see cref="Models.Show.IsMovie"/> equals to true). /// The <see cref="LibraryItem"/> is a Movie (a <see cref="Show"/> with
/// <see cref="Models.Show.IsMovie"/> equals to true).
/// </summary> /// </summary>
Movie, Movie,
@ -48,7 +49,7 @@ namespace Kyoo.Abstractions.Models
/// A type union between <see cref="Show"/> and <see cref="Collection"/>. /// A type union between <see cref="Show"/> and <see cref="Collection"/>.
/// This is used to list content put inside a library. /// This is used to list content put inside a library.
/// </summary> /// </summary>
public class LibraryItem : IResource, IThumbnails public class LibraryItem : CustomTypeDescriptor, IResource, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -86,14 +87,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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.
/// </summary>
[SerializeAs("{HOST}/api/{Type:l}/{Slug}/poster")]
public string Poster => Images?.GetValueOrDefault(Models.Images.Poster);
/// <summary> /// <summary>
/// The type of this item (ether a collection, a show or a movie). /// The type of this item (ether a collection, a show or a movie).
/// </summary> /// </summary>
@ -169,5 +162,11 @@ namespace Kyoo.Abstractions.Models
Images = x.Images, Images = x.Images,
Type = ItemType.Collection Type = ItemType.Collection
}; };
/// <inheritdoc />
public override string GetClassName()
{
return Type.ToString();
}
} }
} }

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
@ -42,15 +41,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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.
/// </summary>
[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);
/// <summary> /// <summary>
/// The description of this collection. /// The description of this collection.
/// </summary> /// </summary>

View File

@ -127,15 +127,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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.
/// </summary>
[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);
/// <summary> /// <summary>
/// The title of this episode. /// The title of this episode.
/// </summary> /// </summary>

View File

@ -33,9 +33,8 @@ namespace Kyoo.Abstractions.Models
/// <remarks> /// <remarks>
/// An arbitrary index should not be used, instead use indexes from <see cref="Models.Images"/> /// An arbitrary index should not be used, instead use indexes from <see cref="Models.Images"/>
/// </remarks> /// </remarks>
/// <example>{"0": "example.com/dune/poster"}</example>
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
// TODO remove Posters properties add them via the json serializer for every IThumbnails
} }
/// <summary> /// <summary>
@ -63,5 +62,17 @@ namespace Kyoo.Abstractions.Models
/// A video of a few minutes that tease the content. /// A video of a few minutes that tease the content.
/// </summary> /// </summary>
public const int Trailer = 3; public const int Trailer = 3;
/// <summary>
/// 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.
/// </summary>
public static Dictionary<int, string> ImageName { get; } = new()
{
[Poster] = nameof(Poster),
[Thumbnail] = nameof(Thumbnail),
[Logo] = nameof(Logo),
[Trailer] = nameof(Trailer)
};
} }
} }

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
@ -41,15 +40,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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.
/// </summary>
[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);
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } [EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
@ -44,15 +43,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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.
/// </summary>
[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);
/// <summary> /// <summary>
/// The list of libraries that uses this provider. /// The list of libraries that uses this provider.
/// </summary> /// </summary>

View File

@ -98,15 +98,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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.
/// </summary>
[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);
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } [EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }

View File

@ -82,33 +82,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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.
/// </summary>
[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);
/// <summary>
/// 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.
/// </summary>
[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);
/// <summary>
/// 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.
/// </summary>
[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);
/// <summary> /// <summary>
/// True if this show represent a movie, false otherwise. /// True if this show represent a movie, false otherwise.
/// </summary> /// </summary>

View File

@ -52,7 +52,7 @@ namespace Kyoo.Abstractions.Models
Subtitle = 3, Subtitle = 3,
/// <summary> /// <summary>
/// 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. /// Only fonts are handled by kyoo but they are not saved to the database.
/// </summary> /// </summary>
Attachment = 4 Attachment = 4
@ -73,7 +73,7 @@ namespace Kyoo.Abstractions.Models
{ {
string type = Type.ToString().ToLower(); string type = Type.ToString().ToLower();
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; 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}"; 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}]"); "Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]");
} }
EpisodeSlug = match.Groups["ep"].Value; _episodeSlug = match.Groups["ep"].Value;
Language = match.Groups["lang"].Value; Language = match.Groups["lang"].Value;
if (Language == "und") if (Language == "und")
Language = null; Language = null;
@ -100,11 +100,6 @@ namespace Kyoo.Abstractions.Models
} }
} }
/// <summary>
/// The slug of the episode that contain this track. If this is not set, this track is ill-formed.
/// </summary>
[SerializeIgnore] public string EpisodeSlug { private get; set; }
/// <summary> /// <summary>
/// The title of the stream. /// The title of the stream.
/// </summary> /// </summary>
@ -153,7 +148,16 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The episode that uses this track. /// The episode that uses this track.
/// </summary> /// </summary>
[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;
}
}
/// <summary> /// <summary>
/// The index of this track on the episode. /// The index of this track on the episode.
@ -184,6 +188,17 @@ namespace Kyoo.Abstractions.Models
} }
} }
/// <summary>
/// The slug of the episode that contain this track. If this is not set, this track is ill-formed.
/// </summary>
[SerializeIgnore] private string _episodeSlug;
/// <summary>
/// The episode that uses this track.
/// This is the baking field of <see cref="Episode"/>.
/// </summary>
[SerializeIgnore] private Episode _episode;
// Converting mkv track language to c# system language tag. // Converting mkv track language to c# system language tag.
private static string _GetLanguage(string mkvLanguage) private static string _GetLanguage(string mkvLanguage)
{ {

View File

@ -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 <https://www.gnu.org/licenses/>.
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models.Utils
{
/// <summary>
/// A class containing constant numbers.
/// </summary>
public static class Constants
{
/// <summary>
/// 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.
/// </summary>
public const int AlternativeRoute = 1;
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
/// </summary>
public const string ResourcesGroup = "0:Resources";
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>.
/// It should be used for sub resources of kyoo that help define the main resources.
/// </summary>
public const string MetadataGroup = "1:Metadata";
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
/// </summary>
public const string WatchGroup = "2:Watch";
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
/// </summary>
public const string AdminGroup = "3:Admin";
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// 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 <see cref="IResource"/>.
/// </summary>
[TypeConverter(typeof(IdentifierConvertor))]
public class Identifier
{
/// <summary>
/// The ID of the resource or null if the slug is specified.
/// </summary>
private readonly int? _id;
/// <summary>
/// The slug of the resource or null if the id is specified.
/// </summary>
private readonly string _slug;
/// <summary>
/// Create a new <see cref="Identifier"/> for the given id.
/// </summary>
/// <param name="id">The id of the resource.</param>
public Identifier(int id)
{
_id = id;
}
/// <summary>
/// Create a new <see cref="Identifier"/> for the given slug.
/// </summary>
/// <param name="slug">The slug of the resource.</param>
public Identifier([NotNull] string slug)
{
if (slug == null)
throw new ArgumentNullException(nameof(slug));
_slug = slug;
}
/// <summary>
/// Pattern match out of the identifier to a resource.
/// </summary>
/// <param name="idFunc">The function to match the ID to a type <typeparamref name="T"/>.</param>
/// <param name="slugFunc">The function to match the slug to a type <typeparamref name="T"/>.</param>
/// <typeparam name="T">The return type that will be converted to from an ID or a slug.</typeparam>
/// <returns>
/// The result of the <paramref name="idFunc"/> or <paramref name="slugFunc"/> depending on the pattern.
/// </returns>
/// <example>
/// Example usage:
/// <code lang="csharp">
/// T ret = await identifier.Match(
/// id => _repository.GetOrDefault(id),
/// slug => _repository.GetOrDefault(slug)
/// );
/// </code>
/// </example>
public T Match<T>(Func<int, T> idFunc, Func<string, T> slugFunc)
{
return _id.HasValue
? idFunc(_id.Value)
: slugFunc(_slug);
}
/// <summary>
/// Match a custom type to an identifier. This can be used for wrapped resources (see example for more details).
/// </summary>
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
/// <typeparam name="T">The type to match against this identifier.</typeparam>
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
/// <example>
/// <code lang="csharp">
/// identifier.Matcher&lt;Season&gt;(x => x.ShowID, x => x.Show.Slug)
/// </code>
/// </example>
public Expression<Func<T, bool>> Matcher<T>(Expression<Func<T, int>> idGetter,
Expression<Func<T, string>> slugGetter)
{
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
return Expression.Lambda<Func<T, bool>>(equal, parameters);
}
/// <summary>
/// A matcher overload for nullable IDs. See
/// <see cref="Matcher{T}(System.Linq.Expressions.Expression{System.Func{T,int}},System.Linq.Expressions.Expression{System.Func{T,string}})"/>
/// for more details.
/// </summary>
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
/// <typeparam name="T">The type to match against this identifier.</typeparam>
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
public Expression<Func<T, bool>> Matcher<T>(Expression<Func<T, int?>> idGetter,
Expression<Func<T, string>> slugGetter)
{
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
return Expression.Lambda<Func<T, bool>>(equal, parameters);
}
/// <summary>
/// Return true if this <see cref="Identifier"/> match a resource.
/// </summary>
/// <param name="resource">The resource to match</param>
/// <returns>
/// <c>true</c> if the <paramref name="resource"/> match this identifier, <c>false</c> otherwise.
/// </returns>
public bool IsSame(IResource resource)
{
return Match(
id => resource.ID == id,
slug => resource.Slug == slug
);
}
/// <summary>
/// Return an expression that return true if this <see cref="Identifier"/> match a given resource.
/// </summary>
/// <typeparam name="T">The type of resource to match against.</typeparam>
/// <returns>
/// <c>true</c> if the given resource match this identifier, <c>false</c> otherwise.
/// </returns>
public Expression<Func<T, bool>> IsSame<T>()
where T : IResource
{
return _id.HasValue
? x => x.ID == _id.Value
: x => x.Slug == _slug;
}
/// <summary>
/// Return an expression that return true if this <see cref="Identifier"/> is containing in a collection.
/// </summary>
/// <param name="listGetter">An expression to retrieve the list to check.</param>
/// <typeparam name="T">The type that contain the list to check.</typeparam>
/// <typeparam name="T2">The type of resource to check this identifier against.</typeparam>
/// <returns>An expression to check if this <see cref="Identifier"/> is contained.</returns>
public Expression<Func<T, bool>> IsContainedIn<T, T2>(Expression<Func<T, IEnumerable<T2>>> 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<T2>());
return Expression.Lambda<Func<T, bool>>(call, listGetter.Parameters);
}
/// <inheritdoc />
public override string ToString()
{
return _id.HasValue
? _id.Value.ToString()
: _slug;
}
/// <summary>
/// A custom <see cref="TypeConverter"/> used to convert int or strings to an <see cref="Identifier"/>.
/// </summary>
public class IdentifierConvertor : TypeConverter
{
/// <inheritdoc />
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(int) || sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
/// <inheritdoc />
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);
}
}
}
}

View File

@ -31,14 +31,14 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Where to start? Using the given sort. /// Where to start? Using the given sort.
/// </summary> /// </summary>
public int AfterID { get; } public int? AfterID { get; }
/// <summary> /// <summary>
/// Create a new <see cref="Pagination"/> instance. /// Create a new <see cref="Pagination"/> instance.
/// </summary> /// </summary>
/// <param name="count">Set the <see cref="Count"/> value</param> /// <param name="count">Set the <see cref="Count"/> value</param>
/// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param> /// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param>
public Pagination(int count, int afterID = 0) public Pagination(int count, int? afterID = null)
{ {
Count = count; Count = count;
AfterID = afterID; AfterID = afterID;

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Linq;
using JetBrains.Annotations;
namespace Kyoo.Abstractions.Models.Utils
{
/// <summary>
/// The list of errors that where made in the request.
/// </summary>
public class RequestError
{
/// <summary>
/// The list of errors that where made in the request.
/// </summary>
/// <example><c>["InvalidFilter: no field 'startYear' on a collection"]</c></example>
[NotNull] public string[] Errors { get; set; }
/// <summary>
/// Create a new <see cref="RequestError"/> with one error.
/// </summary>
/// <param name="error">The error to specify in the response.</param>
public RequestError([NotNull] string error)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
Errors = new[] { error };
}
/// <summary>
/// Create a new <see cref="RequestError"/> with multiple errors.
/// </summary>
/// <param name="errors">The errors to specify in the response.</param>
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;
}
}
}

View File

@ -18,6 +18,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; 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. /// Information about tracks and display information that could be used by the player.
/// This contains mostly data from an <see cref="Episode"/> with another form. /// This contains mostly data from an <see cref="Episode"/> with another form.
/// </summary> /// </summary>
public class WatchItem public class WatchItem : CustomTypeDescriptor, IThumbnails
{ {
/// <summary> /// <summary>
/// The ID of the episode associated with this item. /// The ID of the episode associated with this item.
@ -101,26 +102,8 @@ namespace Kyoo.Abstractions.Models
/// </summary> /// </summary>
public bool IsMovie { get; set; } public bool IsMovie { get; set; }
/// <summary> /// <inheritdoc />
/// The path of this item's poster. public Dictionary<int, string> Images { get; set; }
/// By default, the http path for the poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; }
/// <summary>
/// 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.
/// </summary>
[SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; }
/// <summary>
/// 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.
/// </summary>
[SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; }
/// <summary> /// <summary>
/// The container of the video file of this episode. /// The container of the video file of this episode.
@ -158,36 +141,50 @@ namespace Kyoo.Abstractions.Models
/// <returns>A new WatchItem representing the given episode.</returns> /// <returns>A new WatchItem representing the given episode.</returns>
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library) public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library)
{ {
Episode previous = null;
Episode next = null;
await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Show);
await library.Load(ep, x => x.Tracks); 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) if (ep.AbsoluteNumber != null)
previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value - 1);
else if (ep.SeasonNumber > 1)
{ {
previous = (await library.GetAll(x => x.ShowID == ep.ShowID previous = await library.GetOrDefault(
&& x.SeasonNumber == ep.SeasonNumber.Value - 1, x => x.ShowID == ep.ShowID && x.AbsoluteNumber < ep.AbsoluteNumber,
limit: 1, new Sort<Episode>(x => x.AbsoluteNumber, true)
sort: new Sort<Episode>(x => x.EpisodeNumber, true)) );
).FirstOrDefault(); next = await library.GetOrDefault(
x => x.ShowID == ep.ShowID && x.AbsoluteNumber > ep.AbsoluteNumber,
new Sort<Episode>(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<Episode>(x => x.EpisodeNumber, true)
);
previous ??= await library.GetOrDefault(
x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber - 1,
new Sort<Episode>(x => x.EpisodeNumber, true)
);
if (ep.EpisodeNumber >= await library.GetCount<Episode>(x => x.SeasonID == ep.SeasonID)) next = await library.GetOrDefault(
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value + 1, 1); x => x.ShowID == ep.ShowID
else && x.SeasonNumber == ep.SeasonNumber
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value + 1); && x.EpisodeNumber > ep.EpisodeNumber,
} new Sort<Episode>(x => x.EpisodeNumber)
else if (!ep.Show.IsMovie && ep.AbsoluteNumber != null) );
{ next ??= await library.GetOrDefault(
previous = await library.GetOrDefault<Episode>(x => x.ShowID == ep.ShowID x => x.ShowID == ep.ShowID
&& x.AbsoluteNumber == ep.EpisodeNumber + 1); && x.SeasonNumber == ep.SeasonNumber + 1,
next = await library.GetOrDefault<Episode>(x => x.ShowID == ep.ShowID new Sort<Episode>(x => x.EpisodeNumber)
&& x.AbsoluteNumber == ep.AbsoluteNumber + 1); );
}
} }
return new WatchItem return new WatchItem
@ -202,6 +199,7 @@ namespace Kyoo.Abstractions.Models
Title = ep.Title, Title = ep.Title,
ReleaseDate = ep.ReleaseDate, ReleaseDate = ep.ReleaseDate,
Path = ep.Path, Path = ep.Path,
Images = ep.Show.Images,
Container = PathIO.GetExtension(ep.Path)![1..], Container = PathIO.GetExtension(ep.Path)![1..],
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
@ -239,5 +237,17 @@ namespace Kyoo.Abstractions.Models
return Array.Empty<Chapter>(); return Array.Empty<Chapter>();
} }
} }
/// <inheritdoc />
public override string GetClassName()
{
return nameof(Show);
}
/// <inheritdoc />
public override string GetComponentName()
{
return ShowSlug;
}
} }
} }

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using Autofac; using Autofac;
using Autofac.Builder; using Autofac.Builder;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
@ -96,9 +97,10 @@ namespace Kyoo.Abstractions
/// </summary> /// </summary>
/// <param name="configuration">The configuration instance</param> /// <param name="configuration">The configuration instance</param>
/// <returns>The public URl of kyoo (without a slash at the end)</returns> /// <returns>The public URl of kyoo (without a slash at the end)</returns>
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);
} }
} }
} }

View File

@ -425,6 +425,11 @@ namespace Kyoo.Utils
return (T)method.MakeGenericMethod(types).Invoke(instance, args.ToArray()); return (T)method.MakeGenericMethod(types).Invoke(instance, args.ToArray());
} }
/// <summary>
/// Convert a dictionary to a query string.
/// </summary>
/// <param name="query">The list of query parameters.</param>
/// <returns>A valid query string with all items in the dictionary.</returns>
public static string ToQueryString(this Dictionary<string, string> query) public static string ToQueryString(this Dictionary<string, string> query)
{ {
if (!query.Any()) if (!query.Any())
@ -432,6 +437,11 @@ namespace Kyoo.Utils
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
} }
/// <summary>
/// Rethrow the exception without modifying the stack trace.
/// This is similar to the <c>rethrow;</c> code but is useful when the exception is not in a catch block.
/// </summary>
/// <param name="ex">The exception to rethrow.</param>
[System.Diagnostics.CodeAnalysis.DoesNotReturn] [System.Diagnostics.CodeAnalysis.DoesNotReturn]
public static void ReThrow([NotNull] this Exception ex) public static void ReThrow([NotNull] this Exception ex)
{ {

View File

@ -104,7 +104,7 @@ namespace Kyoo.Authentication
DefaultCorsPolicyService cors = new(_logger) DefaultCorsPolicyService cors = new(_logger)
{ {
AllowedOrigins = { new Uri(_configuration.GetPublicUrl()).GetLeftPart(UriPartial.Authority) } AllowedOrigins = { _configuration.GetPublicUrl().GetLeftPart(UriPartial.Authority) }
}; };
builder.RegisterInstance(cors).As<ICorsPolicyService>().SingleInstance(); builder.RegisterInstance(cors).As<ICorsPolicyService>().SingleInstance();
} }
@ -112,7 +112,7 @@ namespace Kyoo.Authentication
/// <inheritdoc /> /// <inheritdoc />
public void Configure(IServiceCollection services) public void Configure(IServiceCollection services)
{ {
string publicUrl = _configuration.GetPublicUrl(); Uri publicUrl = _configuration.GetPublicUrl();
if (_environment.IsDevelopment()) if (_environment.IsDevelopment())
IdentityModelEventSource.ShowPII = true; IdentityModelEventSource.ShowPII = true;
@ -136,7 +136,7 @@ namespace Kyoo.Authentication
services.AddIdentityServer(options => services.AddIdentityServer(options =>
{ {
options.IssuerUri = publicUrl; options.IssuerUri = publicUrl.ToString();
options.UserInteraction.LoginUrl = $"{publicUrl}/login"; options.UserInteraction.LoginUrl = $"{publicUrl}/login";
options.UserInteraction.ErrorUrl = $"{publicUrl}/error"; options.UserInteraction.ErrorUrl = $"{publicUrl}/error";
options.UserInteraction.LogoutUrl = $"{publicUrl}/logout"; options.UserInteraction.LogoutUrl = $"{publicUrl}/logout";
@ -151,7 +151,7 @@ namespace Kyoo.Authentication
services.AddAuthentication() services.AddAuthentication()
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {
options.Authority = publicUrl; options.Authority = publicUrl.ToString();
options.Audience = "kyoo"; options.Audience = "kyoo";
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
}); });
@ -189,7 +189,7 @@ namespace Kyoo.Authentication
{ {
app.Use((ctx, next) => app.Use((ctx, next) =>
{ {
ctx.SetIdentityServerOrigin(_configuration.GetPublicUrl()); ctx.SetIdentityServerOrigin(_configuration.GetPublicUrl().ToString());
return next(); return next();
}); });
app.UseIdentityServer(); app.UseIdentityServer();

View File

@ -23,6 +23,9 @@ using IdentityModel;
namespace Kyoo.Authentication namespace Kyoo.Authentication
{ {
/// <summary>
/// Some functions to handle password management.
/// </summary>
public static class PasswordUtils public static class PasswordUtils
{ {
/// <summary> /// <summary>

View File

@ -61,7 +61,7 @@ namespace Kyoo.Authentication
/// <inheritdoc /> /// <inheritdoc />
public IFilterMetadata Create(PartialPermissionAttribute attribute) 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);
} }
/// <summary> /// <summary>
@ -109,15 +109,24 @@ namespace Kyoo.Authentication
/// Create a new permission validator with the given options. /// Create a new permission validator with the given options.
/// </summary> /// </summary>
/// <param name="partialInfo">The partial permission to validate.</param> /// <param name="partialInfo">The partial permission to validate.</param>
/// <param name="group">The group of the permission.</param>
/// <param name="options">The option containing default values.</param> /// <param name="options">The option containing default values.</param>
public PermissionValidatorFilter(object partialInfo, IOptionsMonitor<PermissionOption> options) public PermissionValidatorFilter(object partialInfo, Group? group, IOptionsMonitor<PermissionOption> options)
{ {
if (partialInfo is Kind kind) switch (partialInfo)
_kind = kind; {
else if (partialInfo is string perm) case Kind kind:
_permission = perm; _kind = kind;
else break;
throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind."); 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; _options = options;
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A one time access token
/// </summary>
public class OtacResponse
{
/// <summary>
/// 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.
/// </summary>
public string OTAC { get; set; }
/// <summary>
/// Create a new <see cref="OtacResponse"/>.
/// </summary>
/// <param name="otac">The one time access token.</param>
public OtacResponse(string otac)
{
OTAC = otac;
}
}
}

View File

@ -28,7 +28,9 @@ using IdentityServer4.Models;
using IdentityServer4.Services; using IdentityServer4.Services;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Authentication.Models; using Kyoo.Authentication.Models;
using Kyoo.Authentication.Models.DTO; using Kyoo.Authentication.Models.DTO;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
@ -36,15 +38,19 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Authentication.Views namespace Kyoo.Authentication.Views
{ {
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
[Route("api/account")] /// TODO document this well.
[Route("api/accounts")] [Route("api/accounts")]
[Route("api/account", Order = AlternativeRoute)]
[ApiController] [ApiController]
[ApiDefinition("Account")]
public class AccountApi : Controller, IProfileService public class AccountApi : Controller, IProfileService
{ {
/// <summary> /// <summary>
@ -78,12 +84,17 @@ namespace Kyoo.Authentication.Views
} }
/// <summary> /// <summary>
/// Register a new user and return a OTAC to connect to it. /// Register
/// </summary> /// </summary>
/// <remarks>
/// Register a new user and return a OTAC to connect to it.
/// </remarks>
/// <param name="request">The DTO register request</param> /// <param name="request">The DTO register request</param>
/// <returns>A OTAC to connect to this new account</returns> /// <returns>A OTAC to connect to this new account</returns>
[HttpPost("register")] [HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
public async Task<ActionResult<OtacResponse>> Register([FromBody] RegisterRequest request)
{ {
User user = request.ToUser(); User user = request.ToUser();
user.Permissions = _options.Value.Permissions.NewUser; user.Permissions = _options.Value.Permissions.NewUser;
@ -96,10 +107,10 @@ namespace Kyoo.Authentication.Views
} }
catch (DuplicatedItemException) 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"]));
} }
/// <summary> /// <summary>
@ -119,8 +130,11 @@ namespace Kyoo.Authentication.Views
} }
/// <summary> /// <summary>
/// Login the user. /// Login
/// </summary> /// </summary>
/// <remarks>
/// Login the current session.
/// </remarks>
/// <param name="login">The DTO login request</param> /// <param name="login">The DTO login request</param>
/// <returns>TODO</returns> /// <returns>TODO</returns>
[HttpPost("login")] [HttpPost("login")]
@ -177,6 +191,7 @@ namespace Kyoo.Authentication.Views
} }
/// <inheritdoc /> /// <inheritdoc />
[ApiExplorerSettings(IgnoreApi = true)]
public async Task GetProfileDataAsync(ProfileDataRequestContext context) public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{ {
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
@ -187,6 +202,7 @@ namespace Kyoo.Authentication.Views
} }
/// <inheritdoc /> /// <inheritdoc />
[ApiExplorerSettings(IgnoreApi = true)]
public async Task IsActiveAsync(IsActiveContext context) public async Task IsActiveAsync(IsActiveContext context)
{ {
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));

View File

@ -283,7 +283,7 @@ namespace Kyoo.Core
builder.ReadFrom.Services(services); builder.ReadFrom.Services(services);
const string template = 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}"; + "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}";
if (SystemdHelpers.IsSystemdService()) if (SystemdHelpers.IsSystemdService())

View File

@ -32,6 +32,11 @@ using Newtonsoft.Json.Linq;
namespace Kyoo.Core.Controllers namespace Kyoo.Core.Controllers
{ {
/// <summary>
/// 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 <see cref="ConfigurationReference"/>.
/// </summary>
public class ConfigurationManager : IConfigurationManager public class ConfigurationManager : IConfigurationManager
{ {
/// <summary> /// <summary>

View File

@ -214,5 +214,19 @@ namespace Kyoo.Core.Controllers
}; };
return await CreateDirectory(path); return await CreateDirectory(path);
} }
/// <inheritdoc />
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _);
return fs.ExtractInfos(episode, reExtract);
}
/// <inheritdoc />
public IActionResult Transmux(Episode episode)
{
IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _);
return fs.Transmux(episode);
}
} }
} }

View File

@ -110,6 +110,18 @@ namespace Kyoo.Core.Controllers
throw new NotSupportedException("Extras can not be stored inside an http filesystem."); throw new NotSupportedException("Extras can not be stored inside an http filesystem.");
} }
/// <inheritdoc />
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
throw new NotSupportedException("Extracting infos is not supported on an http filesystem.");
}
/// <inheritdoc />
public IActionResult Transmux(Episode episode)
{
throw new NotSupportedException("Transmuxing is not supported on an http filesystem.");
}
/// <summary> /// <summary>
/// An <see cref="IActionResult"/> to proxy an http request. /// An <see cref="IActionResult"/> to proxy an http request.
/// </summary> /// </summary>

View File

@ -41,6 +41,11 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly IContentTypeProvider _provider; private readonly IContentTypeProvider _provider;
/// <summary>
/// The transcoder of local files.
/// </summary>
private readonly ITranscoder _transcoder;
/// <summary> /// <summary>
/// Options to check if the metadata should be kept in the show directory or in a kyoo's directory. /// Options to check if the metadata should be kept in the show directory or in a kyoo's directory.
/// </summary> /// </summary>
@ -51,10 +56,14 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
/// <param name="options">The options to use.</param> /// <param name="options">The options to use.</param>
/// <param name="provider">An extension provider to get content types from files extensions.</param> /// <param name="provider">An extension provider to get content types from files extensions.</param>
public LocalFileSystem(IOptionsMonitor<BasicOptions> options, IContentTypeProvider provider) /// <param name="transcoder">The transcoder of local files.</param>
public LocalFileSystem(IOptionsMonitor<BasicOptions> options,
IContentTypeProvider provider,
ITranscoder transcoder)
{ {
_options = options; _options = options;
_provider = provider; _provider = provider;
_transcoder = transcoder;
} }
/// <summary> /// <summary>
@ -155,5 +164,17 @@ namespace Kyoo.Core.Controllers
_ => null _ => null
}); });
} }
/// <inheritdoc />
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
return _transcoder.ExtractInfos(episode, reExtract);
}
/// <inheritdoc />
public IActionResult Transmux(Episode episode)
{
return _transcoder.Transmux(episode);
}
} }
} }

View File

@ -16,17 +16,25 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Threading.Tasks; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Abstractions.Models; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Kyoo.Abstractions.Controllers namespace Kyoo.Core.Controllers
{ {
public interface ITranscoder /// <summary>
/// The route constraint that goes with the <see cref="Identifier"/>.
/// </summary>
public class IdentifierRouteConstraint : IRouteConstraint
{ {
Task<Track[]> ExtractInfos(Episode episode, bool reextract); /// <inheritdoc />
public bool Match(HttpContext httpContext,
Task<string> Transmux(Episode episode); IRouter route,
string routeKey,
Task<string> Transcode(Episode episode); RouteValueDictionary values,
RouteDirection routeDirection)
{
return values.ContainsKey(routeKey);
}
} }
} }

View File

@ -29,6 +29,9 @@ using Kyoo.Utils;
namespace Kyoo.Core.Controllers namespace Kyoo.Core.Controllers
{ {
/// <summary>
/// An class to interact with the database. Every repository is mapped through here.
/// </summary>
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
/// <summary> /// <summary>
@ -163,10 +166,10 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where) public async Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T> sortBy)
where T : class, IResource where T : class, IResource
{ {
return await GetRepository<T>().GetOrDefault(where); return await GetRepository<T>().GetOrDefault(where, sortBy);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -29,6 +29,10 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
public class PassthroughPermissionValidator : IPermissionValidator public class PassthroughPermissionValidator : IPermissionValidator
{ {
/// <summary>
/// Create a new <see cref="PassthroughPermissionValidator"/>.
/// </summary>
/// <param name="logger">The logger used to warn that no real permission validator exists.</param>
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor", [SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor",
Justification = "ILogger should include the typeparam for context.")] Justification = "ILogger should include the typeparam for context.")]
public PassthroughPermissionValidator(ILogger<PassthroughPermissionValidator> logger) public PassthroughPermissionValidator(ILogger<PassthroughPermissionValidator> logger)

View File

@ -169,7 +169,6 @@ namespace Kyoo.Core.Controllers
resource.Tracks = await resource.Tracks.SelectAsync(x => resource.Tracks = await resource.Tracks.SelectAsync(x =>
{ {
x.Episode = resource; x.Episode = resource;
x.EpisodeSlug = resource.Slug;
return _tracks.Create(x); return _tracks.Create(x);
}).ToListAsync(); }).ToListAsync();
_database.Tracks.AttachRange(resource.Tracks); _database.Tracks.AttachRange(resource.Tracks);

View File

@ -115,9 +115,12 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual Task<T> GetOrDefault(Expression<Func<T, bool>> where) public virtual Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default)
{ {
return Database.Set<T>().FirstOrDefaultAsync(where); IQueryable<T> query = Database.Set<T>();
Expression<Func<T, object>> sortKey = sortBy.Key ?? DefaultSort;
query = sortBy.Descendant ? query.OrderByDescending(sortKey) : query.OrderBy(sortKey);
return query.FirstOrDefaultAsync(where);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -179,9 +182,9 @@ namespace Kyoo.Core.Controllers
query = sort.Descendant ? query.OrderByDescending(sortKey) : query.OrderBy(sortKey); 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); Expression key = Expression.Constant(sortKey.Compile()(after), sortExpression.Type);
query = query.Where(Expression.Lambda<Func<TValue, bool>>( query = query.Where(Expression.Lambda<Func<TValue, bool>>(
ApiHelper.StringCompatibleExpression(Expression.GreaterThan, sortExpression, key), ApiHelper.StringCompatibleExpression(Expression.GreaterThan, sortExpression, key),

View File

@ -17,34 +17,70 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Core.Models.Options; using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
// We use threads so tasks are not always awaited.
#pragma warning disable 4014
namespace Kyoo.Core.Controllers namespace Kyoo.Core.Controllers
{ {
/// <summary>
/// The transcoder used by the <see cref="LocalFileSystem"/>.
/// </summary>
public class Transcoder : ITranscoder public class Transcoder : ITranscoder
{ {
/// <summary>
/// The class that interact with the transcoder written in C.
/// </summary>
private static class TranscoderAPI private static class TranscoderAPI
{ {
/// <summary>
/// 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.
/// </summary>
private const string TranscoderPath = "transcoder"; private const string TranscoderPath = "transcoder";
/// <summary>
/// Initialize the C library, setup the logger and return the size of a <see cref="Models.Watch.Stream"/>.
/// </summary>
/// <returns>The size of a <see cref="Models.Watch.Stream"/></returns>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern int init(); private static extern int init();
/// <summary>
/// Initialize the C library, setup the logger and return the size of a <see cref="Models.Watch.Stream"/>.
/// </summary>
/// <returns>The size of a <see cref="Models.Watch.Stream"/></returns>
public static int Init() => init(); public static int Init() => init();
/// <summary>
/// Transmux the file at the specified path. The path must be a local one with '/' as a separator.
/// </summary>
/// <param name="path">The path of a local file with '/' as a separators.</param>
/// <param name="outPath">The path of the hls output file.</param>
/// <param name="playableDuration">
/// The number of seconds currently playable. This is incremented as the file gets transmuxed.
/// </param>
/// <returns><c>0</c> on success, non 0 on failure.</returns>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl, [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] 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);
/// <summary>
/// Transmux the file at the specified path. The path must be a local one.
/// </summary>
/// <param name="path">The path of a local file.</param>
/// <param name="outPath">The path of the hls output file.</param>
/// <param name="playableDuration">
/// The number of seconds currently playable. This is incremented as the file gets transmuxed.
/// </param>
/// <returns><c>0</c> on success, non 0 on failure.</returns>
public static int Transmux(string path, string outPath, out float playableDuration) public static int Transmux(string path, string outPath, out float playableDuration)
{ {
path = path.Replace('\\', '/'); path = path.Replace('\\', '/');
@ -52,24 +88,47 @@ namespace Kyoo.Core.Controllers
return transmux(path, outPath, out playableDuration); return transmux(path, outPath, out playableDuration);
} }
/// <summary>
/// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file.
/// </summary>
/// <param name="path">
/// The path of the video file to analyse. This must be a local path with '/' as a separator.
/// </param>
/// <param name="outPath">The directory that will be used to store extracted files.</param>
/// <param name="length">The size of the returned array.</param>
/// <param name="trackCount">The number of tracks in the returned array.</param>
/// <param name="reExtract">Should the cache be invalidated and information re-extracted or not?</param>
/// <returns>A pointer to an array of <see cref="Models.Watch.Stream"/></returns>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl, [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern IntPtr extract_infos(string path, private static extern IntPtr extract_infos(string path,
string outpath, string outPath,
out uint length, out uint length,
out uint trackCount, out uint trackCount,
bool reextracct); bool reExtract);
/// <summary>
/// An helper method to free an array of <see cref="Models.Watch.Stream"/>.
/// </summary>
/// <param name="streams">A pointer to the first element of the array</param>
/// <param name="count">The number of items in the array.</param>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern void free_streams(IntPtr streams, uint count); private static extern void free_streams(IntPtr streams, uint count);
public static Track[] ExtractInfos(string path, string outPath, bool reextract) /// <summary>
/// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file.
/// </summary>
/// <param name="path">The path of the video file to analyse. This must be a local path.</param>
/// <param name="outPath">The directory that will be used to store extracted files.</param>
/// <param name="reExtract">Should the cache be invalidated and information re-extracted or not?</param>
/// <returns>An array of <see cref="Track"/>.</returns>
public static Track[] ExtractInfos(string path, string outPath, bool reExtract)
{ {
path = path.Replace('\\', '/'); path = path.Replace('\\', '/');
outPath = outPath.Replace('\\', '/'); outPath = outPath.Replace('\\', '/');
int size = Marshal.SizeOf<Models.Watch.Stream>(); int size = Marshal.SizeOf<Models.Watch.Stream>();
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; IntPtr streamsPtr = ptr;
Track[] tracks; Track[] tracks;
@ -98,67 +157,161 @@ namespace Kyoo.Core.Controllers
} }
} }
public class BadTranscoderException : Exception { } /// <summary>
/// The file system used to retrieve the extra directory of shows to know where to extract information.
/// </summary>
private readonly IFileSystem _files; private readonly IFileSystem _files;
private readonly IOptions<BasicOptions> _options;
private readonly Lazy<ILibraryManager> _library;
public Transcoder(IFileSystem files, IOptions<BasicOptions> options, Lazy<ILibraryManager> library) /// <summary>
/// Options to know where to cache transmuxed/transcoded episodes.
/// </summary>
private readonly IOptions<BasicOptions> _options;
/// <summary>
/// The logger to use. This is also used by the wrapped C library.
/// </summary>
private readonly ILogger<Transcoder> _logger;
/// <summary>
/// Create a new <see cref="Transcoder"/>.
/// </summary>
/// <param name="files">
/// The file system used to retrieve the extra directory of shows to know where to extract information.
/// </param>
/// <param name="options">Options to know where to cache transmuxed/transcoded episodes.</param>
/// <param name="logger">The logger to use. This is also used by the wrapped C library.</param>
public Transcoder(IFileSystem files, IOptions<BasicOptions> options, ILogger<Transcoder> logger)
{ {
_files = files; _files = files;
_options = options; _options = options;
_library = library; _logger = logger;
if (TranscoderAPI.Init() != Marshal.SizeOf<Models.Watch.Stream>()) if (TranscoderAPI.Init() != Marshal.SizeOf<Models.Watch.Stream>())
throw new BadTranscoderException(); _logger.LogCritical("The transcoder library could not be initialized correctly");
} }
public async Task<Track[]> ExtractInfos(Episode episode, bool reextract) /// <inheritdoc />
public async Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{ {
await _library.Value.Load(episode, x => x.Show); string dir = await _files.GetExtraDirectory(episode);
string dir = await _files.GetExtraDirectory(episode.Show);
if (dir == null) if (dir == null)
throw new ArgumentException("Invalid path."); throw new ArgumentException("Invalid path.");
return await Task.Factory.StartNew( return await Task.Factory.StartNew(
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract), () => TranscoderAPI.ExtractInfos(episode.Path, dir, reExtract),
TaskCreationOptions.LongRunning); TaskCreationOptions.LongRunning
);
} }
public async Task<string> Transmux(Episode episode) /// <inheritdoc />
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 folder = Path.Combine(_options.Value.TransmuxPath, episode.Slug);
string manifest = Path.Combine(folder, episode.Slug + ".m3u8"); string manifest = Path.GetFullPath(Path.Combine(folder, episode.Slug + ".m3u8"));
float playableDuration = 0;
bool transmuxFailed = false;
try try
{ {
Directory.CreateDirectory(folder); Directory.CreateDirectory(folder);
if (File.Exists(manifest)) if (File.Exists(manifest))
return manifest; return new PhysicalFileResult(manifest, "application/x-mpegurl");
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config."); _logger.LogCritical("Access to the path {Manifest} is denied. " +
return null; "Please change your transmux path in the config", manifest);
return new StatusCodeResult(500);
} }
Task.Factory.StartNew(() => return new TransmuxResult(episode.Path, manifest, _logger);
{
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;
} }
public Task<string> Transcode(Episode episode) /// <summary>
/// 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.
/// </summary>
private class TransmuxResult : IActionResult
{ {
return Task.FromResult<string>(null); // Not implemented yet. /// <summary>
/// The path of the episode to transmux. It must be a local one.
/// </summary>
private readonly string _path;
/// <summary>
/// The path of the manifest file to create. It must be a local one.
/// </summary>
private readonly string _manifest;
/// <summary>
/// The logger to use in case of issue.
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// Create a new <see cref="TransmuxResult"/>.
/// </summary>
/// <param name="path">The path of the episode to transmux. It must be a local one.</param>
/// <param name="manifest">The path of the manifest file to create. It must be a local one.</param>
/// <param name="logger">The logger to use in case of issue.</param>
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
/// <inheritdoc />
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
} }
} }
/// <summary>
/// The transcoder used by the <see cref="LocalFileSystem"/>. This is on a different interface than the file system
/// to offset the work.
/// </summary>
public interface ITranscoder
{
/// <summary>
/// Retrieve tracks for a specific episode.
/// Subtitles, chapters and fonts should also be extracted and cached when calling this method.
/// </summary>
/// <param name="episode">The episode to retrieve tracks for.</param>
/// <param name="reExtract">Should the cache be invalidated and subtitles and others be re-extracted?</param>
/// <returns>The list of tracks available for this episode.</returns>
Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract);
/// <summary>
/// Transmux the selected episode to hls.
/// </summary>
/// <param name="episode">The episode to transmux.</param>
/// <returns>The master file (m3u8) of the transmuxed hls file.</returns>
IActionResult Transmux(Episode episode);
}
} }

View File

@ -18,24 +18,28 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Autofac; using Autofac;
using Autofac.Core; using Autofac.Core;
using Autofac.Core.Registration; using Autofac.Core.Registration;
using Autofac.Extras.AttributeMetadata; using Autofac.Extras.AttributeMetadata;
using Kyoo.Abstractions; using Kyoo.Abstractions;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Core.Api; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Controllers; using Kyoo.Core.Controllers;
using Kyoo.Core.Models.Options; using Kyoo.Core.Models.Options;
using Kyoo.Core.Tasks; using Kyoo.Core.Tasks;
using Kyoo.Database; using Kyoo.Database;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Serilog; using Serilog;
using IMetadataProvider = Kyoo.Abstractions.Controllers.IMetadataProvider; using IMetadataProvider = Kyoo.Abstractions.Controllers.IMetadataProvider;
using JsonOptions = Kyoo.Core.Api.JsonOptions;
namespace Kyoo.Core namespace Kyoo.Core
{ {
@ -63,20 +67,6 @@ namespace Kyoo.Core
{ "logging", null } { "logging", null }
}; };
/// <summary>
/// The configuration to use.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// Create a new core module instance and use the given configuration.
/// </summary>
/// <param name="configuration">The configuration to use</param>
public CoreModule(IConfiguration configuration)
{
_configuration = configuration;
}
/// <inheritdoc /> /// <inheritdoc />
public void Configure(ContainerBuilder builder) public void Configure(ContainerBuilder builder)
{ {
@ -136,16 +126,31 @@ namespace Kyoo.Core
/// <inheritdoc /> /// <inheritdoc />
public void Configure(IServiceCollection services) public void Configure(IServiceCollection services)
{ {
string publicUrl = _configuration.GetPublicUrl(); services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
services.AddMvc().AddControllersAsServices(); services.AddMvcCore()
services.AddControllers() .AddNewtonsoftJson()
.AddNewtonsoftJson(x => .AddDataAnnotations()
.AddControllersAsServices()
.AddApiExplorer()
.ConfigureApiBehaviorOptions(options =>
{ {
x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); options.SuppressMapClientErrors = true;
x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); 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<RouteOptions>(x =>
{
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
});
services.AddResponseCompression(x => services.AddResponseCompression(x =>
{ {
x.EnableForHttps = true; x.EnableForHttps = true;

View File

@ -22,6 +22,9 @@ using Newtonsoft.Json;
namespace Kyoo.Core namespace Kyoo.Core
{ {
/// <summary>
/// A class containing helper methods.
/// </summary>
public static class Helper public static class Helper
{ {
/// <summary> /// <summary>

View File

@ -1,13 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<TranscoderRoot>../Kyoo.Transcoder/</TranscoderRoot>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
<AssemblyName>Kyoo.Core</AssemblyName>
<RootNamespace>Kyoo.Core</RootNamespace>
<TranscoderRoot>../Kyoo.Transcoder/</TranscoderRoot>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@ -41,6 +38,7 @@
<ProjectReference Include="../Kyoo.SqLite/Kyoo.SqLite.csproj" /> <ProjectReference Include="../Kyoo.SqLite/Kyoo.SqLite.csproj" />
<ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj" /> <ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj" />
<ProjectReference Include="../Kyoo.WebApp/Kyoo.WebApp.csproj" /> <ProjectReference Include="../Kyoo.WebApp/Kyoo.WebApp.csproj" />
<ProjectReference Include="../Kyoo.Swagger/Kyoo.Swagger.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true' And !Exists('$(TranscoderRoot)/build/$(TranscoderBinary)')"> <Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true' And !Exists('$(TranscoderRoot)/build/$(TranscoderBinary)')">
@ -62,6 +60,7 @@
<ItemGroup> <ItemGroup>
<Content Include="../../LICENSE" CopyToOutputDirectory="Always" Visible="false" /> <Content Include="../../LICENSE" CopyToOutputDirectory="Always" Visible="false" />
<Content Include="../../AUTHORS.md" CopyToOutputDirectory="Always" Visible="false" />
<Content Include="settings.json" CopyToOutputDirectory="PreserveNewest" /> <Content Include="settings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -19,6 +19,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using Autofac; using Autofac;
using Kyoo.Abstractions; using Kyoo.Abstractions;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
@ -28,6 +29,7 @@ using Kyoo.Core.Models.Options;
using Kyoo.Core.Tasks; using Kyoo.Core.Tasks;
using Kyoo.Postgresql; using Kyoo.Postgresql;
using Kyoo.SqLite; using Kyoo.SqLite;
using Kyoo.Swagger;
using Kyoo.TheMovieDb; using Kyoo.TheMovieDb;
using Kyoo.TheTvdb; using Kyoo.TheTvdb;
using Kyoo.Utils; using Kyoo.Utils;
@ -75,7 +77,8 @@ namespace Kyoo.Core
typeof(PostgresModule), typeof(PostgresModule),
typeof(SqLiteModule), typeof(SqLiteModule),
typeof(PluginTvdb), typeof(PluginTvdb),
typeof(PluginTmdb) typeof(PluginTmdb),
typeof(SwaggerModule)
); );
} }
@ -106,6 +109,9 @@ namespace Kyoo.Core
/// <param name="services">The service collection to fill.</param> /// <param name="services">The service collection to fill.</param>
public void ConfigureServices(IServiceCollection services) 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()) foreach (IPlugin plugin in _plugins.GetAllPlugins())
plugin.Configure(services); plugin.Configure(services);

View File

@ -72,7 +72,7 @@ namespace Kyoo.Core.Tasks
/// <inheritdoc /> /// <inheritdoc />
public TaskParameters GetParameters() public TaskParameters GetParameters()
{ {
return new(); return new TaskParameters();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -56,7 +56,7 @@ namespace Kyoo.Core.Tasks
/// <summary> /// <summary>
/// The transcoder used to extract subtitles and metadata. /// The transcoder used to extract subtitles and metadata.
/// </summary> /// </summary>
private readonly ITranscoder _transcoder; private readonly IFileSystem _transcoder;
/// <summary> /// <summary>
/// Create a new <see cref="RegisterEpisode"/> task. /// Create a new <see cref="RegisterEpisode"/> task.
@ -74,13 +74,13 @@ namespace Kyoo.Core.Tasks
/// The thumbnail manager used to download images. /// The thumbnail manager used to download images.
/// </param> /// </param>
/// <param name="transcoder"> /// <param name="transcoder">
/// The transcoder used to extract subtitles and metadata. /// The file manager used to retrieve episodes metadata.
/// </param> /// </param>
public RegisterEpisode(IIdentifier identifier, public RegisterEpisode(IIdentifier identifier,
ILibraryManager libraryManager, ILibraryManager libraryManager,
AProviderComposite metadataProvider, AProviderComposite metadataProvider,
IThumbnailsManager thumbnailsManager, IThumbnailsManager thumbnailsManager,
ITranscoder transcoder) IFileSystem transcoder)
{ {
_identifier = identifier; _identifier = identifier;
_libraryManager = libraryManager; _libraryManager = libraryManager;

View File

@ -19,18 +19,23 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
{ {
/// <summary> /// <summary>
/// An API to retrieve or edit configuration settings /// An API to retrieve or edit configuration settings
/// </summary> /// </summary>
[Route("api/config")]
[Route("api/configuration")] [Route("api/configuration")]
[Route("api/config", Order = AlternativeRoute)]
[ApiController] [ApiController]
[PartialPermission("Configuration", Group = Group.Admin)]
[ApiDefinition("Configuration", Group = AdminGroup)]
public class ConfigurationApi : Controller public class ConfigurationApi : Controller
{ {
/// <summary> /// <summary>
@ -48,14 +53,19 @@ namespace Kyoo.Core.Api
} }
/// <summary> /// <summary>
/// Get a permission from it's slug. /// Get config value
/// </summary> /// </summary>
/// <remarks>
/// Retrieve a configuration's value from it's slug.
/// </remarks>
/// <param name="slug">The permission to retrieve. You can use ':' or "__" to get a child value.</param> /// <param name="slug">The permission to retrieve. You can use ':' or "__" to get a child value.</param>
/// <returns>The associate value or list of values.</returns> /// <returns>The associate value or list of values.</returns>
/// <response code="200">Return the configuration value or the list of configurations</response> /// <response code="200">Return the configuration value or the list of configurations</response>
/// <response code="404">No configuration exists for the given slug</response> /// <response code="404">No configuration exists for the given slug</response>
[HttpGet("{slug}")] [HttpGet("{slug}")]
[Permission(nameof(ConfigurationApi), Kind.Read, Group.Admin)] [PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<object> GetConfiguration(string slug) public ActionResult<object> GetConfiguration(string slug)
{ {
try try
@ -69,15 +79,20 @@ namespace Kyoo.Core.Api
} }
/// <summary> /// <summary>
/// Edit a permission from it's slug. /// Edit config
/// </summary> /// </summary>
/// <remarks>
/// Edit a configuration's value from it's slug.
/// </remarks>
/// <param name="slug">The permission to edit. You can use ':' or "__" to get a child value.</param> /// <param name="slug">The permission to edit. You can use ':' or "__" to get a child value.</param>
/// <param name="newValue">The new value of the configuration</param> /// <param name="newValue">The new value of the configuration</param>
/// <returns>The edited value.</returns> /// <returns>The edited value.</returns>
/// <response code="200">Return the edited value</response> /// <response code="200">Return the edited value</response>
/// <response code="404">No configuration exists for the given slug</response> /// <response code="404">No configuration exists for the given slug</response>
[HttpPut("{slug}")] [HttpPut("{slug}")]
[Permission(nameof(ConfigurationApi), Kind.Write, Group.Admin)] [PartialPermission(Kind.Write)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<object>> EditConfiguration(string slug, [FromBody] object newValue) public async Task<ActionResult<object>> EditConfiguration(string slug, [FromBody] object newValue)
{ {
try try

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// An endpoint to list and run tasks in the background.
/// </summary>
[Route("api/tasks")]
[Route("api/task", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission("Task", Group = Group.Admin)]
[ApiDefinition("Tasks", Group = AdminGroup)]
public class TaskApi : ControllerBase
{
/// <summary>
/// The task manager used to retrieve and start tasks.
/// </summary>
private readonly ITaskManager _taskManager;
/// <summary>
/// Create a new <see cref="TaskApi"/>.
/// </summary>
/// <param name="taskManager">The task manager used to start tasks.</param>
public TaskApi(ITaskManager taskManager)
{
_taskManager = taskManager;
}
/// <summary>
/// Get all tasks
/// </summary>
/// <remarks>
/// Retrieve all tasks available in this instance of Kyoo.
/// </remarks>
/// <returns>A list of every tasks that this instance know.</returns>
[HttpGet]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<ICollection<ITask>> GetTasks()
{
return Ok(_taskManager.GetAllTasks());
}
/// <summary>
/// Start task
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="taskSlug">The slug of the task to start.</param>
/// <param name="args">The list of arguments to give to the task.</param>
/// <returns>The task has been started or is queued.</returns>
/// <response code="400">The task misses an argument or an argument is invalid.</response>
/// <response code="404">No task could be found with the given slug.</response>
[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<string, object> args)
{
try
{
_taskManager.StartTask(taskSlug, new Progress<float>(), args);
return Ok();
}
catch (ItemNotFoundException)
{
return NotFound();
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.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<Collection>
{
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _files;
private readonly IThumbnailsManager _thumbs;
public CollectionApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs,
IOptions<BasicOptions> 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<ActionResult<Page<Show>>> GetShows(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.ID == id)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(id) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/show")]
[HttpGet("{slug}/shows")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(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<ActionResult<Page<Library>>> GetLibraries(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.ID == id)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(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<ActionResult<Page<Library>>> GetLibraries(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.Slug == slug)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/poster")]
public async Task<IActionResult> GetPoster(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Poster));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/logo")]
public async Task<IActionResult> GetLogo(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Logo));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/backdrop")]
[HttpGet("{slug}/thumbnail")]
public async Task<IActionResult> GetBackdrop(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Thumbnail));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.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<Episode>
{
private readonly ILibraryManager _libraryManager;
private readonly IThumbnailsManager _thumbnails;
private readonly IFileSystem _files;
public EpisodeApi(ILibraryManager libraryManager,
IOptions<BasicOptions> 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<ActionResult<Show>> GetShow(int episodeID)
{
Show ret = await _libraryManager.GetOrDefault<Show>(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<ActionResult<Show>> GetShow(string showSlug, int seasonNumber, int episodeNumber)
{
Show ret = await _libraryManager.GetOrDefault<Show>(showSlug);
if (ret == null)
return NotFound();
return ret;
}
[HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Show>> GetShow(int showID, int seasonNumber, int episodeNumber)
{
Show ret = await _libraryManager.GetOrDefault<Show>(showID);
if (ret == null)
return NotFound();
return ret;
}
[HttpGet("{episodeID:int}/season")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Season>> GetSeason(int episodeID)
{
Season ret = await _libraryManager.GetOrDefault<Season>(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<ActionResult<Season>> 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<ActionResult<Season>> 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<ActionResult<Page<Track>>> GetEpisode(int episodeID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Track>(where, x => x.Episode.ID == episodeID),
new Sort<Track>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Episode>(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<ActionResult<Page<Track>>> GetEpisode(int showID,
int seasonNumber,
int episodeNumber,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Track>(where, x => x.Episode.ShowID == showID
&& x.Episode.SeasonNumber == seasonNumber
&& x.Episode.EpisodeNumber == episodeNumber),
new Sort<Track>(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<ActionResult<Page<Track>>> GetEpisode(string slug,
int seasonNumber,
int episodeNumber,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Track>(where, x => x.Episode.Show.Slug == slug
&& x.Episode.SeasonNumber == seasonNumber
&& x.Episode.EpisodeNumber == episodeNumber),
new Sort<Track>(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<IActionResult> GetThumb(int id)
{
try
{
Episode episode = await _libraryManager.Get<Episode>(id);
return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/thumbnail")]
[HttpGet("{slug}/backdrop")]
public async Task<IActionResult> GetThumb(string slug)
{
try
{
Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using 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<Genre>
{
private readonly ILibraryManager _libraryManager;
public GenreApi(ILibraryManager libraryManager, IOptions<BasicOptions> options)
: base(libraryManager.GenreRepository, options.Value.PublicUrl)
{
_libraryManager = libraryManager;
}
[HttpGet("{id:int}/show")]
[HttpGet("{id:int}/shows")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.ID == id)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Genre>(id) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/show")]
[HttpGet("{slug}/shows")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Genre>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
}
}

View File

@ -22,24 +22,53 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
{ {
/// <summary>
/// A static class containing methods to parse the <c>where</c> query string.
/// </summary>
public static class ApiHelper public static class ApiHelper
{ {
public static Expression StringCompatibleExpression(Func<Expression, Expression, BinaryExpression> operand, /// <summary>
Expression left, /// Make an expression (like
Expression right) /// <see cref="Expression.LessThan(System.Linq.Expressions.Expression,System.Linq.Expressions.Expression)"/>
/// compatible with strings). If the expressions are not strings, the given <paramref name="operand"/> is
/// constructed but if the expressions are strings, this method make the <paramref name="operand"/> compatible with
/// strings.
/// </summary>
/// <param name="operand">
/// The expression to make compatible. It should be something like
/// <see cref="Expression.LessThan(System.Linq.Expressions.Expression,System.Linq.Expressions.Expression)"/> or
/// <see cref="Expression.Equal(System.Linq.Expressions.Expression,System.Linq.Expressions.Expression)"/>.
/// </param>
/// <param name="left">The first parameter to compare.</param>
/// <param name="right">The second parameter to compare.</param>
/// <returns>A comparison expression compatible with strings</returns>
public static BinaryExpression StringCompatibleExpression(
[NotNull] Func<Expression, Expression, BinaryExpression> operand,
[NotNull] Expression left,
[NotNull] Expression right)
{ {
if (left is MemberExpression member && ((PropertyInfo)member.Member).PropertyType == typeof(string)) 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); MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right);
return operand(call, Expression.Constant(0)); return operand(call, Expression.Constant(0));
}
return operand(left, right);
} }
/// <summary>
/// Parse a <c>where</c> query for the given <typeparamref name="T"/>. Items can be filtered by any property
/// of the given type.
/// </summary>
/// <param name="where">The list of filters.</param>
/// <param name="defaultWhere">
/// A custom expression to initially filter a collection. It will be combined with the parsed expression.
/// </param>
/// <typeparam name="T">The type to create filters for.</typeparam>
/// <exception cref="ArgumentException">A filter is invalid.</exception>
/// <returns>An expression representing the filters that can be used anywhere or compiled</returns>
public static Expression<Func<T, bool>> ParseWhere<T>(Dictionary<string, string> where, public static Expression<Func<T, bool>> ParseWhere<T>(Dictionary<string, string> where,
Expression<Func<T, bool>> defaultWhere = null) Expression<Func<T, bool>> defaultWhere = null)
{ {
@ -96,18 +125,17 @@ namespace Kyoo.Core.Api
"not" when valueExpr == null => _ResourceEqual(propertyExpr, value, true), "not" when valueExpr == null => _ResourceEqual(propertyExpr, value, true),
"eq" => Expression.Equal(propertyExpr, valueExpr), "eq" => Expression.Equal(propertyExpr, valueExpr),
"not" => Expression.NotEqual(propertyExpr, valueExpr!), "not" => Expression.NotEqual(propertyExpr, valueExpr),
"lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr), "lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr!),
"lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr), "lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr!),
"gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr), "gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr!),
"gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr), "gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr!),
_ => throw new ArgumentException($"Invalid operand: {operand}") _ => throw new ArgumentException($"Invalid operand: {operand}")
}; };
if (expression != null) expression = expression != null
expression = Expression.AndAlso(expression, condition); ? Expression.AndAlso(expression, condition)
else : condition;
expression = condition;
} }
return Expression.Lambda<Func<T, bool>>(expression!, param); return Expression.Lambda<Func<T, bool>>(expression!, param);

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// A common API containing custom methods to help inheritors.
/// </summary>
public abstract class BaseApi : ControllerBase
{
/// <summary>
/// Construct and return a page from an api.
/// </summary>
/// <param name="resources">The list of resources that should be included in the current page.</param>
/// <param name="limit">
/// 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.
/// </param>
/// <typeparam name="TResult">The type of items on the page.</typeparam>
/// <returns>A Page representing the response.</returns>
protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit)
where TResult : IResource
{
Uri publicUrl = HttpContext.RequestServices
.GetRequiredService<IConfiguration>()
.GetPublicUrl();
return new Page<TResult>(
resources,
new Uri(publicUrl, Request.Path),
Request.Query.ToDictionary(
x => x.Key,
x => x.Value.ToString(),
StringComparer.InvariantCultureIgnoreCase
),
limit
);
}
}
}

View File

@ -18,126 +18,190 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
{ {
/// <summary>
/// A base class to handle CRUD operations on a specific resource type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of resource to make CRUD apis for.</typeparam>
[ApiController] [ApiController]
[ResourceView] [ResourceView]
public class CrudApi<T> : ControllerBase public class CrudApi<T> : BaseApi
where T : class, IResource where T : class, IResource
{ {
private readonly IRepository<T> _repository; /// <summary>
/// The repository of the resource, used to retrieve, save and do operations on the baking store.
/// </summary>
protected IRepository<T> Repository { get; }
protected Uri BaseURL { get; } /// <summary>
/// Create a new <see cref="CrudApi{T}"/> using the given repository and base url.
public CrudApi(IRepository<T> repository, Uri baseURL) /// </summary>
/// <param name="repository">
/// The repository to use as a baking store for the type <typeparamref name="T"/>.
/// </param>
public CrudApi(IRepository<T> repository)
{ {
_repository = repository; Repository = repository;
BaseURL = baseURL;
} }
[HttpGet("{id:int}")] /// <summary>
/// Get item
/// </summary>
/// <remarks>
/// Get a specific resource via it's ID or it's slug.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to retrieve.</param>
/// <returns>The retrieved resource.</returns>
/// <response code="404">A resource with the given ID or slug does not exist.</response>
[HttpGet("{identifier:id}")]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<T>> Get(int id) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Get(Identifier identifier)
{ {
T ret = await _repository.GetOrDefault(id); T ret = await identifier.Match(
if (ret == null) id => Repository.GetOrDefault(id),
return NotFound(); slug => Repository.GetOrDefault(slug)
return ret; );
}
[HttpGet("{slug}")]
[PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<T>> Get(string slug)
{
T ret = await _repository.GetOrDefault(slug);
if (ret == null) if (ret == null)
return NotFound(); return NotFound();
return ret; return ret;
} }
/// <summary>
/// Get count
/// </summary>
/// <remarks>
/// Get the number of resources that match the filters.
/// </remarks>
/// <param name="where">A list of filters to respect.</param>
/// <returns>How many resources matched that filter.</returns>
/// <response code="400">Invalid filters.</response>
[HttpGet("count")] [HttpGet("count")]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<int>> GetCount([FromQuery] Dictionary<string, string> where) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<ActionResult<int>> GetCount([FromQuery] Dictionary<string, string> where)
{ {
try try
{ {
return await _repository.GetCount(ApiHelper.ParseWhere<T>(where)); return await Repository.GetCount(ApiHelper.ParseWhere<T>(where));
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
return BadRequest(new { Error = ex.Message }); return BadRequest(new RequestError(ex.Message));
} }
} }
/// <summary>
/// Get all
/// </summary>
/// <remarks>
/// Get all resources that match the given filter.
/// </remarks>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="where">Filter the returned items.</param>
/// <param name="limit">How many items per page should be returned.</param>
/// <param name="afterID">Where the pagination should start.</param>
/// <returns>A list of resources that match every filters.</returns>
/// <response code="400">Invalid filters or sort information.</response>
[HttpGet] [HttpGet]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<Page<T>>> GetAll([FromQuery] string sortBy, [ProducesResponseType(StatusCodes.Status200OK)]
[FromQuery] int afterID, [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<ActionResult<Page<T>>> GetAll(
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{ {
try try
{ {
ICollection<T> resources = await _repository.GetAll(ApiHelper.ParseWhere<T>(where), ICollection<T> resources = await Repository.GetAll(
ApiHelper.ParseWhere<T>(where),
new Sort<T>(sortBy), new Sort<T>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID)
);
return Page(resources, limit); return Page(resources, limit);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
return BadRequest(new { Error = ex.Message }); return BadRequest(new RequestError(ex.Message));
} }
} }
protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit) /// <summary>
where TResult : IResource /// Create new
{ /// </summary>
return new Page<TResult>(resources, /// <remarks>
new Uri(BaseURL, Request.Path), /// Create a new item and store it. You may leave the ID unspecified, it will be filed by Kyoo.
Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), /// </remarks>
limit); /// <param name="resource">The resource to create.</param>
} /// <returns>The created resource.</returns>
/// <response code="400">The resource in the request body is invalid.</response>
/// <response code="409">This item already exists (maybe a duplicated slug).</response>
[HttpPost] [HttpPost]
[PartialPermission(Kind.Create)] [PartialPermission(Kind.Create)]
public virtual async Task<ActionResult<T>> Create([FromBody] T resource) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))]
public async Task<ActionResult<T>> Create([FromBody] T resource)
{ {
try try
{ {
return await _repository.Create(resource); return await Repository.Create(resource);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
return BadRequest(new { Error = ex.Message }); return BadRequest(new RequestError(ex.Message));
} }
catch (DuplicatedItemException) catch (DuplicatedItemException)
{ {
T existing = await _repository.GetOrDefault(resource.Slug); T existing = await Repository.GetOrDefault(resource.Slug);
return Conflict(existing); return Conflict(existing);
} }
} }
/// <summary>
/// Edit
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="resource">The resource to edit.</param>
/// <param name="resetOld">
/// Should old properties of the resource be discarded or should null values considered as not changed?
/// </param>
/// <returns>The created resource.</returns>
/// <response code="400">The resource in the request body is invalid.</response>
/// <response code="404">No item found with the specified ID (or slug).</response>
[HttpPut] [HttpPut]
[PartialPermission(Kind.Write)] [PartialPermission(Kind.Write)]
public virtual async Task<ActionResult<T>> Edit([FromQuery] bool resetOld, [FromBody] T resource) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Edit([FromBody] T resource, [FromQuery] bool resetOld = true)
{ {
try try
{ {
if (resource.ID > 0) 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; resource.ID = old.ID;
return await _repository.Edit(resource, resetOld); return await Repository.Edit(resource, resetOld);
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {
@ -145,44 +209,27 @@ namespace Kyoo.Core.Api
} }
} }
[HttpPut("{id:int}")] /// <summary>
[PartialPermission(Kind.Write)] /// Delete an item
public virtual async Task<ActionResult<T>> Edit(int id, [FromQuery] bool resetOld, [FromBody] T resource) /// </summary>
{ /// <remarks>
resource.ID = id; /// Delete one item via it's ID or it's slug.
try /// </remarks>
{ /// <param name="identifier">The ID or slug of the resource to delete.</param>
return await _repository.Edit(resource, resetOld); /// <returns>The item has successfully been deleted.</returns>
} /// <response code="404">No item could be found with the given id or slug.</response>
catch (ItemNotFoundException) [HttpDelete("{identifier:id}")]
{
return NotFound();
}
}
[HttpPut("{slug}")]
[PartialPermission(Kind.Write)]
public virtual async Task<ActionResult<T>> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource)
{
try
{
T old = await _repository.Get(slug);
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld);
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpDelete("{id:int}")]
[PartialPermission(Kind.Delete)] [PartialPermission(Kind.Delete)]
public virtual async Task<IActionResult> Delete(int id) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(Identifier identifier)
{ {
try try
{ {
await _repository.Delete(id); await identifier.Match(
id => Repository.Delete(id),
slug => Repository.Delete(slug)
);
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {
@ -192,32 +239,28 @@ namespace Kyoo.Core.Api
return Ok(); return Ok();
} }
[HttpDelete("{slug}")] /// <summary>
/// Delete all where
/// </summary>
/// <remarks>
/// Delete all items matching the given filters. If no filter is specified, delete all items.
/// </remarks>
/// <param name="where">The list of filters.</param>
/// <returns>The item(s) has successfully been deleted.</returns>
/// <response code="400">One or multiple filters are invalid.</response>
[HttpDelete]
[PartialPermission(Kind.Delete)] [PartialPermission(Kind.Delete)]
public virtual async Task<IActionResult> Delete(string slug) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<IActionResult> Delete([FromQuery] Dictionary<string, string> where)
{ {
try try
{ {
await _repository.Delete(slug); await Repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
} }
catch (ItemNotFoundException) catch (ArgumentException ex)
{ {
return NotFound(); return BadRequest(new RequestError(ex.Message));
}
return Ok();
}
[PartialPermission(Kind.Delete)]
public virtual async Task<IActionResult> Delete(Dictionary<string, string> where)
{
try
{
await _repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
}
catch (ItemNotFoundException)
{
return NotFound();
} }
return Ok(); return Ok();

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// A base class to handle CRUD operations and services thumbnails for
/// a specific resource type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of resource to make CRUD and thumbnails apis for.</typeparam>
[ApiController]
[ResourceView]
public class CrudThumbsApi<T> : CrudApi<T>
where T : class, IResource, IThumbnails
{
/// <summary>
/// The file manager used to send images.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// The thumbnail manager used to retrieve images paths.
/// </summary>
private readonly IThumbnailsManager _thumbs;
/// <summary>
/// Create a new <see cref="CrudThumbsApi{T}"/> that handles crud requests and thumbnails.
/// </summary>
/// <param name="repository">
/// The repository to use as a baking store for the type <typeparamref name="T"/>.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public CrudThumbsApi(IRepository<T> repository,
IFileSystem files,
IThumbnailsManager thumbs)
: base(repository)
{
_files = files;
_thumbs = thumbs;
}
/// <summary>
/// Get image
/// </summary>
/// <remarks>
/// Get an image for the specified item.
/// List of commonly available images:<br/>
/// - Poster: Image 0, also available at /poster<br/>
/// - Thumbnail: Image 1, also available at /thumbnail<br/>
/// - Logo: Image 3, also available at /logo<br/>
/// <br/>
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="image">The number of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">No item exist with the specific identifier or the image does not exists on kyoo.</response>
[HttpGet("{identifier:id}/image-{image:int}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> 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);
}
/// <summary>
/// Get Poster
/// </summary>
/// <remarks>
/// Get the poster for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/poster", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetPoster(Identifier identifier)
{
return GetImage(identifier, Images.Poster);
}
/// <summary>
/// Get Logo
/// </summary>
/// <remarks>
/// Get the logo for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/logo", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetLogo(Identifier identifier)
{
return GetImage(identifier, Images.Logo);
}
/// <summary>
/// Get Thumbnail
/// </summary>
/// <remarks>
/// Get the thumbnail for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
[HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)]
public Task<IActionResult> GetBackdrop(Identifier identifier)
{
return GetImage(identifier, Images.Thumbnail);
}
}
}

View File

@ -24,6 +24,7 @@ using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils; using Kyoo.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
@ -32,8 +33,13 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
{ {
/// <summary>
/// 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 <c>where</c> query parameter.
/// </summary>
public class ResourceViewAttribute : ActionFilterAttribute public class ResourceViewAttribute : ActionFilterAttribute
{ {
/// <inheritdoc />
public override void OnActionExecuting(ActionExecutingContext context) public override void OnActionExecuting(ActionExecutingContext context)
{ {
if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary<string, string> where) if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary<string, string> where)
@ -59,13 +65,13 @@ namespace Kyoo.Core.Api
type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type; type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type;
type = Utility.GetGenericDefinition(type, typeof(Page<>))?.GetGenericArguments()[0] ?? type; type = Utility.GetGenericDefinition(type, typeof(Page<>))?.GetGenericArguments()[0] ?? type;
context.HttpContext.Items["ResourceType"] = type.Name;
PropertyInfo[] properties = type.GetProperties() PropertyInfo[] properties = type.GetProperties()
.Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null) .Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null)
.ToArray(); .ToArray();
if (fields.Count == 1 && fields.Contains("all")) if (fields.Count == 1 && fields.Contains("all"))
{
fields = properties.Select(x => x.Name).ToList(); fields = properties.Select(x => x.Name).ToList();
}
else else
{ {
fields = fields fields = fields
@ -77,10 +83,9 @@ namespace Kyoo.Core.Api
?.Name; ?.Name;
if (property != null) if (property != null)
return property; return property;
context.Result = new BadRequestObjectResult(new context.Result = new BadRequestObjectResult(
{ new RequestError($"{x} does not exist on {type.Name}.")
Error = $"{x} does not exist on {type.Name}." );
});
return null; return null;
}) })
.ToList(); .ToList();
@ -92,6 +97,7 @@ namespace Kyoo.Core.Api
base.OnActionExecuting(context); base.OnActionExecuting(context);
} }
/// <inheritdoc />
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{ {
if (context.Result is ObjectResult result) if (context.Result is ObjectResult result)
@ -104,7 +110,7 @@ namespace Kyoo.Core.Api
if (result.DeclaredType == null) if (result.DeclaredType == null)
return; return;
ILibraryManager library = context.HttpContext.RequestServices.GetService<ILibraryManager>(); ILibraryManager library = context.HttpContext.RequestServices.GetRequiredService<ILibraryManager>();
ICollection<string> fields = (ICollection<string>)context.HttpContext.Items["fields"]; ICollection<string> fields = (ICollection<string>)context.HttpContext.Items["fields"];
Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>)); 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 (IResource resource in ((dynamic)result.Value).Items)
{ {
foreach (string field in fields!) foreach (string field in fields!)
await library!.Load(resource, field); await library.Load(resource, field);
} }
} }
else if (result.DeclaredType.IsAssignableTo(typeof(IResource))) else if (result.DeclaredType.IsAssignableTo(typeof(IResource)))
{ {
foreach (string field in fields!) foreach (string field in fields!)
await library!.Load((IResource)result.Value, field); await library.Load((IResource)result.Value, field);
} }
} }
} }

View File

@ -0,0 +1,64 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Kyoo.Core.Api
{
/// <summary>
/// The custom options of newtonsoft json. This simply add the <see cref="PeopleRoleConverter"/> and set
/// the <see cref="JsonSerializerContract"/>. It is on a separate class to use dependency injection.
/// </summary>
public class JsonOptions : IConfigureOptions<MvcNewtonsoftJsonOptions>
{
/// <summary>
/// The http context accessor given to the <see cref="JsonSerializerContract"/>.
/// </summary>
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// The options containing the public URL of kyoo, given to <see cref="JsonSerializerContract"/>.
/// </summary>
private readonly IOptions<BasicOptions> _options;
/// <summary>
/// Create a new <see cref="JsonOptions"/>.
/// </summary>
/// <param name="httpContextAccessor">
/// The http context accessor given to the <see cref="JsonSerializerContract"/>.
/// </param>
/// <param name="options">
/// The options containing the public URL of kyoo, given to <see cref="JsonSerializerContract"/>.
/// </param>
public JsonOptions(IHttpContextAccessor httpContextAccessor, IOptions<BasicOptions> options)
{
_httpContextAccessor = httpContextAccessor;
_options = options;
}
/// <inheritdoc />
public void Configure(MvcNewtonsoftJsonOptions options)
{
options.SerializerSettings.ContractResolver = new JsonSerializerContract(_httpContextAccessor, _options);
options.SerializerSettings.Converters.Add(new PeopleRoleConverter());
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<LoadableRelationAttribute>();
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<SerializeIgnoreAttribute>() != null)
property.ShouldSerialize = _ => false;
if (member.GetCustomAttribute<DeserializeIgnoreAttribute>() != 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<SerializeAsAttribute>();
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;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// A custom json serializer that respects <see cref="SerializeIgnoreAttribute"/> and
/// <see cref="DeserializeIgnoreAttribute"/>. It also handle <see cref="LoadableRelationAttribute"/> via the
/// <c>fields</c> query parameter and <see cref="IThumbnails"/> items.
/// </summary>
public class JsonSerializerContract : CamelCasePropertyNamesContractResolver
{
/// <summary>
/// The http context accessor used to retrieve the <c>fields</c> query parameter as well as the type of
/// resource currently serializing.
/// </summary>
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// The options containing the public URL of kyoo.
/// </summary>
private readonly IOptions<BasicOptions> _options;
/// <summary>
/// Create a new <see cref="JsonSerializerContract"/>.
/// </summary>
/// <param name="httpContextAccessor">The http context accessor to use.</param>
/// <param name="options">The options containing the public URL of kyoo.</param>
public JsonSerializerContract(IHttpContextAccessor httpContextAccessor, IOptions<BasicOptions> options)
{
_httpContextAccessor = httpContextAccessor;
_options = options;
}
/// <inheritdoc />
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
LoadableRelationAttribute relation = member.GetCustomAttribute<LoadableRelationAttribute>();
if (relation != null)
{
property.ShouldSerialize = _ =>
{
string resType = (string)_httpContextAccessor.HttpContext!.Items["ResourceType"];
if (member.DeclaringType!.Name != resType)
return false;
ICollection<string> fields = (ICollection<string>)_httpContextAccessor.HttpContext!.Items["fields"];
return fields!.Contains(member.Name);
};
}
if (member.GetCustomAttribute<SerializeIgnoreAttribute>() != null)
property.ShouldSerialize = _ => false;
if (member.GetCustomAttribute<DeserializeIgnoreAttribute>() != null)
property.ShouldDeserialize = _ => false;
return property;
}
/// <inheritdoc />
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> 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;
}
/// <summary>
/// A custom <see cref="IValueProvider"/> that uses the
/// <see cref="IThumbnails"/>.<see cref="IThumbnails.Images"/> as a value.
/// </summary>
private class ThumbnailProvider : IValueProvider
{
/// <summary>
/// The public address of kyoo.
/// </summary>
private readonly Uri _host;
/// <summary>
/// The index/ID of the image to retrieve/set.
/// </summary>
private readonly int _imageIndex;
/// <summary>
/// Create a new <see cref="ThumbnailProvider"/>.
/// </summary>
/// <param name="host">The public address of kyoo.</param>
/// <param name="imageIndex">The index/ID of the image to retrieve/set.</param>
public ThumbnailProvider(Uri host, int imageIndex)
{
_host = host;
_imageIndex = imageIndex;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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();
}
}
}
}

View File

@ -24,8 +24,13 @@ using Newtonsoft.Json.Linq;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
{ {
/// <summary>
/// A custom role's convertor to inline the person or the show depending on the value of
/// <see cref="PeopleRole.ForPeople"/>.
/// </summary>
public class PeopleRoleConverter : JsonConverter<PeopleRole> public class PeopleRoleConverter : JsonConverter<PeopleRole>
{ {
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, PeopleRole value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, PeopleRole value, JsonSerializer serializer)
{ {
ICollection<PeopleRole> oldPeople = value.Show?.People; ICollection<PeopleRole> oldPeople = value.Show?.People;
@ -46,6 +51,7 @@ namespace Kyoo.Core.Api
value.People.Roles = oldRoles; value.People.Roles = oldRoles;
} }
/// <inheritdoc />
public override PeopleRole ReadJson(JsonReader reader, public override PeopleRole ReadJson(JsonReader reader,
Type objectType, Type objectType,
PeopleRole existingValue, PeopleRole existingValue,

View File

@ -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 <https://www.gnu.org/licenses/>.
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, @"(?<!{){(\w+)(:(\w+))?}", x =>
{
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.
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using 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<Library>
{
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
public LibraryApi(ILibraryManager libraryManager, ITaskManager taskManager, IOptions<BasicOptions> options)
: base(libraryManager.LibraryRepository, options.Value.PublicUrl)
{
_libraryManager = libraryManager;
_taskManager = taskManager;
}
[PartialPermission(Kind.Create)]
public override async Task<ActionResult<Library>> Create(Library resource)
{
ActionResult<Library> result = await base.Create(resource);
if (result.Value != null)
{
_taskManager.StartTask("scan",
new Progress<float>(),
new Dictionary<string, object> { { "slug", result.Value.Slug } });
}
return result;
}
[HttpGet("{id:int}/show")]
[HttpGet("{id:int}/shows")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.ID == id)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(id) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/show")]
[HttpGet("{slug}/shows")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(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<ActionResult<Page<Collection>>> GetCollections(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.ID == id)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(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<ActionResult<Page<Collection>>> GetCollections(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.Slug == slug)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(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<ActionResult<Page<LibraryItem>>> GetItems(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(id,
ApiHelper.ParseWhere<LibraryItem>(where),
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(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<ActionResult<Page<LibraryItem>>> GetItems(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(slug,
ApiHelper.ParseWhere<LibraryItem>(where),
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.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<BasicOptions> options)
{
_libraryItems = libraryItems;
_baseURL = options.Value.PublicUrl;
}
[HttpGet]
[Permission(nameof(LibraryItemApi), Kind.Read)]
public async Task<ActionResult<Page<LibraryItem>>> GetAll([FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<LibraryItem> resources = await _libraryItems.GetAll(
ApiHelper.ParseWhere<LibraryItem>(where),
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID));
return new Page<LibraryItem>(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 });
}
}
}
}

View File

@ -0,0 +1,105 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Genre"/>.
/// </summary>
[Route("api/genres")]
[Route("api/genre", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(Genre))]
[ApiDefinition("Genres", Group = MetadataGroup)]
public class GenreApi : CrudApi<Genre>
{
/// <summary>
/// The library manager used to modify or retrieve information about the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="GenreApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
public GenreApi(ILibraryManager libraryManager)
: base(libraryManager.GenreRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get shows with genre
/// </summary>
/// <remarks>
/// Lists the shows that have the selected genre.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Genre"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No genre with the given ID could be found.</response>
[HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Genre>(x => x.Genres)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Genre>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// Information about one or multiple <see cref="Provider"/>.
/// Providers are links to external websites or database.
/// They are mostly linked to plugins that provide metadata from those websites.
/// </summary>
[Route("api/providers")]
[Route("api/provider", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(Provider))]
[ApiDefinition("Providers", Group = MetadataGroup)]
public class ProviderApi : CrudThumbsApi<Provider>
{
/// <summary>
/// Create a new <see cref="ProviderApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images and fonts.</param>
/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
public ProviderApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbnails)
: base(libraryManager.ProviderRepository, files, thumbnails)
{ }
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// Information about one or multiple staff member.
/// </summary>
[Route("api/staff")]
[Route("api/people", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(People))]
[ApiDefinition("Staff", Group = MetadataGroup)]
public class StaffApi : CrudThumbsApi<People>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="StaffApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images and fonts.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public StaffApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs)
: base(libraryManager.PeopleRepository, files, thumbs)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get roles
/// </summary>
/// <remarks>
/// List the roles in witch this person has played, written or worked in a way.
/// </remarks>
/// <param name="identifier">The ID or slug of the person.</param>
/// <param name="sortBy">A key to sort roles by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of roles to return.</param>
/// <param name="afterID">An optional role's ID to start the query from this specific item.</param>
/// <returns>A page of roles.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No person with the given ID or slug could be found.</response>
[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<ActionResult<Page<PeopleRole>>> GetRoles(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{
try
{
Expression<Func<PeopleRole, bool>> whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
Sort<PeopleRole> sort = new(sortBy);
Pagination pagination = new(limit, afterID);
ICollection<PeopleRole> 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));
}
}
}
}

View File

@ -0,0 +1,105 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Studio"/>.
/// </summary>
[Route("api/studios")]
[Route("api/studio", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(Show))]
[ApiDefinition("Studios", Group = MetadataGroup)]
public class StudioApi : CrudApi<Studio>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="StudioApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
public StudioApi(ILibraryManager libraryManager)
: base(libraryManager.StudioRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get shows
/// </summary>
/// <remarks>
/// List shows that were made by this specific studio.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Studio"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No studio with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioID, x => x.Studio.Slug)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Studio>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<People>
{
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _files;
private readonly IThumbnailsManager _thumbs;
public PeopleApi(ILibraryManager libraryManager,
IOptions<BasicOptions> 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<ActionResult<Page<PeopleRole>>> GetRoles(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<PeopleRole> resources = await _libraryManager.GetRolesFromPeople(id,
ApiHelper.ParseWhere<PeopleRole>(where),
new Sort<PeopleRole>(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<ActionResult<Page<PeopleRole>>> GetRoles(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<PeopleRole> resources = await _libraryManager.GetRolesFromPeople(slug,
ApiHelper.ParseWhere<PeopleRole>(where),
new Sort<PeopleRole>(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<IActionResult> GetPeopleIcon(int id)
{
People people = await _libraryManager.GetOrDefault<People>(id);
if (people == null)
return NotFound();
return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster));
}
[HttpGet("{slug}/poster")]
public async Task<IActionResult> GetPeopleIcon(string slug)
{
People people = await _libraryManager.GetOrDefault<People>(slug);
if (people == null)
return NotFound();
return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Provider>
{
private readonly IThumbnailsManager _thumbnails;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _files;
public ProviderApi(ILibraryManager libraryManager,
IOptions<BasicOptions> options,
IFileSystem files,
IThumbnailsManager thumbnails)
: base(libraryManager.ProviderRepository, options.Value.PublicUrl)
{
_libraryManager = libraryManager;
_files = files;
_thumbnails = thumbnails;
}
[HttpGet("{id:int}/logo")]
public async Task<IActionResult> GetLogo(int id)
{
Provider provider = await _libraryManager.GetOrDefault<Provider>(id);
if (provider == null)
return NotFound();
return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo));
}
[HttpGet("{slug}/logo")]
public async Task<IActionResult> GetLogo(string slug)
{
Provider provider = await _libraryManager.GetOrDefault<Provider>(slug);
if (provider == null)
return NotFound();
return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Collection"/>.
/// </summary>
[Route("api/collections")]
[Route("api/collection", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(Collection))]
[ApiDefinition("Collections", Group = ResourcesGroup)]
public class CollectionApi : CrudThumbsApi<Collection>
{
/// <summary>
/// The library manager used to modify or retrieve information about the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="CollectionApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public CollectionApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs)
: base(libraryManager.CollectionRepository, files, thumbs)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get shows in collection
/// </summary>
/// <remarks>
/// Lists the shows that are contained in the collection with the given id or slug.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given ID could be found.</response>
[HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Collection>(x => x.Collections)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get libraries containing this collection
/// </summary>
/// <remarks>
/// Lists the libraries that contain the collection with the given id or slug.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort libraries by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of libraries to return.</param>
/// <param name="afterID">An optional library's ID to start the query from this specific item.</param>
/// <returns>A page of libraries.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given ID or slug could be found.</response>
[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<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Library, Collection>(x => x.Collections)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Episode"/>.
/// </summary>
[Route("api/episodes")]
[Route("api/episode", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(Episode))]
[ApiDefinition("Episodes", Group = ResourcesGroup)]
public class EpisodeApi : CrudThumbsApi<Episode>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="EpisodeApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
public EpisodeApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbnails)
: base(libraryManager.EpisodeRepository, files, thumbnails)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get episode's show
/// </summary>
/// <remarks>
/// Get the show that this episode is part of.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <returns>The show that contains this episode.</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/show")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Show>> GetShow(Identifier identifier)
{
Show ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn<Show, Episode>(x => x.Episodes));
if (ret == null)
return NotFound();
return ret;
}
/// <summary>
/// Get episode's season
/// </summary>
/// <remarks>
/// Get the season that this episode is part of.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <returns>The season that contains this episode.</returns>
/// <response code="204">The episode is not part of a season.</response>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/season")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Season>> GetSeason(Identifier identifier)
{
Season ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn<Season, Episode>(x => x.Episodes));
if (ret != null)
return ret;
Episode episode = await identifier.Match(
id => _libraryManager.GetOrDefault<Episode>(id),
slug => _libraryManager.GetOrDefault<Episode>(slug)
);
return episode == null
? NotFound()
: NoContent();
}
/// <summary>
/// Get tracks
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <param name="sortBy">A key to sort tracks by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of tracks to return.</param>
/// <param name="afterID">An optional track's ID to start the query from this specific item.</param>
/// <returns>A page of tracks.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No episode with the given ID or slug could be found.</response>
/// 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<ActionResult<Page<Track>>> GetEpisode(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Track>(x => x.EpisodeID, x => x.Episode.Slug)),
new Sort<Track>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Episode>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// Information about one or multiple <see cref="Library"/>.
/// </summary>
[Route("api/libraries")]
[Route("api/library", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(Library), Group = Group.Admin)]
[ApiDefinition("Library", Group = ResourcesGroup)]
public class LibraryApi : CrudApi<Library>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="EpisodeApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
public LibraryApi(ILibraryManager libraryManager)
: base(libraryManager.LibraryRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get shows
/// </summary>
/// <remarks>
/// List the shows that are part of this library.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Library>(x => x.Libraries)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get collections
/// </summary>
/// <remarks>
/// List the collections that are part of this library.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort collections by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of collections to return.</param>
/// <param name="afterID">An optional collection's ID to start the query from this specific item.</param>
/// <returns>A page of collections.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{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<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Library>(x => x.Libraries)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get items
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort items by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of items to return.</param>
/// <param name="afterID">An optional item's ID to start the query from this specific item.</param>
/// <returns>A page of items.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{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<ActionResult<Page<LibraryItem>>> GetItems(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50,
[FromQuery] int? afterID = null)
{
try
{
Expression<Func<LibraryItem, bool>> whereQuery = ApiHelper.ParseWhere<LibraryItem>(where);
Sort<LibraryItem> sort = new(sortBy);
Pagination pagination = new(limit, afterID);
ICollection<LibraryItem> 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));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// Endpoint for items that are not part of a specific library.
/// An item can ether represent a collection or a show.
/// </summary>
[Route("api/items")]
[Route("api/item", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(LibraryItem))]
[ApiDefinition("Items", Group = ResourcesGroup)]
public class LibraryItemApi : BaseApi
{
/// <summary>
/// The library item repository used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryItemRepository _libraryItems;
/// <summary>
/// Create a new <see cref="LibraryItemApi"/>.
/// </summary>
/// <param name="libraryItems">
/// The library item repository used to modify or retrieve information in the data store.
/// </param>
public LibraryItemApi(ILibraryItemRepository libraryItems)
{
_libraryItems = libraryItems;
}
/// <summary>
/// Get items
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="sortBy">A key to sort items by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of items to return.</param>
/// <param name="afterID">An optional item's ID to start the query from this specific item.</param>
/// <returns>A page of items.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<LibraryItem>>> GetAll(
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50,
[FromQuery] int? afterID = null)
{
try
{
ICollection<LibraryItem> resources = await _libraryItems.GetAll(
ApiHelper.ParseWhere<LibraryItem>(where),
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID)
);
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
}
}

View File

@ -0,0 +1,194 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// An endpoint to search for every resources of kyoo. Searching for only a specific type of resource
/// is available on the said endpoint.
/// </summary>
[Route("api/search/{query}")]
[ApiController]
[ResourceView]
[ApiDefinition("Search", Group = ResourcesGroup)]
public class SearchApi : ControllerBase
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="SearchApi"/>.
/// </summary>
/// <param name="libraryManager">The library manager used to interact with the data store.</param>
public SearchApi(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Global search
/// </summary>
/// <remarks>
/// Search for collections, shows, episodes, staff, genre and studios at the same time
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of every resources found for the specified query.</returns>
[HttpGet]
[Permission(nameof(Collection), Kind.Read)]
[Permission(nameof(Show), Kind.Read)]
[Permission(nameof(Episode), Kind.Read)]
[Permission(nameof(People), Kind.Read)]
[Permission(nameof(Genre), Kind.Read)]
[Permission(nameof(Studio), Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<SearchResult>> Search(string query)
{
return new SearchResult
{
Query = query,
Collections = await _libraryManager.Search<Collection>(query),
Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.Search<People>(query),
Genres = await _libraryManager.Search<Genre>(query),
Studios = await _libraryManager.Search<Studio>(query)
};
}
/// <summary>
/// Search collections
/// </summary>
/// <remarks>
/// Search for collections
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of collections found for the specified query.</returns>
[HttpGet("collections")]
[HttpGet("collection", Order = AlternativeRoute)]
[Permission(nameof(Collection), Kind.Read)]
[ApiDefinition("Collections")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Collection>> SearchCollections(string query)
{
return _libraryManager.Search<Collection>(query);
}
/// <summary>
/// Search shows
/// </summary>
/// <remarks>
/// Search for shows
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of shows found for the specified query.</returns>
[HttpGet("shows")]
[HttpGet("show", Order = AlternativeRoute)]
[Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Shows")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Show>> SearchShows(string query)
{
return _libraryManager.Search<Show>(query);
}
/// <summary>
/// Search episodes
/// </summary>
/// <remarks>
/// Search for episodes
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of episodes found for the specified query.</returns>
[HttpGet("episodes")]
[HttpGet("episode", Order = AlternativeRoute)]
[Permission(nameof(Episode), Kind.Read)]
[ApiDefinition("Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Episode>> SearchEpisodes(string query)
{
return _libraryManager.Search<Episode>(query);
}
/// <summary>
/// Search staff
/// </summary>
/// <remarks>
/// Search for staff
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of staff members found for the specified query.</returns>
[HttpGet("staff")]
[HttpGet("person", Order = AlternativeRoute)]
[HttpGet("people", Order = AlternativeRoute)]
[Permission(nameof(People), Kind.Read)]
[ApiDefinition("Staff")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<People>> SearchPeople(string query)
{
return _libraryManager.Search<People>(query);
}
/// <summary>
/// Search genres
/// </summary>
/// <remarks>
/// Search for genres
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of genres found for the specified query.</returns>
[HttpGet("genres")]
[HttpGet("genre", Order = AlternativeRoute)]
[Permission(nameof(Genre), Kind.Read)]
[ApiDefinition("Genres")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Genre>> SearchGenres(string query)
{
return _libraryManager.Search<Genre>(query);
}
/// <summary>
/// Search studios
/// </summary>
/// <remarks>
/// Search for studios
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of studios found for the specified query.</returns>
[HttpGet("studios")]
[HttpGet("studio", Order = AlternativeRoute)]
[Permission(nameof(Studio), Kind.Read)]
[ApiDefinition("Studios")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Studio>> SearchStudios(string query)
{
return _libraryManager.Search<Studio>(query);
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Season"/>.
/// </summary>
[Route("api/seasons")]
[Route("api/season", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(Season))]
[ApiDefinition("Seasons", Group = ResourcesGroup)]
public class SeasonApi : CrudThumbsApi<Season>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="SeasonApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public SeasonApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs)
: base(libraryManager.SeasonRepository, files, thumbs)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get episodes in the season
/// </summary>
/// <remarks>
/// List the episodes that are part of the specified season.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Season"/>.</param>
/// <param name="sortBy">A key to sort episodes by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of episodes to return.</param>
/// <param name="afterID">An optional episode's ID to start the query from this specific item.</param>
/// <returns>A page of episodes.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No season with the given ID or slug could be found.</response>
[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<ActionResult<Page<Episode>>> GetEpisode(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonID, x => x.Season.Slug)),
new Sort<Episode>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Season>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get season's show
/// </summary>
/// <remarks>
/// Get the show that this season is part of.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Season"/>.</param>
/// <returns>The show that contains this season.</returns>
/// <response code="404">No season with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/show")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Show>> GetShow(Identifier identifier)
{
Show ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn<Show, Season>(x => x.Seasons));
if (ret == null)
return NotFound();
return ret;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// Information about one or multiple <see cref="Show"/>.
/// </summary>
[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<Show>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used to send images and fonts.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// The base URL of Kyoo. This will be used to create links for images and
/// <see cref="Abstractions.Models.Page{T}"/>.
/// </summary>
private readonly Uri _baseURL;
/// <summary>
/// Create a new <see cref="ShowApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images and fonts.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
/// <param name="options">
/// Options used to retrieve the base URL of Kyoo.
/// </param>
public ShowApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs,
IOptions<BasicOptions> options)
: base(libraryManager.ShowRepository, files, thumbs)
{
_libraryManager = libraryManager;
_files = files;
_baseURL = options.Value.PublicUrl;
}
/// <summary>
/// Get seasons of this show
/// </summary>
/// <remarks>
/// List the seasons that are part of the specified show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort seasons by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of seasons to return.</param>
/// <param name="afterID">An optional season's ID to start the query from this specific item.</param>
/// <returns>A page of seasons.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[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<ActionResult<Page<Season>>> GetSeasons(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)),
new Sort<Season>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get episodes of this show
/// </summary>
/// <remarks>
/// List the episodes that are part of the specified show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort episodes by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of episodes to return.</param>
/// <param name="afterID">An optional episode's ID to start the query from this specific item.</param>
/// <returns>A page of episodes.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[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<ActionResult<Page<Episode>>> GetEpisodes(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowID, x => x.Show.Slug)),
new Sort<Episode>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get staff
/// </summary>
/// <remarks>
/// List staff members that made this show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort staff members by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of people to return.</param>
/// <param name="afterID">An optional person's ID to start the query from this specific item.</param>
/// <returns>A page of people.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[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<ActionResult<Page<PeopleRole>>> GetPeople(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
Expression<Func<PeopleRole, bool>> whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
Sort<PeopleRole> sort = new(sortBy);
Pagination pagination = new(limit, afterID);
ICollection<PeopleRole> 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));
}
}
/// <summary>
/// Get genres of this show
/// </summary>
/// <remarks>
/// List the genres that represent this show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort genres by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of genres to return.</param>
/// <param name="afterID">An optional genre's ID to start the query from this specific item.</param>
/// <returns>A page of genres.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[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<ActionResult<Page<Genre>>> GetGenres(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Genre> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Genre, Show>(x => x.Shows)),
new Sort<Genre>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get studio that made the show
/// </summary>
/// <remarks>
/// Get the studio that made the show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <returns>The studio that made the show.</returns>
/// <response code="404">No show with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/studio")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Studio>> GetStudio(Identifier identifier)
{
Studio studio = await _libraryManager.GetOrDefault(identifier.IsContainedIn<Studio, Show>(x => x.Shows));
if (studio == null)
return NotFound();
return studio;
}
/// <summary>
/// Get libraries containing this show
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort libraries by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of libraries to return.</param>
/// <param name="afterID">An optional library's ID to start the query from this specific item.</param>
/// <returns>A page of libraries.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[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<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Library, Show>(x => x.Shows)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// Get collections containing this show
/// </summary>
/// <remarks>
/// List the collections that contain this show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort collections by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of collections to return.</param>
/// <param name="afterID">An optional collection's ID to start the query from this specific item.</param>
/// <returns>A page of collections.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[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<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Show>(x => x.Shows)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
/// <summary>
/// List fonts
/// </summary>
/// <remarks>
/// List available fonts for this show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <returns>An object containing the name of the font followed by the url to retrieve it.</returns>
[HttpGet("{identifier:id}/fonts")]
[HttpGet("{identifier:id}/font", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Dictionary<string, string>>> GetFonts(Identifier identifier)
{
Show show = await identifier.Match(
id => _libraryManager.GetOrDefault<Show>(id),
slug => _libraryManager.GetOrDefault<Show>(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)}"
);
}
/// <summary>
/// Get font
/// </summary>
/// <remarks>
/// Get a font file that is used in subtitles of this show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="font">The name of the font to retrieve (with it's file extension).</param>
/// <returns>A page of collections.</returns>
/// <response code="400">The font name is invalid.</response>
/// <response code="404">No show with the given ID/slug could be found or the font does not exist.</response>
[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<IActionResult> 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<Show>(id),
slug => _libraryManager.GetOrDefault<Show>(slug)
);
if (show == null)
return NotFound();
string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments", font);
return _files.FileResult(path);
}
}
}

View File

@ -1,107 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api
{
[Route("api/search/{query}")]
[ApiController]
public class SearchApi : ControllerBase
{
private readonly ILibraryManager _libraryManager;
public SearchApi(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
[HttpGet]
[Permission(nameof(Collection), Kind.Read)]
[Permission(nameof(Show), Kind.Read)]
[Permission(nameof(Episode), Kind.Read)]
[Permission(nameof(People), Kind.Read)]
[Permission(nameof(Genre), Kind.Read)]
[Permission(nameof(Studio), Kind.Read)]
public async Task<ActionResult<SearchResult>> Search(string query)
{
return new SearchResult
{
Query = query,
Collections = await _libraryManager.Search<Collection>(query),
Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.Search<People>(query),
Genres = await _libraryManager.Search<Genre>(query),
Studios = await _libraryManager.Search<Studio>(query)
};
}
[HttpGet("collection")]
[HttpGet("collections")]
[Permission(nameof(Collection), Kind.Read)]
public Task<ICollection<Collection>> SearchCollections(string query)
{
return _libraryManager.Search<Collection>(query);
}
[HttpGet("show")]
[HttpGet("shows")]
[Permission(nameof(Show), Kind.Read)]
public Task<ICollection<Show>> SearchShows(string query)
{
return _libraryManager.Search<Show>(query);
}
[HttpGet("episode")]
[HttpGet("episodes")]
[Permission(nameof(Episode), Kind.Read)]
public Task<ICollection<Episode>> SearchEpisodes(string query)
{
return _libraryManager.Search<Episode>(query);
}
[HttpGet("people")]
[Permission(nameof(People), Kind.Read)]
public Task<ICollection<People>> SearchPeople(string query)
{
return _libraryManager.Search<People>(query);
}
[HttpGet("genre")]
[HttpGet("genres")]
[Permission(nameof(Genre), Kind.Read)]
public Task<ICollection<Genre>> SearchGenres(string query)
{
return _libraryManager.Search<Genre>(query);
}
[HttpGet("studio")]
[HttpGet("studios")]
[Permission(nameof(Studio), Kind.Read)]
public Task<ICollection<Studio>> SearchStudios(string query)
{
return _libraryManager.Search<Studio>(query);
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using 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<Season>
{
private readonly ILibraryManager _libraryManager;
private readonly IThumbnailsManager _thumbs;
private readonly IFileSystem _files;
public SeasonApi(ILibraryManager libraryManager,
IOptions<BasicOptions> 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<ActionResult<Page<Episode>>> GetEpisode(int seasonID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.SeasonID == seasonID),
new Sort<Episode>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Season>(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<ActionResult<Page<Episode>>> GetEpisode(string showSlug,
int seasonNumber,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber),
new Sort<Episode>(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<ActionResult<Page<Episode>>> GetEpisode(int showID,
int seasonNumber,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.ShowID == showID && x.SeasonNumber == seasonNumber),
new Sort<Episode>(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<ActionResult<Show>> GetShow(int seasonID)
{
Show ret = await _libraryManager.GetOrDefault<Show>(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<ActionResult<Show>> GetShow(string showSlug, int seasonNumber)
{
Show ret = await _libraryManager.GetOrDefault<Show>(showSlug);
if (ret == null)
return NotFound();
return ret;
}
[HttpGet("{showID:int}-s{seasonNumber:int}/show")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Show>> GetShow(int showID, int seasonNumber)
{
Show ret = await _libraryManager.GetOrDefault<Show>(showID);
if (ret == null)
return NotFound();
return ret;
}
[HttpGet("{id:int}/poster")]
public async Task<IActionResult> GetPoster(int id)
{
Season season = await _libraryManager.GetOrDefault<Season>(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<IActionResult> GetPoster(string slug)
{
Season season = await _libraryManager.GetOrDefault<Season>(slug);
if (season == null)
return NotFound();
await _libraryManager.Load(season, x => x.Show);
return _files.FileResult(await _thumbs.GetImagePath(season, Images.Poster));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Show>
{
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _files;
private readonly IThumbnailsManager _thumbs;
public ShowApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs,
IOptions<BasicOptions> 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<ActionResult<Page<Season>>> GetSeasons(int showID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Season>(where, x => x.ShowID == showID),
new Sort<Season>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Season>>> GetSeasons(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Season>(where, x => x.Show.Slug == slug),
new Sort<Season>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Episode>>> GetEpisodes(int showID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.ShowID == showID),
new Sort<Episode>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Episode>>> GetEpisodes(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.Show.Slug == slug),
new Sort<Episode>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<PeopleRole>>> GetPeople(int showID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<PeopleRole> resources = await _libraryManager.GetPeopleFromShow(showID,
ApiHelper.ParseWhere<PeopleRole>(where),
new Sort<PeopleRole>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<PeopleRole>>> GetPeople(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<PeopleRole> resources = await _libraryManager.GetPeopleFromShow(slug,
ApiHelper.ParseWhere<PeopleRole>(where),
new Sort<PeopleRole>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Genre>>> GetGenres(int showID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Genre> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Genre>(where, x => x.Shows.Any(y => y.ID == showID)),
new Sort<Genre>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Genre>>> GetGenre(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Genre> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Genre>(where, x => x.Shows.Any(y => y.Slug == slug)),
new Sort<Genre>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Studio>> GetStudio(int showID)
{
try
{
return await _libraryManager.Get<Studio>(x => x.Shows.Any(y => y.ID == showID));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/studio")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Studio>> GetStudio(string slug)
{
try
{
return await _libraryManager.Get<Studio>(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<ActionResult<Page<Library>>> GetLibraries(int showID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Shows.Any(y => y.ID == showID)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Library>>> GetLibraries(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Shows.Any(y => y.Slug == slug)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Collection>>> GetCollections(int showID,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Shows.Any(y => y.ID == showID)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Page<Collection>>> GetCollections(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30)
{
try
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Shows.Any(y => y.Slug == slug)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(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<ActionResult<Dictionary<string, string>>> GetFonts(string slug)
{
try
{
Show show = await _libraryManager.Get<Show>(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<IActionResult> GetFont(string showSlug, string slug)
{
try
{
Show show = await _libraryManager.Get<Show>(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<IActionResult> GetPoster(string slug)
{
try
{
Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetImagePath(show, Images.Poster));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/logo")]
public async Task<IActionResult> GetLogo(string slug)
{
try
{
Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetImagePath(show, Images.Logo));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/backdrop")]
[HttpGet("{slug}/thumbnail")]
public async Task<IActionResult> GetBackdrop(string slug)
{
try
{
Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetImagePath(show, Images.Thumbnail));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using 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<Studio>
{
private readonly ILibraryManager _libraryManager;
public StudioApi(ILibraryManager libraryManager, IOptions<BasicOptions> options)
: base(libraryManager.StudioRepository, options.Value.PublicUrl)
{
_libraryManager = libraryManager;
}
[HttpGet("{id:int}/show")]
[HttpGet("{id:int}/shows")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.StudioID == id),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Studio>(id) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/show")]
[HttpGet("{slug}/shows")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Studio.Slug == slug),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Studio>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<IActionResult> GetSubtitle(int id)
{
Track subtitle = await _libraryManager.GetOrDefault<Track>(id);
return subtitle != null
? _files.FileResult(subtitle.Path)
: NotFound();
}
[HttpGet("{id:int}.{extension}")]
[Permission(nameof(SubtitleApi), Kind.Read)]
public async Task<IActionResult> GetSubtitle(int id, string extension)
{
Track subtitle = await _libraryManager.GetOrDefault<Track>(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<IActionResult> 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>(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<string> 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<string> 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<string> _ConvertBlock(IList<string> 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;
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<ICollection<ITask>> GetTasks()
{
return Ok(_taskManager.GetAllTasks());
}
[HttpGet("{taskSlug}")]
[HttpPut("{taskSlug}")]
[Permission(nameof(TaskApi), Kind.Create)]
public IActionResult RunTask(string taskSlug, [FromQuery] Dictionary<string, object> args)
{
try
{
_taskManager.StartTask(taskSlug, new Progress<float>(), args);
return Ok();
}
catch (ItemNotFoundException)
{
return NotFound();
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Track>
{
private readonly ILibraryManager _libraryManager;
public TrackApi(ILibraryManager libraryManager, IOptions<BasicOptions> options)
: base(libraryManager.TrackRepository, options.Value.PublicUrl)
{
_libraryManager = libraryManager;
}
[HttpGet("{id:int}/episode")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Episode>> GetEpisode(int id)
{
try
{
return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.ID == id));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/episode")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Episode>> GetEpisode(string slug)
{
try
{
return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.Slug == slug));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<BasicOptions> _options;
private readonly IFileSystem _files;
public VideoApi(ILibraryManager libraryManager,
ITranscoder transcoder,
IOptions<BasicOptions> 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<IActionResult> Direct(string slug)
{
try
{
Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(episode.Path, true);
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("transmux/{slug}/master.m3u8")]
[Permission("video", Kind.Read)]
public async Task<IActionResult> Transmux(string slug)
{
try
{
Episode episode = await _libraryManager.Get<Episode>(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<IActionResult> Transcode(string slug)
{
try
{
Episode episode = await _libraryManager.Get<Episode>(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");
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// An endpoint to retrieve subtitles for a specific episode.
/// </summary>
[Route("subtitles")]
[Route("subtitle", Order = AlternativeRoute)]
[PartialPermission(nameof(SubtitleApi))]
[ApiController]
[ApiDefinition("Subtitles", Group = WatchGroup)]
public class SubtitleApi : ControllerBase
{
/// <summary>
/// The library manager used to modify or retrieve information about the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used to send subtitles files.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// Create a new <see cref="SubtitleApi"/>.
/// </summary>
/// <param name="libraryManager">The library manager used to interact with the data store.</param>
/// <param name="files">The file manager used to send subtitle files.</param>
public SubtitleApi(ILibraryManager libraryManager, IFileSystem files)
{
_libraryManager = libraryManager;
_files = files;
}
/// <summary>
/// Get subtitle
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="identifier">
/// The ID or slug of the subtitle (the same as the corresponding <see cref="Track"/>).
/// </param>
/// <param name="extension">An optional extension for the subtitle file.</param>
/// <returns>The subtitle file</returns>
/// <response code="404">No subtitle exist with the given ID or slug.</response>
[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<IActionResult> GetSubtitle(Identifier identifier, string extension)
{
Track subtitle = await identifier.Match(
id => _libraryManager.GetOrDefault<Track>(id),
slug =>
{
if (slug.Count(x => x == '.') == 3)
{
int idx = slug.LastIndexOf('.');
extension = slug[(idx + 1)..];
slug = slug[..idx];
}
return _libraryManager.GetOrDefault<Track>(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);
}
/// <summary>
/// An action result that convert a subrip subtitle to vtt.
/// </summary>
private class ConvertSubripToVtt : IActionResult
{
/// <summary>
/// The path of the file to convert. It can be any path supported by a <see cref="IFileSystem"/>.
/// </summary>
private readonly string _path;
/// <summary>
/// The file system used to manipulate the given file.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// Create a new <see cref="ConvertSubripToVtt"/>.
/// </summary>
/// <param name="subtitlePath">
/// The path of the subtitle file. It can be any path supported by the given <paramref name="files"/>.
/// </param>
/// <param name="files">
/// The file system used to interact with the file at the given <paramref name="subtitlePath"/>.
/// </param>
public ConvertSubripToVtt(string subtitlePath, IFileSystem files)
{
_path = subtitlePath;
_files = files;
}
/// <inheritdoc />
public async Task ExecuteResultAsync(ActionContext context)
{
List<string> 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<string> processedBlock = _ConvertBlock(lines);
foreach (string t in processedBlock)
await writer.WriteLineAsync(t);
lines.Clear();
}
else
lines.Add(line);
}
}
await context.HttpContext.Response.Body.FlushAsync();
}
/// <summary>
/// Convert a block from subrip to vtt.
/// </summary>
/// <param name="lines">All the lines in the block.</param>
/// <returns>The given block, converted to vtt.</returns>
private static IList<string> _ConvertBlock(IList<string> 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;
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Track"/>.
/// A track contain metadata about a video, an audio or a subtitles.
/// </summary>
[Route("api/tracks")]
[Route("api/track", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(Track))]
[ApiDefinition("Tracks", Group = WatchGroup)]
public class TrackApi : CrudApi<Track>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="TrackApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
public TrackApi(ILibraryManager libraryManager)
: base(libraryManager.TrackRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get track's episode
/// </summary>
/// <remarks>
/// Get the episode that uses this track.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Track"/>.</param>
/// <returns>The episode that uses this track.</returns>
/// <response code="404">No track with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/episode")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Episode>> GetEpisode(Identifier identifier)
{
Episode ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn<Episode, Track>(x => x.Tracks));
if (ret == null)
return NotFound();
return ret;
}
}
}

Some files were not shown because too many files have changed in this diff Show More