From 2dd0806517d9f9be8efdddca578637ae570efa07 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 26 Oct 2020 06:22:03 +0100 Subject: [PATCH] Cleaning up the video & the subtitle api --- Kyoo.Common/Controllers/ILibraryManager.cs | 3 +- .../Implementations/LibraryManager.cs | 7 +- Kyoo.Common/Models/Resources/Episode.cs | 4 +- .../Repositories/TrackRepository.cs | 12 +- Kyoo/Controllers/Transcoder/Transcoder.cs | 2 +- Kyoo/Startup.cs | 3 +- .../API/{SubtitleAPI.cs => SubtitleApi.cs} | 78 +++------ Kyoo/Views/API/{VideoAPI.cs => VideoApi.cs} | 157 ++++++++++-------- transcoder | 2 +- 9 files changed, 140 insertions(+), 128 deletions(-) rename Kyoo/Views/API/{SubtitleAPI.cs => SubtitleApi.cs} (64%) rename Kyoo/Views/API/{VideoAPI.cs => VideoApi.cs} (55%) diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 0a849ec7..e9a14311 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -32,6 +32,7 @@ namespace Kyoo.Controllers Task GetEpisode(int id); Task GetEpisode(int showID, int seasonNumber, int episodeNumber); Task GetGenre(int id); + Task GetTrack(int id); Task GetStudio(int id); Task GetPeople(int id); @@ -42,7 +43,7 @@ namespace Kyoo.Controllers Task GetSeason(string showSlug, int seasonNumber); Task GetEpisode(string showSlug, int seasonNumber, int episodeNumber); Task GetMovieEpisode(string movieSlug); - Task GetTrack(int id); + Task GetTrack(string slug); Task GetGenre(string slug); Task GetStudio(string slug); Task GetPeople(string slug); diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index 87e9b383..9607fa8d 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -109,7 +109,12 @@ namespace Kyoo.Controllers { return EpisodeRepository.Get(showID, seasonNumber, episodeNumber); } - + + public Task GetTrack(string slug) + { + return TrackRepository.Get(slug); + } + public Task GetGenre(int id) { return GenreRepository.Get(id); diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 250ee4db..f32a03ef 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -93,7 +93,9 @@ namespace Kyoo.Models public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber) { - return showSlug + "-s" + seasonNumber + "e" + episodeNumber; + if (seasonNumber == -1) + return showSlug; + return $"{showSlug}-s{seasonNumber}e{episodeNumber}"; } public void OnMerge(object merged) diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index f1d4fa54..1238885b 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -39,18 +39,21 @@ namespace Kyoo.Controllers public override Task Get(string slug) { Match match = Regex.Match(slug, - @"(?.*)-s(?\d*)-e(?\d*).(?.{0,3})(?-forced)?(\..*)?"); + @"(?.*)-s(?\d+)e(?\d+)\.(?.{0,3})(?-forced)?(\..*)?"); if (!match.Success) { if (int.TryParse(slug, out int id)) return Get(id); - throw new ArgumentException("Invalid track slug. Format: {episodeSlug}.{language}[-forced][.{extension}]"); + match = Regex.Match(slug, @"(?.*)\.(?.{0,3})(?-forced)?(\..*)?"); + if (!match.Success) + throw new ArgumentException("Invalid track slug. " + + "Format: {episodeSlug}.{language}[-forced][.{extension}]"); } string showSlug = match.Groups["show"].Value; - int seasonNumber = int.Parse(match.Groups["season"].Value); - int episodeNumber = int.Parse(match.Groups["episode"].Value); + int seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : -1; + int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1; string language = match.Groups["language"].Value; bool forced = match.Groups["forced"].Success; return _database.Tracks.FirstOrDefaultAsync(x => x.Episode.Show.Slug == showSlug @@ -59,6 +62,7 @@ namespace Kyoo.Controllers && x.Language == language && x.IsForced == forced); } + public Task> Search(string query) { throw new InvalidOperationException("Tracks do not support the search method."); diff --git a/Kyoo/Controllers/Transcoder/Transcoder.cs b/Kyoo/Controllers/Transcoder/Transcoder.cs index 43807b0d..c1335486 100644 --- a/Kyoo/Controllers/Transcoder/Transcoder.cs +++ b/Kyoo/Controllers/Transcoder/Transcoder.cs @@ -79,7 +79,7 @@ namespace Kyoo.Controllers public Task Transcode(Episode episode) { - return null; // Not implemented yet. + return Task.FromResult(null); // Not implemented yet. } } } diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index fb0884cb..7c30527b 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -32,7 +32,6 @@ namespace Kyoo _loggerFactory = loggerFactory; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { @@ -132,7 +131,7 @@ namespace Kyoo { AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) } }); - + services.AddScoped(); services.AddScoped(); diff --git a/Kyoo/Views/API/SubtitleAPI.cs b/Kyoo/Views/API/SubtitleApi.cs similarity index 64% rename from Kyoo/Views/API/SubtitleAPI.cs rename to Kyoo/Views/API/SubtitleApi.cs index 3ffd72c3..b078ddbe 100644 --- a/Kyoo/Views/API/SubtitleAPI.cs +++ b/Kyoo/Views/API/SubtitleApi.cs @@ -1,68 +1,46 @@ -using Kyoo.Models; +using System; +using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; -using Kyoo.Models.Watch; using Microsoft.AspNetCore.Authorization; namespace Kyoo.Api { - [Route("[controller]")] + [Route("subtitle")] [ApiController] - public class SubtitleController : ControllerBase + public class SubtitleApi : ControllerBase { private readonly ILibraryManager _libraryManager; - //private readonly ITranscoder _transcoder; - public SubtitleController(ILibraryManager libraryManager/*, ITranscoder transcoder*/) + public SubtitleApi(ILibraryManager libraryManager) { _libraryManager = libraryManager; - // _transcoder = transcoder; } - //TODO Create a real route for movie's subtitles. - - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}.{identifier}.{extension?}")] + + [HttpGet("{slug}.{extension?}")] [Authorize(Policy="Play")] - public async Task GetSubtitle(string showSlug, - int seasonNumber, - int episodeNumber, - string identifier, - string extension) + public async Task GetSubtitle(string slug, string extension) { - string languageTag = identifier.Length >= 3 ? identifier.Substring(0, 3) : null; - bool forced = identifier.Length > 4 && identifier.Substring(4) == "forced"; - Track subtitle = null; - - if (languageTag != null) - subtitle = (await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber))?.Tracks - .FirstOrDefault(x => x.Type == StreamType.Subtitle && x.Language == languageTag && x.IsForced == forced); - - if (subtitle == null) + Track subtitle; + try { - string idString = identifier.IndexOf('-') != -1 - ? identifier.Substring(0, identifier.IndexOf('-')) - : identifier; - int.TryParse(idString, out int id); - subtitle = await _libraryManager.GetTrack(id); + subtitle = await _libraryManager.GetTrack(slug); } - + catch (ArgumentException ex) + { + return BadRequest(new {error = ex.Message}); + } + if (subtitle == null) return NotFound(); if (subtitle.Codec == "subrip" && extension == "vtt") return new ConvertSubripToVtt(subtitle.Path); - - string mime; - if (subtitle.Codec == "ass") - mime = "text/x-ssa"; - else - mime = "application/x-subrip"; - - // TODO Should use appropriate mime type here + string mime = subtitle.Codec == "ass" ? "text/x-ssa" : "application/x-subrip"; return PhysicalFile(subtitle.Path, mime); } @@ -129,21 +107,19 @@ namespace Kyoo.Api await writer.WriteLineAsync(""); await writer.WriteLineAsync(""); - using (StreamReader reader = new StreamReader(_path)) + using StreamReader reader = new StreamReader(_path); + while ((line = await reader.ReadLineAsync()) != null) { - while ((line = await reader.ReadLineAsync()) != null) + if (line == "") { - if (line == "") - { - lines.Add(""); - IEnumerable processedBlock = ConvertBlock(lines); - foreach (string t in processedBlock) - await writer.WriteLineAsync(t); - lines.Clear(); - } - else - lines.Add(line); + lines.Add(""); + IEnumerable processedBlock = ConvertBlock(lines); + foreach (string t in processedBlock) + await writer.WriteLineAsync(t); + lines.Clear(); } + else + lines.Add(line); } } diff --git a/Kyoo/Views/API/VideoAPI.cs b/Kyoo/Views/API/VideoApi.cs similarity index 55% rename from Kyoo/Views/API/VideoAPI.cs rename to Kyoo/Views/API/VideoApi.cs index 2c12cea7..cafba9ba 100644 --- a/Kyoo/Views/API/VideoAPI.cs +++ b/Kyoo/Views/API/VideoApi.cs @@ -5,19 +5,21 @@ using Microsoft.Extensions.Configuration; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.StaticFiles; namespace Kyoo.Api { - [Route("[controller]")] + [Route("video")] [ApiController] - public class VideoController : ControllerBase + public class VideoApi : ControllerBase { private readonly ILibraryManager _libraryManager; private readonly ITranscoder _transcoder; private readonly string _transmuxPath; private readonly string _transcodePath; + private FileExtensionContentTypeProvider _provider; - public VideoController(ILibraryManager libraryManager, ITranscoder transcoder, IConfiguration config) + public VideoApi(ILibraryManager libraryManager, ITranscoder transcoder, IConfiguration config) { _libraryManager = libraryManager; _transcoder = transcoder; @@ -25,101 +27,124 @@ namespace Kyoo.Api _transcodePath = config.GetValue("transcodeTempPath"); } - [HttpGet("{showSlug}-s{seasonNumber}e{episodeNumber}")] - [Authorize(Policy="Play")] - public async Task Index(string showSlug, int seasonNumber, int episodeNumber) + private string _GetContentType(string path) { - Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); + 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}e{episodeNumber}")] + [HttpGet("direct/{showSlug}-s{seasonNumber}e{episodeNumber}")] + [Authorize(Policy="Play")] + public async Task DirectEpisode(string showSlug, int seasonNumber, int episodeNumber) + { + if (seasonNumber < 0 || episodeNumber < 0) + 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, "video/x-matroska", true); + return PhysicalFile(episode.Path, _GetContentType(episode.Path), true); return NotFound(); } + + [HttpGet("{movieSlug}")] + [HttpGet("direct/{movieSlug}")] + [Authorize(Policy="Play")] + public async Task DirectMovie(string movieSlug) + { + 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(); + } + [HttpGet("transmux/{showSlug}-s{seasonNumber}e{episodeNumber}")] [Authorize(Policy="Play")] - public async Task Transmux(string showSlug, int seasonNumber, int episodeNumber) + public async Task TransmuxEpisode(string showSlug, int seasonNumber, int episodeNumber) { + if (seasonNumber < 0 || episodeNumber < 0) + 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 NotFound(); + string path = await _transcoder.Transmux(episode); + if (path == null) + return StatusCode(500); + return PhysicalFile(path, "application/x-mpegURL ", true); + } + + [HttpGet("transmux/{movieSlug}")] + [Authorize(Policy="Play")] + public async Task TransmuxMovie(string movieSlug) + { + Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); if (episode == null || !System.IO.File.Exists(episode.Path)) return NotFound(); string path = await _transcoder.Transmux(episode); - if (path != null) - return PhysicalFile(path, "application/x-mpegURL ", true); - return StatusCode(500); - } - - [HttpGet("transmux/{episodeLink}/segment/{chunk}")] - public IActionResult GetTransmuxedChunk(string episodeLink, string chunk) - { - string path = Path.GetFullPath(Path.Combine(_transmuxPath, episodeLink)); - path = Path.Combine(path, "segments" + Path.DirectorySeparatorChar + chunk); - - return PhysicalFile(path, "video/MP2T"); + if (path == null) + return StatusCode(500); + return PhysicalFile(path, "application/x-mpegURL ", true); } [HttpGet("transcode/{showSlug}-s{seasonNumber}e{episodeNumber}")] [Authorize(Policy="Play")] - public async Task Transcode(string showSlug, int seasonNumber, int episodeNumber) + public async Task TranscodeEpisode(string showSlug, int seasonNumber, int episodeNumber) { + if (seasonNumber < 0 || episodeNumber < 0) + 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 NotFound(); + string path = await _transcoder.Transcode(episode); + if (path == null) + return StatusCode(500); + return PhysicalFile(path, "application/x-mpegURL ", true); + } + + [HttpGet("transcode/{movieSlug}")] + [Authorize(Policy="Play")] + public async Task TranscodeMovie(string movieSlug) + { + Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); if (episode == null || !System.IO.File.Exists(episode.Path)) return NotFound(); string path = await _transcoder.Transcode(episode); - if (path != null) - return PhysicalFile(path, "application/x-mpegURL ", true); - return StatusCode(500); + if (path == null) + return StatusCode(500); + return PhysicalFile(path, "application/x-mpegURL ", true); } - [HttpGet("transcode/{episodeLink}/segment/{chunk}")] - public IActionResult GetTranscodedChunk(string episodeLink, string chunk) + + [HttpGet("transmux/{episodeLink}/segment/{chunk}")] + [Authorize(Policy="Play")] + public IActionResult GetTransmuxedChunk(string episodeLink, string chunk) { - string path = Path.GetFullPath(Path.Combine(_transcodePath, episodeLink)); - path = Path.Combine(path, "segments" + Path.DirectorySeparatorChar + chunk); - + string path = Path.GetFullPath(Path.Combine(_transmuxPath, episodeLink)); + path = Path.Combine(path, "segments", chunk); return PhysicalFile(path, "video/MP2T"); } - - [HttpGet("{movieSlug}")] + [HttpGet("transcode/{episodeLink}/segment/{chunk}")] [Authorize(Policy="Play")] - public async Task Index(string movieSlug) + public IActionResult GetTranscodedChunk(string episodeLink, string chunk) { - Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - - if (episode != null && System.IO.File.Exists(episode.Path)) - return PhysicalFile(episode.Path, "video/webm", true); - return NotFound(); - } - - [HttpGet("transmux/{movieSlug}")] - [Authorize(Policy="Play")] - public async Task Transmux(string movieSlug) - { - Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - - if (episode == null || !System.IO.File.Exists(episode.Path)) - return NotFound(); - string path = await _transcoder.Transmux(episode); - if (path != null) - return PhysicalFile(path, "application/x-mpegURL ", true); - return StatusCode(500); - } - - [HttpGet("transcode/{movieSlug}")] - [Authorize(Policy="Play")] - public async Task Transcode(string movieSlug) - { - Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - - if (episode == null || !System.IO.File.Exists(episode.Path)) - return NotFound(); - string path = await _transcoder.Transcode(episode); - if (path != null) - return PhysicalFile(path, "application/x-mpegURL ", true); - return StatusCode(500); + string path = Path.GetFullPath(Path.Combine(_transcodePath, episodeLink)); + path = Path.Combine(path, "segments", chunk); + return PhysicalFile(path, "video/MP2T"); } } } \ No newline at end of file diff --git a/transcoder b/transcoder index 59da0095..d2d90e28 160000 --- a/transcoder +++ b/transcoder @@ -1 +1 @@ -Subproject commit 59da0095a85914eabde9900973331a25a16088b3 +Subproject commit d2d90e28cd697a20fdd2e1c28fdfc2038c7f1d7c