Transcoder: Including transcode methods in the filesystem interface

This commit is contained in:
Zoe Roux 2021-10-03 18:56:57 +02:00
parent 40e32a1689
commit cd5b953e0f
12 changed files with 448 additions and 252 deletions

View File

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

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

@ -1,32 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Threading.Tasks;
using Kyoo.Abstractions.Models;
namespace Kyoo.Abstractions.Controllers
{
public interface ITranscoder
{
Task<Track[]> ExtractInfos(Episode episode, bool reextract);
Task<string> Transmux(Episode episode);
Task<string> Transcode(Episode episode);
}
}

View File

@ -107,7 +107,8 @@ namespace Kyoo.Abstractions.Models.Utils
{
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
return Expression.Lambda<Func<T, bool>>(equal);
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
return Expression.Lambda<Func<T, bool>>(equal, parameters);
}
/// <summary>
@ -124,7 +125,8 @@ namespace Kyoo.Abstractions.Models.Utils
{
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
return Expression.Lambda<Func<T, bool>>(equal);
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
return Expression.Lambda<Func<T, bool>>(equal, parameters);
}
/// <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,15 @@ namespace Kyoo.Core.Controllers
_ => null
});
}
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
return _transcoder.ExtractInfos(episode, reExtract);
}
public IActionResult Transmux(Episode episode)
{
return _transcoder.Transmux(episode);
}
}
}

View File

@ -17,36 +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
// Private items that are external can't start with an _
#pragma warning disable IDE1006
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('\\', '/');
@ -54,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;
@ -100,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

@ -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

@ -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,134 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <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
{
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _files;
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");
}
}
}

View File

@ -28,7 +28,6 @@ using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;
using NSwag;
using NSwag.Generation.AspNetCore;
using NSwag.Generation.Processors.Security;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Swagger
@ -90,40 +89,35 @@ namespace Kyoo.Swagger
x.Type = JsonObjectType.String | JsonObjectType.Integer;
}));
document.AddSecurity("Kyoo", new OpenApiSecurityScheme()
document.AddSecurity("Kyoo", new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.OpenIdConnect,
Description = "Kyoo's OpenID Authentication",
Flow = OpenApiOAuth2Flow.AccessCode,
Flows = new OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow()
{
Scopes = new Dictionary<string, string>
{
{ "read", "Read access to protected resources" },
{ "write", "Write access to protected resources" }
},
AuthorizationUrl = "https://localhost:44333/core/connect/authorize",
TokenUrl = "https://localhost:44333/core/connect/token"
}
}
OpenIdConnectUrl = "/.well-known/openid-configuration",
Description = "You can login via an OIDC client, clients must be first registered in kyoo. " +
"Documentation coming soon."
});
document.OperationProcessors.Add(new OperationPermissionProcessor());
document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Overall.ToString(), new OpenApiSecurityScheme
// This does not respect the swagger's specification but it works for swaggerUi and ReDoc so honestly this will do.
document.AddSecurity(Group.Overall.ToString(), new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header,
Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate."
}));
document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Admin.ToString(), new OpenApiSecurityScheme
ExtensionData = new Dictionary<string, object>
{
["type"] = "OpenID Connect or Api Key"
},
Description = "Kyoo's permissions work by groups. Permissions are attributed to " +
"a specific group and if a user has a group permission, it will be the same as having every " +
"permission in the group. For example, having overall.read gives you collections.read, " +
"shows.read and so on."
});
document.AddSecurity(Group.Admin.ToString(), new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header,
Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate."
}));
ExtensionData = new Dictionary<string, object>
{
["type"] = "OpenID Connect or Api Key"
},
Description = "The permission group used for administrative items like tasks, account management " +
"and library creation."
});
});
}