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_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
Alphabetical order by first name.
Ordered by the date of the first commit.
* 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
- [Why should you indent with tabs](https://www.reddit.com/r/javascript/comments/c8drjo/nobody_talks_about_the_real_reason_to_use_tabs/)
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests)
- [GitHub Help](https://docs.github.com/en)

View File

@ -23,6 +23,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.WindowsTrait", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.Console", "src\Kyoo.Host.Console\Kyoo.Host.Console.csproj", "{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Swagger", "src\Kyoo.Swagger\Kyoo.Swagger.csproj", "{7D1A7596-73F6-4D35-842E-A5AD9C620596}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{FEAE1B0E-D797-470F-9030-0EF743575ECC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{8D28F5EF-0CD7-4697-A2A7-24EC31A48F21}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Databases", "Databases", "{865461CA-EC06-4B42-91CF-8723B0A9BB67}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{C569FF25-7E01-484C-9F72-5B99845AD94B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -81,5 +91,19 @@ Global
{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5}.Release|Any CPU.Build.0 = Release|Any CPU
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5} = {FEAE1B0E-D797-470F-9030-0EF743575ECC}
{BAB270D4-E0EA-4329-BA65-512FDAB01001} = {8D28F5EF-0CD7-4697-A2A7-24EC31A48F21}
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C} = {8D28F5EF-0CD7-4697-A2A7-24EC31A48F21}
{6F91B645-F785-46BB-9C4F-1EFC83E489B6} = {865461CA-EC06-4B42-91CF-8723B0A9BB67}
{3213C96D-0BF3-460B-A8B5-B9977229408A} = {865461CA-EC06-4B42-91CF-8723B0A9BB67}
{6515380E-1E57-42DA-B6E3-E1C8A848818A} = {865461CA-EC06-4B42-91CF-8723B0A9BB67}
{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5} = {C569FF25-7E01-484C-9F72-5B99845AD94B}
{98851001-40DD-46A6-94B3-2F8D90722076} = {C569FF25-7E01-484C-9F72-5B99845AD94B}
EndGlobalSection
EndGlobal

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>
<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>
<IsWindows Condition="$([MSBuild]::IsOSPlatform('Windows'))">true</IsWindows>
<IsOSX Condition="$([MSBuild]::IsOSPlatform('OSX'))">true</IsOSX>
@ -11,6 +31,10 @@
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="$(CheckCodingStyle) == true">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.354" PrivateAssets="All" />
@ -21,7 +45,6 @@
<PropertyGroup Condition="$(CheckCodingStyle) == true">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591;SA1600;SA1601</NoWarn>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet>
<!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode>-->

View File

@ -31,8 +31,6 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
public interface IFileSystem
{
// TODO find a way to handle Transmux/Transcode with this system.
/// <summary>
/// Used for http queries returning a file. This should be used to return local files
/// or proxy them from a distant server.
@ -51,7 +49,7 @@ namespace Kyoo.Abstractions.Controllers
/// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file).
/// </param>
/// <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>
/// 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>
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
/// <returns>A reader to read the file.</returns>
public Task<Stream> GetReader([NotNull] string path);
Task<Stream> GetReader([NotNull] string path);
/// <summary>
/// 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>
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
/// <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>
/// Create a new file at <paramref name="path"></paramref>.
/// </summary>
/// <param name="path">The path of the new file.</param>
/// <returns>A writer to write to the new file.</returns>
public Task<Stream> NewFile([NotNull] string path);
Task<Stream> NewFile([NotNull] string path);
/// <summary>
/// Create a new directory at the given path
/// </summary>
/// <param name="path">The path of the directory</param>
/// <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>
/// Combine multiple paths.
/// </summary>
/// <param name="paths">The paths to combine</param>
/// <returns>The combined path.</returns>
public string Combine(params string[] paths);
string Combine(params string[] paths);
/// <summary>
/// List files in a directory.
@ -99,7 +97,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="path">The path of the directory</param>
/// <param name="options">Should the search be recursive or not.</param>
/// <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);
/// <summary>
@ -107,7 +105,7 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path exists, false otherwise</returns>
public Task<bool> Exists([NotNull] string path);
Task<bool> Exists([NotNull] string path);
/// <summary>
/// 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>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <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.
/// </summary>
/// <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>
/// <returns>The first resource found that match the where function</returns>
[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;
/// <summary>
@ -317,6 +318,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
@ -330,6 +332,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
[Optional] Expression<Func<LibraryItem, bool>> where,
@ -344,6 +347,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null,
@ -357,6 +361,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
[Optional] Expression<Func<LibraryItem, bool>> where,
@ -371,6 +376,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null,
@ -384,6 +390,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
[Optional] Expression<Func<PeopleRole, bool>> where,
@ -398,6 +405,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null,
@ -411,6 +419,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
[Optional] Expression<Func<PeopleRole, bool>> where,
@ -425,6 +434,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null,
@ -438,6 +448,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
[Optional] Expression<Func<PeopleRole, bool>> where,
@ -452,6 +463,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null,
@ -465,6 +477,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
[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.
/// </summary>
/// <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>
[ItemCanBeNull]
Task<T> GetOrDefault(Expression<Func<T, bool>> where);
Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default);
/// <summary>
/// Search for resources.
@ -179,7 +180,6 @@ namespace Kyoo.Abstractions.Controllers
/// Delete all resources that match the predicate.
/// </summary>
/// <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>
Task DeleteAll([NotNull] Expression<Func<T, bool>> where);
}
@ -264,6 +264,8 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
public interface IEpisodeRepository : IRepository<Episode>
{
// TODO replace the next methods with extension methods.
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number.
/// </summary>
@ -343,6 +345,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
@ -356,6 +359,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
[Optional] Expression<Func<LibraryItem, bool>> where,
@ -370,6 +374,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null,
@ -383,6 +388,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
[Optional] Expression<Func<LibraryItem, bool>> where,
@ -418,6 +424,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null,
@ -431,6 +438,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetFromShow(int showID,
[Optional] Expression<Func<PeopleRole, bool>> where,
@ -445,6 +453,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null,
@ -458,6 +467,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
[Optional] Expression<Func<PeopleRole, bool>> where,
@ -472,6 +482,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null,
@ -485,6 +496,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetFromPeople(int id,
[Optional] Expression<Func<PeopleRole, bool>> where,
@ -499,6 +511,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</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>
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null,
@ -512,6 +525,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</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>
Task<ICollection<PeopleRole>> GetFromPeople(string slug,
[Optional] Expression<Func<PeopleRole, bool>> where,

View File

@ -1,20 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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>
<Title>Kyoo.Abstractions</Title>
<Description>Base package to create plugins for Kyoo.</Description>
<RootNamespace>Kyoo.Abstractions</RootNamespace>
</PropertyGroup>
@ -24,8 +13,6 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.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" />
</ItemGroup>
</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>
public Kind Kind { get; }
/// <summary>
/// The group of this permission.
/// </summary>
public Group Group { get; set; }
/// <summary>
/// Ask a permission to run an action.
/// </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
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
/// <param name="type">The type of the action</param>
public PartialPermissionAttribute(string type)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
}

View File

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

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));
}
/// <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)
{
return new(path, null);
return new ConfigurationReference(path, null);
}
}
}

View File

@ -18,8 +18,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models
{
@ -34,7 +34,8 @@ namespace Kyoo.Abstractions.Models
Show,
/// <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>
Movie,
@ -48,7 +49,7 @@ namespace Kyoo.Abstractions.Models
/// A type union between <see cref="Show"/> and <see cref="Collection"/>.
/// This is used to list content put inside a library.
/// </summary>
public class LibraryItem : IResource, IThumbnails
public class LibraryItem : CustomTypeDescriptor, IResource, IThumbnails
{
/// <inheritdoc />
public int ID { get; set; }
@ -86,14 +87,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
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>
/// The type of this item (ether a collection, a show or a movie).
/// </summary>
@ -169,5 +162,11 @@ namespace Kyoo.Abstractions.Models
Images = x.Images,
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
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes;
@ -42,15 +41,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
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>
/// The description of this collection.
/// </summary>

View File

@ -127,15 +127,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
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>
/// The title of this episode.
/// </summary>

View File

@ -33,9 +33,8 @@ namespace Kyoo.Abstractions.Models
/// <remarks>
/// An arbitrary index should not be used, instead use indexes from <see cref="Models.Images"/>
/// </remarks>
/// <example>{"0": "example.com/dune/poster"}</example>
public Dictionary<int, string> Images { get; set; }
// TODO remove Posters properties add them via the json serializer for every IThumbnails
}
/// <summary>
@ -63,5 +62,17 @@ namespace Kyoo.Abstractions.Models
/// A video of a few minutes that tease the content.
/// </summary>
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
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes;
@ -41,15 +40,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
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 />
[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
// 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;
@ -44,15 +43,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
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>
/// The list of libraries that uses this provider.
/// </summary>

View File

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

View File

@ -82,33 +82,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
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>
/// True if this show represent a movie, false otherwise.
/// </summary>

View File

@ -52,7 +52,7 @@ namespace Kyoo.Abstractions.Models
Subtitle = 3,
/// <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.
/// </summary>
Attachment = 4
@ -73,7 +73,7 @@ namespace Kyoo.Abstractions.Models
{
string type = Type.ToString().ToLower();
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString();
string episode = _episodeSlug ?? Episode?.Slug ?? EpisodeID.ToString();
return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : string.Empty)}.{type}";
}
@ -90,7 +90,7 @@ namespace Kyoo.Abstractions.Models
"Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]");
}
EpisodeSlug = match.Groups["ep"].Value;
_episodeSlug = match.Groups["ep"].Value;
Language = match.Groups["lang"].Value;
if (Language == "und")
Language = null;
@ -100,11 +100,6 @@ namespace Kyoo.Abstractions.Models
}
}
/// <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>
/// The title of the stream.
/// </summary>
@ -153,7 +148,16 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The episode that uses this track.
/// </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>
/// 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.
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>
/// Where to start? Using the given sort.
/// </summary>
public int AfterID { get; }
public int? AfterID { get; }
/// <summary>
/// Create a new <see cref="Pagination"/> instance.
/// </summary>
/// <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>
public Pagination(int count, int afterID = 0)
public Pagination(int count, int? afterID = null)
{
Count = count;
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.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -32,7 +33,7 @@ namespace Kyoo.Abstractions.Models
/// Information about tracks and display information that could be used by the player.
/// This contains mostly data from an <see cref="Episode"/> with another form.
/// </summary>
public class WatchItem
public class WatchItem : CustomTypeDescriptor, IThumbnails
{
/// <summary>
/// The ID of the episode associated with this item.
@ -101,26 +102,8 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public bool IsMovie { get; set; }
/// <summary>
/// The path of this item's poster.
/// By default, the http path for the poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </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; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// 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>
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.Tracks);
if (!ep.Show.IsMovie && ep.SeasonNumber != null && ep.EpisodeNumber != null)
Episode previous = null;
Episode next = null;
if (!ep.Show.IsMovie)
{
if (ep.EpisodeNumber > 1)
previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value - 1);
else if (ep.SeasonNumber > 1)
if (ep.AbsoluteNumber != null)
{
previous = (await library.GetAll(x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber.Value - 1,
limit: 1,
sort: new Sort<Episode>(x => x.EpisodeNumber, true))
).FirstOrDefault();
previous = await library.GetOrDefault(
x => x.ShowID == ep.ShowID && x.AbsoluteNumber < ep.AbsoluteNumber,
new Sort<Episode>(x => x.AbsoluteNumber, true)
);
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(ep.ShowID, ep.SeasonNumber.Value + 1, 1);
else
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value + 1);
}
else if (!ep.Show.IsMovie && ep.AbsoluteNumber != null)
{
previous = await library.GetOrDefault<Episode>(x => x.ShowID == ep.ShowID
&& x.AbsoluteNumber == ep.EpisodeNumber + 1);
next = await library.GetOrDefault<Episode>(x => x.ShowID == ep.ShowID
&& x.AbsoluteNumber == ep.AbsoluteNumber + 1);
next = await library.GetOrDefault(
x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber
&& x.EpisodeNumber > ep.EpisodeNumber,
new Sort<Episode>(x => x.EpisodeNumber)
);
next ??= await library.GetOrDefault(
x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber + 1,
new Sort<Episode>(x => x.EpisodeNumber)
);
}
}
return new WatchItem
@ -202,6 +199,7 @@ namespace Kyoo.Abstractions.Models
Title = ep.Title,
ReleaseDate = ep.ReleaseDate,
Path = ep.Path,
Images = ep.Show.Images,
Container = PathIO.GetExtension(ep.Path)![1..],
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
@ -239,5 +237,17 @@ namespace Kyoo.Abstractions.Models
return Array.Empty<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
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using Autofac;
using Autofac.Builder;
using Kyoo.Abstractions.Controllers;
@ -96,9 +97,10 @@ namespace Kyoo.Abstractions
/// </summary>
/// <param name="configuration">The configuration instance</param>
/// <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());
}
/// <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)
{
if (!query.Any())
@ -432,6 +437,11 @@ namespace Kyoo.Utils
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]
public static void ReThrow([NotNull] this Exception ex)
{

View File

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

View File

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

View File

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

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

View File

@ -283,7 +283,7 @@ namespace Kyoo.Core
builder.ReadFrom.Services(services);
const string template =
"[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 15} "
"[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 25} "
+ "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}";
if (SystemdHelpers.IsSystemdService())

View File

@ -32,6 +32,11 @@ using Newtonsoft.Json.Linq;
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
{
/// <summary>

View File

@ -214,5 +214,19 @@ namespace Kyoo.Core.Controllers
};
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.");
}
/// <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>
/// An <see cref="IActionResult"/> to proxy an http request.
/// </summary>

View File

@ -41,6 +41,11 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly IContentTypeProvider _provider;
/// <summary>
/// The transcoder of local files.
/// </summary>
private readonly ITranscoder _transcoder;
/// <summary>
/// Options to check if the metadata should be kept in the show directory or in a kyoo's directory.
/// </summary>
@ -51,10 +56,14 @@ namespace Kyoo.Core.Controllers
/// </summary>
/// <param name="options">The options to use.</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;
_provider = provider;
_transcoder = transcoder;
}
/// <summary>
@ -155,5 +164,17 @@ namespace Kyoo.Core.Controllers
_ => 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
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Threading.Tasks;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Kyoo.Abstractions.Controllers
namespace Kyoo.Core.Controllers
{
public interface ITranscoder
/// <summary>
/// The route constraint that goes with the <see cref="Identifier"/>.
/// </summary>
public class IdentifierRouteConstraint : IRouteConstraint
{
Task<Track[]> ExtractInfos(Episode episode, bool reextract);
Task<string> Transmux(Episode episode);
Task<string> Transcode(Episode episode);
/// <inheritdoc />
public bool Match(HttpContext httpContext,
IRouter route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
return values.ContainsKey(routeKey);
}
}
}

View File

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

View File

@ -29,6 +29,10 @@ namespace Kyoo.Core.Controllers
/// </summary>
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",
Justification = "ILogger should include the typeparam for context.")]
public PassthroughPermissionValidator(ILogger<PassthroughPermissionValidator> logger)

View File

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

View File

@ -115,9 +115,12 @@ namespace Kyoo.Core.Controllers
}
/// <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/>
@ -179,9 +182,9 @@ namespace Kyoo.Core.Controllers
query = sort.Descendant ? query.OrderByDescending(sortKey) : query.OrderBy(sortKey);
if (limit.AfterID != 0)
if (limit.AfterID != null)
{
TValue after = await get(limit.AfterID);
TValue after = await get(limit.AfterID.Value);
Expression key = Expression.Constant(sortKey.Compile()(after), sortExpression.Type);
query = query.Where(Expression.Lambda<Func<TValue, bool>>(
ApiHelper.StringCompatibleExpression(Expression.GreaterThan, sortExpression, key),

View File

@ -17,34 +17,70 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
// We use threads so tasks are not always awaited.
#pragma warning disable 4014
namespace Kyoo.Core.Controllers
{
/// <summary>
/// The transcoder used by the <see cref="LocalFileSystem"/>.
/// </summary>
public class Transcoder : ITranscoder
{
/// <summary>
/// The class that interact with the transcoder written in C.
/// </summary>
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";
/// <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)]
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();
/// <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,
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)
{
path = path.Replace('\\', '/');
@ -52,24 +88,47 @@ namespace Kyoo.Core.Controllers
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,
CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern IntPtr extract_infos(string path,
string outpath,
string outPath,
out uint length,
out uint trackCount,
bool reextracct);
bool reExtract);
/// <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)]
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('\\', '/');
outPath = outPath.Replace('\\', '/');
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;
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 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;
_options = options;
_library = library;
_logger = logger;
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.Show);
string dir = await _files.GetExtraDirectory(episode);
if (dir == null)
throw new ArgumentException("Invalid path.");
return await Task.Factory.StartNew(
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract),
TaskCreationOptions.LongRunning);
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reExtract),
TaskCreationOptions.LongRunning
);
}
public async Task<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 manifest = Path.Combine(folder, episode.Slug + ".m3u8");
float playableDuration = 0;
bool transmuxFailed = false;
string manifest = Path.GetFullPath(Path.Combine(folder, episode.Slug + ".m3u8"));
try
{
Directory.CreateDirectory(folder);
if (File.Exists(manifest))
return manifest;
return new PhysicalFileResult(manifest, "application/x-mpegurl");
}
catch (UnauthorizedAccessException)
{
await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config.");
return null;
_logger.LogCritical("Access to the path {Manifest} is denied. " +
"Please change your transmux path in the config", manifest);
return new StatusCodeResult(500);
}
Task.Factory.StartNew(() =>
{
transmuxFailed = TranscoderAPI.Transmux(episode.Path, manifest, out playableDuration) != 0;
}, TaskCreationOptions.LongRunning);
while (playableDuration < 10 || (!File.Exists(manifest) && !transmuxFailed))
await Task.Delay(10);
return transmuxFailed ? null : manifest;
return new TransmuxResult(episode.Path, manifest, _logger);
}
public Task<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.Collections.Generic;
using System.Linq;
using Autofac;
using Autofac.Core;
using Autofac.Core.Registration;
using Autofac.Extras.AttributeMetadata;
using Kyoo.Abstractions;
using Kyoo.Abstractions.Controllers;
using Kyoo.Core.Api;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Controllers;
using Kyoo.Core.Models.Options;
using Kyoo.Core.Tasks;
using Kyoo.Database;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Serilog;
using IMetadataProvider = Kyoo.Abstractions.Controllers.IMetadataProvider;
using JsonOptions = Kyoo.Core.Api.JsonOptions;
namespace Kyoo.Core
{
@ -63,20 +67,6 @@ namespace Kyoo.Core
{ "logging", null }
};
/// <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 />
public void Configure(ContainerBuilder builder)
{
@ -136,16 +126,31 @@ namespace Kyoo.Core
/// <inheritdoc />
public void Configure(IServiceCollection services)
{
string publicUrl = _configuration.GetPublicUrl();
services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
services.AddMvc().AddControllersAsServices();
services.AddControllers()
.AddNewtonsoftJson(x =>
services.AddMvcCore()
.AddNewtonsoftJson()
.AddDataAnnotations()
.AddControllersAsServices()
.AddApiExplorer()
.ConfigureApiBehaviorOptions(options =>
{
x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl);
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
options.SuppressMapClientErrors = true;
options.InvalidModelStateResponseFactory = ctx =>
{
string[] errors = ctx.ModelState
.SelectMany(x => x.Value.Errors)
.Select(x => x.ErrorMessage)
.ToArray();
return new BadRequestObjectResult(new RequestError(errors));
};
});
services.Configure<RouteOptions>(x =>
{
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
});
services.AddResponseCompression(x =>
{
x.EnableForHttps = true;

View File

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

View File

@ -1,13 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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>
<AssemblyName>Kyoo.Core</AssemblyName>
<RootNamespace>Kyoo.Core</RootNamespace>
<TranscoderRoot>../Kyoo.Transcoder/</TranscoderRoot>
</PropertyGroup>
<PropertyGroup>
@ -41,6 +38,7 @@
<ProjectReference Include="../Kyoo.SqLite/Kyoo.SqLite.csproj" />
<ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj" />
<ProjectReference Include="../Kyoo.WebApp/Kyoo.WebApp.csproj" />
<ProjectReference Include="../Kyoo.Swagger/Kyoo.Swagger.csproj" />
</ItemGroup>
<Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true' And !Exists('$(TranscoderRoot)/build/$(TranscoderBinary)')">
@ -62,6 +60,7 @@
<ItemGroup>
<Content Include="../../LICENSE" CopyToOutputDirectory="Always" Visible="false" />
<Content Include="../../AUTHORS.md" CopyToOutputDirectory="Always" Visible="false" />
<Content Include="settings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

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

View File

@ -19,18 +19,23 @@
using System;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// An API to retrieve or edit configuration settings
/// </summary>
[Route("api/config")]
[Route("api/configuration")]
[Route("api/config", Order = AlternativeRoute)]
[ApiController]
[PartialPermission("Configuration", Group = Group.Admin)]
[ApiDefinition("Configuration", Group = AdminGroup)]
public class ConfigurationApi : Controller
{
/// <summary>
@ -48,14 +53,19 @@ namespace Kyoo.Core.Api
}
/// <summary>
/// Get a permission from it's slug.
/// Get config value
/// </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>
/// <returns>The associate value or list of values.</returns>
/// <response code="200">Return the configuration value or the list of configurations</response>
/// <response code="404">No configuration exists for the given slug</response>
[HttpGet("{slug}")]
[Permission(nameof(ConfigurationApi), Kind.Read, Group.Admin)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<object> GetConfiguration(string slug)
{
try
@ -69,15 +79,20 @@ namespace Kyoo.Core.Api
}
/// <summary>
/// Edit a permission from it's slug.
/// Edit config
/// </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="newValue">The new value of the configuration</param>
/// <returns>The edited value.</returns>
/// <response code="200">Return the edited value</response>
/// <response code="404">No configuration exists for the given slug</response>
[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)
{
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.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
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 Expression StringCompatibleExpression(Func<Expression, Expression, BinaryExpression> operand,
Expression left,
Expression right)
/// <summary>
/// Make an expression (like
/// <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))
{
MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right);
return operand(call, Expression.Constant(0));
}
return operand(left, right);
if (left is not MemberExpression member || ((PropertyInfo)member.Member).PropertyType != typeof(string))
return operand(left, right);
MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right);
return operand(call, Expression.Constant(0));
}
/// <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,
Expression<Func<T, bool>> defaultWhere = null)
{
@ -96,18 +125,17 @@ namespace Kyoo.Core.Api
"not" when valueExpr == null => _ResourceEqual(propertyExpr, value, true),
"eq" => Expression.Equal(propertyExpr, valueExpr),
"not" => Expression.NotEqual(propertyExpr, valueExpr!),
"lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr),
"lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr),
"gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr),
"gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr),
"not" => Expression.NotEqual(propertyExpr, valueExpr),
"lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr!),
"lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr!),
"gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr!),
"gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr!),
_ => throw new ArgumentException($"Invalid operand: {operand}")
};
if (expression != null)
expression = Expression.AndAlso(expression, condition);
else
expression = condition;
expression = expression != null
? Expression.AndAlso(expression, condition)
: condition;
}
return Expression.Lambda<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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api
{
/// <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]
[ResourceView]
public class CrudApi<T> : ControllerBase
public class CrudApi<T> : BaseApi
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; }
public CrudApi(IRepository<T> repository, Uri baseURL)
/// <summary>
/// Create a new <see cref="CrudApi{T}"/> using the given repository and base url.
/// </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;
BaseURL = baseURL;
Repository = repository;
}
[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)]
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);
if (ret == null)
return NotFound();
return ret;
}
[HttpGet("{slug}")]
[PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<T>> Get(string slug)
{
T ret = await _repository.GetOrDefault(slug);
T ret = await identifier.Match(
id => Repository.GetOrDefault(id),
slug => Repository.GetOrDefault(slug)
);
if (ret == null)
return NotFound();
return ret;
}
/// <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")]
[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
{
return await _repository.GetCount(ApiHelper.ParseWhere<T>(where));
return await Repository.GetCount(ApiHelper.ParseWhere<T>(where));
}
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]
[PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<Page<T>>> GetAll([FromQuery] string sortBy,
[FromQuery] int afterID,
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<ActionResult<Page<T>>> GetAll(
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
[FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{
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 Pagination(limit, afterID));
new Pagination(limit, afterID)
);
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
return BadRequest(new RequestError(ex.Message));
}
}
protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit)
where TResult : IResource
{
return new Page<TResult>(resources,
new Uri(BaseURL, Request.Path),
Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase),
limit);
}
/// <summary>
/// Create new
/// </summary>
/// <remarks>
/// Create a new item and store it. You may leave the ID unspecified, it will be filed by Kyoo.
/// </remarks>
/// <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]
[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
{
return await _repository.Create(resource);
return await Repository.Create(resource);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
return BadRequest(new RequestError(ex.Message));
}
catch (DuplicatedItemException)
{
T existing = await _repository.GetOrDefault(resource.Slug);
T existing = await Repository.GetOrDefault(resource.Slug);
return Conflict(existing);
}
}
/// <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]
[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
{
if (resource.ID > 0)
return await _repository.Edit(resource, resetOld);
return await Repository.Edit(resource, resetOld);
T old = await _repository.Get(resource.Slug);
T old = await Repository.Get(resource.Slug);
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld);
return await Repository.Edit(resource, resetOld);
}
catch (ItemNotFoundException)
{
@ -145,44 +209,27 @@ namespace Kyoo.Core.Api
}
}
[HttpPut("{id:int}")]
[PartialPermission(Kind.Write)]
public virtual async Task<ActionResult<T>> Edit(int id, [FromQuery] bool resetOld, [FromBody] T resource)
{
resource.ID = id;
try
{
return await _repository.Edit(resource, resetOld);
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpPut("{slug}")]
[PartialPermission(Kind.Write)]
public virtual async Task<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}")]
/// <summary>
/// Delete an item
/// </summary>
/// <remarks>
/// Delete one item via it's ID or it's slug.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to delete.</param>
/// <returns>The item has successfully been deleted.</returns>
/// <response code="404">No item could be found with the given id or slug.</response>
[HttpDelete("{identifier:id}")]
[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
{
await _repository.Delete(id);
await identifier.Match(
id => Repository.Delete(id),
slug => Repository.Delete(slug)
);
}
catch (ItemNotFoundException)
{
@ -192,32 +239,28 @@ namespace Kyoo.Core.Api
return Ok();
}
[HttpDelete("{slug}")]
/// <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)]
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
{
await _repository.Delete(slug);
await Repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
}
catch (ItemNotFoundException)
catch (ArgumentException ex)
{
return NotFound();
}
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 BadRequest(new RequestError(ex.Message));
}
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.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
@ -32,8 +33,13 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Core.Api
{
/// <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
{
/// <inheritdoc />
public override void OnActionExecuting(ActionExecutingContext context)
{
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(Page<>))?.GetGenericArguments()[0] ?? type;
context.HttpContext.Items["ResourceType"] = type.Name;
PropertyInfo[] properties = type.GetProperties()
.Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null)
.ToArray();
if (fields.Count == 1 && fields.Contains("all"))
{
fields = properties.Select(x => x.Name).ToList();
}
else
{
fields = fields
@ -77,10 +83,9 @@ namespace Kyoo.Core.Api
?.Name;
if (property != null)
return property;
context.Result = new BadRequestObjectResult(new
{
Error = $"{x} does not exist on {type.Name}."
});
context.Result = new BadRequestObjectResult(
new RequestError($"{x} does not exist on {type.Name}.")
);
return null;
})
.ToList();
@ -92,6 +97,7 @@ namespace Kyoo.Core.Api
base.OnActionExecuting(context);
}
/// <inheritdoc />
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.Result is ObjectResult result)
@ -104,7 +110,7 @@ namespace Kyoo.Core.Api
if (result.DeclaredType == null)
return;
ILibraryManager library = context.HttpContext.RequestServices.GetService<ILibraryManager>();
ILibraryManager library = context.HttpContext.RequestServices.GetRequiredService<ILibraryManager>();
ICollection<string> fields = (ICollection<string>)context.HttpContext.Items["fields"];
Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>));
@ -113,13 +119,13 @@ namespace Kyoo.Core.Api
foreach (IResource resource in ((dynamic)result.Value).Items)
{
foreach (string field in fields!)
await library!.Load(resource, field);
await library.Load(resource, field);
}
}
else if (result.DeclaredType.IsAssignableTo(typeof(IResource)))
{
foreach (string field in fields!)
await library!.Load((IResource)result.Value, field);
await library.Load((IResource)result.Value, field);
}
}
}

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
{
/// <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>
{
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, PeopleRole value, JsonSerializer serializer)
{
ICollection<PeopleRole> oldPeople = value.Show?.People;
@ -46,6 +51,7 @@ namespace Kyoo.Core.Api
value.People.Roles = oldRoles;
}
/// <inheritdoc />
public override PeopleRole ReadJson(JsonReader reader,
Type objectType,
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