Windows: Fixing windows setup

This commit is contained in:
Zoe Roux 2021-10-18 21:44:40 +02:00
commit 7e1ea00d0a
130 changed files with 4808 additions and 3115 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

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;
}
}
}

View File

@ -0,0 +1,146 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.IO;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Get the video in a raw format or transcoded in the codec you want.
/// </summary>
[Route("videos")]
[Route("video", Order = AlternativeRoute)]
[ApiController]
[ApiDefinition("Videos", Group = WatchGroup)]
public class VideoApi : Controller
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file system used to send video files.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// Create a new <see cref="VideoApi"/>.
/// </summary>
/// <param name="libraryManager">The library manager used to retrieve episodes.</param>
/// <param name="files">The file manager used to send video files.</param>
public VideoApi(ILibraryManager libraryManager,
IFileSystem files)
{
_libraryManager = libraryManager;
_files = files;
}
/// <inheritdoc />
/// <remarks>
/// Disabling the cache prevent an issue on firefox that skip the last 30 seconds of HLS files
/// </remarks>
public override void OnActionExecuted(ActionExecutedContext ctx)
{
base.OnActionExecuted(ctx);
ctx.HttpContext.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
ctx.HttpContext.Response.Headers.Add("Pragma", "no-cache");
ctx.HttpContext.Response.Headers.Add("Expires", "0");
}
/// <summary>
/// Direct video
/// </summary>
/// <remarks>
/// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or
/// transmuxing is done.
/// </remarks>
/// <param name="identifier">The identifier of the episode to retrieve.</param>
/// <returns>The raw video stream</returns>
/// <response code="404">No episode exists for the given identifier.</response>
// TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)]
[HttpGet("direct/{identifier:id}")]
[HttpGet("{identifier:id}", Order = AlternativeRoute)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Direct(Identifier identifier)
{
Episode episode = await identifier.Match(
id => _libraryManager.GetOrDefault<Episode>(id),
slug => _libraryManager.GetOrDefault<Episode>(slug)
);
return _files.FileResult(episode?.Path, true);
}
/// <summary>
/// Transmux video
/// </summary>
/// <remarks>
/// Change the container of the video to hls but don't re-encode the video or audio. This doesn't require mutch
/// resources from the server.
/// </remarks>
/// <param name="identifier">The identifier of the episode to retrieve.</param>
/// <returns>The transmuxed video stream</returns>
/// <response code="404">No episode exists for the given identifier.</response>
[HttpGet("transmux/{identifier:id}/master.m3u8")]
[Permission("video", Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Transmux(Identifier identifier)
{
Episode episode = await identifier.Match(
id => _libraryManager.GetOrDefault<Episode>(id),
slug => _libraryManager.GetOrDefault<Episode>(slug)
);
return _files.Transmux(episode);
}
/// <summary>
/// Transmuxed chunk
/// </summary>
/// <remarks>
/// Retrieve a chunk of a transmuxed video.
/// </remarks>
/// <param name="episodeLink">The identifier of the episode.</param>
/// <param name="chunk">The identifier of the chunk to retrieve.</param>
/// <param name="options">The options used to retrieve the path of the segments.</param>
/// <returns>A transmuxed video chunk.</returns>
[HttpGet("transmux/{episodeLink}/segments/{chunk}", Order = AlternativeRoute)]
[Permission("video", Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult GetTransmuxedChunk(string episodeLink, string chunk,
[FromServices] IOptions<BasicOptions> options)
{
string path = Path.GetFullPath(Path.Combine(options.Value.TransmuxPath, episodeLink));
path = Path.Combine(path, "segments", chunk);
return PhysicalFile(path, "video/MP2T");
}
}
}

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