From 57e49b7e838c230f75892d7ca69ce68f175f8832 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Mar 2021 02:52:18 +0100 Subject: [PATCH] Reworking the transcoder and creating a file manager --- Kyoo.Common/Controllers/IFileManager.cs | 16 +++ Kyoo.Common/Controllers/ITranscoder.cs | 3 +- Kyoo.Common/Kyoo.Common.csproj | 1 + Kyoo.Common/Models/Resources/Track.cs | 4 +- Kyoo/Controllers/FileManager.cs | 41 ++++++ Kyoo/Controllers/Transcoder.cs | 134 +++++++++++++++++++ Kyoo/Controllers/Transcoder/Transcoder.cs | 70 ---------- Kyoo/Controllers/Transcoder/TranscoderAPI.cs | 60 --------- Kyoo/Startup.cs | 1 + Kyoo/Tasks/Crawler.cs | 4 +- Kyoo/Tasks/ExtractMetadata.cs | 4 +- Kyoo/Views/API/SubtitleApi.cs | 21 +-- Kyoo/Views/API/VideoApi.cs | 54 +++----- transcoder | 2 +- 14 files changed, 235 insertions(+), 180 deletions(-) create mode 100644 Kyoo.Common/Controllers/IFileManager.cs create mode 100644 Kyoo/Controllers/FileManager.cs create mode 100644 Kyoo/Controllers/Transcoder.cs delete mode 100644 Kyoo/Controllers/Transcoder/Transcoder.cs delete mode 100644 Kyoo/Controllers/Transcoder/TranscoderAPI.cs diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileManager.cs new file mode 100644 index 00000000..8e2d52fb --- /dev/null +++ b/Kyoo.Common/Controllers/IFileManager.cs @@ -0,0 +1,16 @@ +using System.IO; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Controllers +{ + public interface IFileManager + { + public IActionResult FileResult([NotNull] string path, bool rangeSupport = false); + + public StreamReader GetReader([NotNull] string path); + // TODO implement a List for directorys, a Exist to check existance and all. + // TODO replace every use of System.IO with this to allow custom paths (like uptobox://path) + // TODO find a way to handle Transmux/Transcode with this system. + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ITranscoder.cs b/Kyoo.Common/Controllers/ITranscoder.cs index 0eee0f59..afaa524f 100644 --- a/Kyoo.Common/Controllers/ITranscoder.cs +++ b/Kyoo.Common/Controllers/ITranscoder.cs @@ -1,12 +1,11 @@ using Kyoo.Models; -using Kyoo.Models.Watch; using System.Threading.Tasks; namespace Kyoo.Controllers { public interface ITranscoder { - Task ExtractInfos(string path); + Task ExtractInfos(string path, bool reextract); Task Transmux(Episode episode); Task Transcode(Episode episode); } diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 59d84b4b..9a1ba82b 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -22,6 +22,7 @@ + diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 07a2efbf..92438e0a 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -12,7 +12,7 @@ namespace Kyoo.Models Video = 1, Audio = 2, Subtitle = 3, - Font = 4 + Attachment = 4 } namespace Watch @@ -100,7 +100,7 @@ namespace Kyoo.Models StreamType.Subtitle => "", StreamType.Video => "video.", StreamType.Audio => "audio.", - StreamType.Font => "font.", + StreamType.Attachment => "font.", _ => "" }; string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; diff --git a/Kyoo/Controllers/FileManager.cs b/Kyoo/Controllers/FileManager.cs new file mode 100644 index 00000000..e6be6f6d --- /dev/null +++ b/Kyoo/Controllers/FileManager.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; + +namespace Kyoo.Controllers +{ + public class FileManager : ControllerBase, IFileManager + { + private FileExtensionContentTypeProvider _provider; + + private string _GetContentType(string path) + { + if (_provider == null) + { + _provider = new FileExtensionContentTypeProvider(); + _provider.Mappings[".mkv"] = "video/x-matroska"; + } + + if (_provider.TryGetContentType(path, out string contentType)) + return contentType; + return "video/mp4"; + } + + public IActionResult FileResult(string path, bool range) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (!System.IO.File.Exists(path)) + return NotFound(); + return PhysicalFile(path, _GetContentType(path), range); + } + + public StreamReader GetReader(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return new StreamReader(path); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/Transcoder.cs b/Kyoo/Controllers/Transcoder.cs new file mode 100644 index 00000000..25e1722b --- /dev/null +++ b/Kyoo/Controllers/Transcoder.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Kyoo.Models; +using Microsoft.Extensions.Configuration; +using Stream = Kyoo.Models.Watch.Stream; + +// We use threads so tasks are not always awaited. +#pragma warning disable 4014 + +namespace Kyoo.Controllers +{ + public class BadTranscoderException : Exception {} + + public class Transcoder : ITranscoder + { + private static class TranscoderAPI + { + private const string TranscoderPath = "libtranscoder.so"; + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + public static extern int init(); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + public static extern int transmux(string path, string outpath, out float playableDuration); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + public static extern int transcode(string path, string outpath, out float playableDuration); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr extract_infos(string path, + string outpath, + out int length, + out int trackCount, + bool reextracct); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + private static extern void free(IntPtr ptr); + + + public static Track[] ExtractInfos(string path, string outPath, bool reextract) + { + int size = Marshal.SizeOf(); + IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount, reextract); + IntPtr streamsPtr = ptr; + Track[] tracks; + + if (trackCount > 0 && ptr != IntPtr.Zero) + { + tracks = new Track[trackCount]; + + int j = 0; + for (int i = 0; i < arrayLength; i++) + { + Stream stream = Marshal.PtrToStructure(streamsPtr); + if (stream!.Type != StreamType.Unknown) + { + tracks[j] = new Track(stream); + j++; + } + streamsPtr += size; + } + } + else + tracks = Array.Empty(); + + if (ptr != IntPtr.Zero) + free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers. + return tracks; + } + } + + private readonly string _transmuxPath; + private readonly string _transcodePath; + + public Transcoder(IConfiguration config) + { + _transmuxPath = Path.GetFullPath(config.GetValue("transmuxTempPath")); + _transcodePath = Path.GetFullPath(config.GetValue("transcodeTempPath")); + + if (TranscoderAPI.init() != Marshal.SizeOf()) + throw new BadTranscoderException(); + } + + public Task ExtractInfos(string path, bool reextract) + { + string dir = Path.GetDirectoryName(path); + if (dir == null) + throw new ArgumentException("Invalid path."); + dir = Path.Combine(dir, "Extra"); + return Task.Factory.StartNew( + () => TranscoderAPI.ExtractInfos(path, dir, reextract), + TaskCreationOptions.LongRunning); + } + + public async Task Transmux(Episode episode) + { + if (!File.Exists(episode.Path)) + throw new ArgumentException("Path does not exists. Can't transcode."); + + string folder = Path.Combine(_transmuxPath, episode.Slug); + string manifest = Path.Combine(folder, episode.Slug + ".m3u8"); + float playableDuration = 0; + bool transmuxFailed = false; + + try + { + Directory.CreateDirectory(folder); + if (File.Exists(manifest)) + return manifest; + } + catch (UnauthorizedAccessException) + { + await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config."); + return null; + } + + Task.Factory.StartNew(() => + { + string cleanManifest = manifest.Replace('\\', '/'); + transmuxFailed = TranscoderAPI.transmux(episode.Path, cleanManifest, out playableDuration) != 0; + }, TaskCreationOptions.LongRunning); + while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed) + await Task.Delay(10); + return transmuxFailed ? null : manifest; + } + + public Task Transcode(Episode episode) + { + return Task.FromResult(null); // Not implemented yet. + } + } +} diff --git a/Kyoo/Controllers/Transcoder/Transcoder.cs b/Kyoo/Controllers/Transcoder/Transcoder.cs deleted file mode 100644 index 479ac32b..00000000 --- a/Kyoo/Controllers/Transcoder/Transcoder.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Kyoo.Models; -using Microsoft.Extensions.Configuration; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Kyoo.Controllers.TranscoderLink; -#pragma warning disable 4014 - -namespace Kyoo.Controllers -{ - public class BadTranscoderException : Exception {} - - public class Transcoder : ITranscoder - { - private readonly string _transmuxPath; - private readonly string _transcodePath; - - public Transcoder(IConfiguration config) - { - _transmuxPath = Path.GetFullPath(config.GetValue("transmuxTempPath")); - _transcodePath = Path.GetFullPath(config.GetValue("transcodeTempPath")); - - if (TranscoderAPI.init() != Marshal.SizeOf()) - throw new BadTranscoderException(); - } - - public Task ExtractInfos(string path) - { - string dir = Path.GetDirectoryName(path); - if (dir == null) - throw new ArgumentException("Invalid path."); - - return Task.Factory.StartNew(() => TranscoderAPI.ExtractInfos(path, dir), TaskCreationOptions.LongRunning); - } - - public async Task Transmux(Episode episode) - { - string folder = Path.Combine(_transmuxPath, episode.Slug); - string manifest = Path.Combine(folder, episode.Slug + ".m3u8"); - float playableDuration = 0; - bool transmuxFailed = false; - - try - { - Directory.CreateDirectory(folder); - if (File.Exists(manifest)) - return manifest; - } - catch (UnauthorizedAccessException) - { - await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config."); - return null; - } - - Task.Factory.StartNew(() => - { - transmuxFailed = TranscoderAPI.transmux(episode.Path, manifest.Replace('\\', '/'), out playableDuration) != 0; - }, TaskCreationOptions.LongRunning); - while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed) - await Task.Delay(10); - return transmuxFailed ? null : manifest; - } - - public Task Transcode(Episode episode) - { - return Task.FromResult(null); // Not implemented yet. - } - } -} diff --git a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs deleted file mode 100644 index 6abb5a51..00000000 --- a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Kyoo.Models; -using Kyoo.Models.Watch; -// ReSharper disable InconsistentNaming - -namespace Kyoo.Controllers.TranscoderLink -{ - public static class TranscoderAPI - { - private const string TranscoderPath = "libtranscoder.so"; - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - public static extern int init(); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - public static extern int transmux(string path, string out_path, out float playableDuration); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - public static extern int transcode(string path, string out_path, out float playableDuration); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr extract_infos(string path, string outpath, out int length, out int track_count); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - private static extern void free(IntPtr stream_ptr); - - - public static Track[] ExtractInfos(string path, string outPath) - { - int size = Marshal.SizeOf(); - IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount); - IntPtr streamsPtr = ptr; - Track[] tracks; - - if (trackCount > 0 && ptr != IntPtr.Zero) - { - tracks = new Track[trackCount]; - - int j = 0; - for (int i = 0; i < arrayLength; i++) - { - Stream stream = Marshal.PtrToStructure(streamsPtr); - if (stream!.Type != StreamType.Unknown) - { - tracks[j] = new Track(stream); - j++; - } - streamsPtr += size; - } - } - else - tracks = Array.Empty(); - - if (ptr != IntPtr.Zero) - free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers. - return tracks; - } - } -} diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 9c39498d..475f8691 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -158,6 +158,7 @@ namespace Kyoo services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 0c0db5c4..853ea5a9 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -359,8 +359,8 @@ namespace Kyoo.Controllers private async Task> GetTracks(Episode episode) { - episode.Tracks = (await _transcoder.ExtractInfos(episode.Path)) - .Where(x => x.Type != StreamType.Font) + episode.Tracks = (await _transcoder.ExtractInfos(episode.Path, false)) + .Where(x => x.Type != StreamType.Attachment) .ToArray(); return episode.Tracks; } diff --git a/Kyoo/Tasks/ExtractMetadata.cs b/Kyoo/Tasks/ExtractMetadata.cs index 4dccc70f..a36659f4 100644 --- a/Kyoo/Tasks/ExtractMetadata.cs +++ b/Kyoo/Tasks/ExtractMetadata.cs @@ -101,8 +101,8 @@ namespace Kyoo.Tasks if (subs) { await _library.Load(episode, x => x.Tracks); - episode.Tracks = (await _transcoder!.ExtractInfos(episode.Path)) - .Where(x => x.Type != StreamType.Font) + episode.Tracks = (await _transcoder!.ExtractInfos(episode.Path, true)) + .Where(x => x.Type != StreamType.Attachment) .Concat(episode.Tracks.Where(x => x.IsExternal)) .ToList(); await _library.EditEpisode(episode, false); diff --git a/Kyoo/Views/API/SubtitleApi.cs b/Kyoo/Views/API/SubtitleApi.cs index 8cdc124b..73f151ee 100644 --- a/Kyoo/Views/API/SubtitleApi.cs +++ b/Kyoo/Views/API/SubtitleApi.cs @@ -14,10 +14,12 @@ namespace Kyoo.Api public class SubtitleApi : ControllerBase { private readonly ILibraryManager _libraryManager; + private readonly IFileManager _files; - public SubtitleApi(ILibraryManager libraryManager) + public SubtitleApi(ILibraryManager libraryManager, IFileManager files) { _libraryManager = libraryManager; + _files = files; } @@ -39,9 +41,8 @@ namespace Kyoo.Api return NotFound(); if (subtitle.Codec == "subrip" && extension == "vtt") - return new ConvertSubripToVtt(subtitle.Path); - string mime = subtitle.Codec == "ass" ? "text/x-ssa" : "application/x-subrip"; - return PhysicalFile(subtitle.Path, mime); + return new ConvertSubripToVtt(subtitle.Path, _files); + return _files.FileResult(subtitle.Path); } } @@ -49,27 +50,29 @@ namespace Kyoo.Api public class ConvertSubripToVtt : IActionResult { private readonly string _path; + private readonly IFileManager _files; - public ConvertSubripToVtt(string subtitlePath) + public ConvertSubripToVtt(string subtitlePath, IFileManager files) { _path = subtitlePath; + _files = files; } public async Task ExecuteResultAsync(ActionContext context) { - string line; - List lines = new List(); + List lines = new(); context.HttpContext.Response.StatusCode = 200; context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt"); - await using (StreamWriter writer = new StreamWriter(context.HttpContext.Response.Body)) + await using (StreamWriter writer = new(context.HttpContext.Response.Body)) { await writer.WriteLineAsync("WEBVTT"); await writer.WriteLineAsync(""); await writer.WriteLineAsync(""); - using StreamReader reader = new StreamReader(_path); + using StreamReader reader = _files.GetReader(_path); + string line; while ((line = await reader.ReadLineAsync()) != null) { if (line == "") diff --git a/Kyoo/Views/API/VideoApi.cs b/Kyoo/Views/API/VideoApi.cs index f92bdd6a..1e6651b5 100644 --- a/Kyoo/Views/API/VideoApi.cs +++ b/Kyoo/Views/API/VideoApi.cs @@ -1,12 +1,11 @@ -using Kyoo.Controllers; +using System.IO; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; -using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.StaticFiles; namespace Kyoo.Api { @@ -16,14 +15,18 @@ namespace Kyoo.Api { private readonly ILibraryManager _libraryManager; private readonly ITranscoder _transcoder; + private readonly IFileManager _files; private readonly string _transmuxPath; private readonly string _transcodePath; - private FileExtensionContentTypeProvider _provider; - public VideoApi(ILibraryManager libraryManager, ITranscoder transcoder, IConfiguration config) + public VideoApi(ILibraryManager libraryManager, + ITranscoder transcoder, + IConfiguration config, + IFileManager files) { _libraryManager = libraryManager; _transcoder = transcoder; + _files = files; _transmuxPath = config.GetValue("transmuxTempPath"); _transcodePath = config.GetValue("transcodeTempPath"); } @@ -37,19 +40,6 @@ namespace Kyoo.Api ctx.HttpContext.Response.Headers.Add("Expires", "0"); } - private string _GetContentType(string path) - { - if (_provider == null) - { - _provider = new FileExtensionContentTypeProvider(); - _provider.Mappings[".mkv"] = "video/x-matroska"; - } - - if (_provider.TryGetContentType(path, out string contentType)) - return contentType; - return "video/mp4"; - } - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] [HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] @@ -60,9 +50,9 @@ namespace Kyoo.Api return BadRequest(new {error = "Season number or episode number can not be negative."}); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - if (episode != null && System.IO.File.Exists(episode.Path)) - return PhysicalFile(episode.Path, _GetContentType(episode.Path), true); - return NotFound(); + if (episode == null) + return NotFound(); + return _files.FileResult(episode.Path, true); } [HttpGet("{movieSlug}")] @@ -72,9 +62,9 @@ namespace Kyoo.Api { Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - if (episode != null && System.IO.File.Exists(episode.Path)) - return PhysicalFile(episode.Path, _GetContentType(episode.Path), true); - return NotFound(); + if (episode == null) + return NotFound(); + return _files.FileResult(episode.Path, true); } @@ -86,12 +76,12 @@ namespace Kyoo.Api return BadRequest(new {error = "Season number or episode number can not be negative."}); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transmux(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } [HttpGet("transmux/{movieSlug}/master.m3u8")] @@ -100,12 +90,12 @@ namespace Kyoo.Api { Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transmux(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } [HttpGet("transcode/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")] @@ -116,12 +106,12 @@ namespace Kyoo.Api return BadRequest(new {error = "Season number or episode number can not be negative."}); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transcode(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } [HttpGet("transcode/{movieSlug}/master.m3u8")] @@ -130,12 +120,12 @@ namespace Kyoo.Api { Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transcode(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } diff --git a/transcoder b/transcoder index 3885dca7..9acd635a 160000 --- a/transcoder +++ b/transcoder @@ -1 +1 @@ -Subproject commit 3885dca743bbde5d83cb3816646455856fc5c316 +Subproject commit 9acd635aca92ad81f1de562e34b2c7c270bade29