Cleaning up the video & the subtitle api

This commit is contained in:
Zoe Roux 2020-10-26 06:22:03 +01:00
parent c07494c9e8
commit 2dd0806517
9 changed files with 140 additions and 128 deletions

View File

@ -32,6 +32,7 @@ namespace Kyoo.Controllers
Task<Episode> GetEpisode(int id); Task<Episode> GetEpisode(int id);
Task<Episode> GetEpisode(int showID, int seasonNumber, int episodeNumber); Task<Episode> GetEpisode(int showID, int seasonNumber, int episodeNumber);
Task<Genre> GetGenre(int id); Task<Genre> GetGenre(int id);
Task<Track> GetTrack(int id);
Task<Studio> GetStudio(int id); Task<Studio> GetStudio(int id);
Task<People> GetPeople(int id); Task<People> GetPeople(int id);
@ -42,7 +43,7 @@ namespace Kyoo.Controllers
Task<Season> GetSeason(string showSlug, int seasonNumber); Task<Season> GetSeason(string showSlug, int seasonNumber);
Task<Episode> GetEpisode(string showSlug, int seasonNumber, int episodeNumber); Task<Episode> GetEpisode(string showSlug, int seasonNumber, int episodeNumber);
Task<Episode> GetMovieEpisode(string movieSlug); Task<Episode> GetMovieEpisode(string movieSlug);
Task<Track> GetTrack(int id); Task<Track> GetTrack(string slug);
Task<Genre> GetGenre(string slug); Task<Genre> GetGenre(string slug);
Task<Studio> GetStudio(string slug); Task<Studio> GetStudio(string slug);
Task<People> GetPeople(string slug); Task<People> GetPeople(string slug);

View File

@ -110,6 +110,11 @@ namespace Kyoo.Controllers
return EpisodeRepository.Get(showID, seasonNumber, episodeNumber); return EpisodeRepository.Get(showID, seasonNumber, episodeNumber);
} }
public Task<Track> GetTrack(string slug)
{
return TrackRepository.Get(slug);
}
public Task<Genre> GetGenre(int id) public Task<Genre> GetGenre(int id)
{ {
return GenreRepository.Get(id); return GenreRepository.Get(id);

View File

@ -93,7 +93,9 @@ namespace Kyoo.Models
public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber) 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) public void OnMerge(object merged)

View File

@ -39,18 +39,21 @@ namespace Kyoo.Controllers
public override Task<Track> Get(string slug) public override Task<Track> Get(string slug)
{ {
Match match = Regex.Match(slug, Match match = Regex.Match(slug,
@"(?<show>.*)-s(?<season>\d*)-e(?<episode>\d*).(?<language>.{0,3})(?<forced>-forced)?(\..*)?"); @"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
if (!match.Success) if (!match.Success)
{ {
if (int.TryParse(slug, out int id)) if (int.TryParse(slug, out int id))
return Get(id); return Get(id);
throw new ArgumentException("Invalid track slug. Format: {episodeSlug}.{language}[-forced][.{extension}]"); match = Regex.Match(slug, @"(?<show>.*)\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
if (!match.Success)
throw new ArgumentException("Invalid track slug. " +
"Format: {episodeSlug}.{language}[-forced][.{extension}]");
} }
string showSlug = match.Groups["show"].Value; string showSlug = match.Groups["show"].Value;
int seasonNumber = int.Parse(match.Groups["season"].Value); int seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : -1;
int episodeNumber = int.Parse(match.Groups["episode"].Value); int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1;
string language = match.Groups["language"].Value; string language = match.Groups["language"].Value;
bool forced = match.Groups["forced"].Success; bool forced = match.Groups["forced"].Success;
return _database.Tracks.FirstOrDefaultAsync(x => x.Episode.Show.Slug == showSlug return _database.Tracks.FirstOrDefaultAsync(x => x.Episode.Show.Slug == showSlug
@ -59,6 +62,7 @@ namespace Kyoo.Controllers
&& x.Language == language && x.Language == language
&& x.IsForced == forced); && x.IsForced == forced);
} }
public Task<ICollection<Track>> Search(string query) public Task<ICollection<Track>> Search(string query)
{ {
throw new InvalidOperationException("Tracks do not support the search method."); throw new InvalidOperationException("Tracks do not support the search method.");

View File

@ -79,7 +79,7 @@ namespace Kyoo.Controllers
public Task<string> Transcode(Episode episode) public Task<string> Transcode(Episode episode)
{ {
return null; // Not implemented yet. return Task.FromResult<string>(null); // Not implemented yet.
} }
} }
} }

View File

@ -32,7 +32,6 @@ namespace Kyoo
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
} }
// This method gets called by the runtime. Use this method to add services to the container. // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {

View File

@ -1,53 +1,38 @@
using Kyoo.Models; using System;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Watch;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
namespace Kyoo.Api namespace Kyoo.Api
{ {
[Route("[controller]")] [Route("subtitle")]
[ApiController] [ApiController]
public class SubtitleController : ControllerBase public class SubtitleApi : ControllerBase
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
//private readonly ITranscoder _transcoder;
public SubtitleController(ILibraryManager libraryManager/*, ITranscoder transcoder*/) public SubtitleApi(ILibraryManager libraryManager)
{ {
_libraryManager = 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")] [Authorize(Policy="Play")]
public async Task<IActionResult> GetSubtitle(string showSlug, public async Task<IActionResult> GetSubtitle(string slug, string extension)
int seasonNumber,
int episodeNumber,
string identifier,
string extension)
{ {
string languageTag = identifier.Length >= 3 ? identifier.Substring(0, 3) : null; Track subtitle;
bool forced = identifier.Length > 4 && identifier.Substring(4) == "forced"; try
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)
{ {
string idString = identifier.IndexOf('-') != -1 subtitle = await _libraryManager.GetTrack(slug);
? identifier.Substring(0, identifier.IndexOf('-')) }
: identifier; catch (ArgumentException ex)
int.TryParse(idString, out int id); {
subtitle = await _libraryManager.GetTrack(id); return BadRequest(new {error = ex.Message});
} }
if (subtitle == null) if (subtitle == null)
@ -55,14 +40,7 @@ namespace Kyoo.Api
if (subtitle.Codec == "subrip" && extension == "vtt") if (subtitle.Codec == "subrip" && extension == "vtt")
return new ConvertSubripToVtt(subtitle.Path); return new ConvertSubripToVtt(subtitle.Path);
string mime = subtitle.Codec == "ass" ? "text/x-ssa" : "application/x-subrip";
string mime;
if (subtitle.Codec == "ass")
mime = "text/x-ssa";
else
mime = "application/x-subrip";
// TODO Should use appropriate mime type here
return PhysicalFile(subtitle.Path, mime); return PhysicalFile(subtitle.Path, mime);
} }
@ -129,8 +107,7 @@ namespace Kyoo.Api
await writer.WriteLineAsync(""); await writer.WriteLineAsync("");
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 == "")
@ -145,7 +122,6 @@ namespace Kyoo.Api
lines.Add(line); lines.Add(line);
} }
} }
}
await context.HttpContext.Response.Body.FlushAsync(); await context.HttpContext.Response.Body.FlushAsync();
} }

View File

@ -5,19 +5,21 @@ using Microsoft.Extensions.Configuration;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.StaticFiles;
namespace Kyoo.Api namespace Kyoo.Api
{ {
[Route("[controller]")] [Route("video")]
[ApiController] [ApiController]
public class VideoController : ControllerBase public class VideoApi : ControllerBase
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ITranscoder _transcoder; private readonly ITranscoder _transcoder;
private readonly string _transmuxPath; private readonly string _transmuxPath;
private readonly string _transcodePath; 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; _libraryManager = libraryManager;
_transcoder = transcoder; _transcoder = transcoder;
@ -25,101 +27,124 @@ namespace Kyoo.Api
_transcodePath = config.GetValue<string>("transcodeTempPath"); _transcodePath = config.GetValue<string>("transcodeTempPath");
} }
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}e{episodeNumber}")] [HttpGet("{showSlug}-s{seasonNumber}e{episodeNumber}")]
[HttpGet("direct/{showSlug}-s{seasonNumber}e{episodeNumber}")]
[Authorize(Policy="Play")] [Authorize(Policy="Play")]
public async Task<IActionResult> Index(string showSlug, int seasonNumber, int episodeNumber) public async Task<IActionResult> DirectEpisode(string showSlug, int seasonNumber, int episodeNumber)
{ {
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, 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)) 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(); return NotFound();
} }
[HttpGet("transmux/{showSlug}-s{seasonNumber}e{episodeNumber}")]
[Authorize(Policy="Play")]
public async Task<IActionResult> Transmux(string showSlug, int seasonNumber, int episodeNumber)
{
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 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");
}
[HttpGet("transcode/{showSlug}-s{seasonNumber}e{episodeNumber}")]
[Authorize(Policy="Play")]
public async Task<IActionResult> Transcode(string showSlug, int seasonNumber, int episodeNumber)
{
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 PhysicalFile(path, "application/x-mpegURL ", true);
return StatusCode(500);
}
[HttpGet("transcode/{episodeLink}/segment/{chunk}")]
public IActionResult GetTranscodedChunk(string episodeLink, string chunk)
{
string path = Path.GetFullPath(Path.Combine(_transcodePath, episodeLink));
path = Path.Combine(path, "segments" + Path.DirectorySeparatorChar + chunk);
return PhysicalFile(path, "video/MP2T");
}
[HttpGet("{movieSlug}")] [HttpGet("{movieSlug}")]
[HttpGet("direct/{movieSlug}")]
[Authorize(Policy="Play")] [Authorize(Policy="Play")]
public async Task<IActionResult> Index(string movieSlug) public async Task<IActionResult> DirectMovie(string movieSlug)
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode != null && System.IO.File.Exists(episode.Path)) if (episode != null && System.IO.File.Exists(episode.Path))
return PhysicalFile(episode.Path, "video/webm", true); return PhysicalFile(episode.Path, _GetContentType(episode.Path), true);
return NotFound(); return NotFound();
} }
[HttpGet("transmux/{showSlug}-s{seasonNumber}e{episodeNumber}")]
[Authorize(Policy="Play")]
public async Task<IActionResult> 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}")] [HttpGet("transmux/{movieSlug}")]
[Authorize(Policy="Play")] [Authorize(Policy="Play")]
public async Task<IActionResult> Transmux(string movieSlug) public async Task<IActionResult> TransmuxMovie(string movieSlug)
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null || !System.IO.File.Exists(episode.Path)) if (episode == null || !System.IO.File.Exists(episode.Path))
return NotFound(); return NotFound();
string path = await _transcoder.Transmux(episode); string path = await _transcoder.Transmux(episode);
if (path != null) if (path == null)
return PhysicalFile(path, "application/x-mpegURL ", true);
return StatusCode(500); return StatusCode(500);
return PhysicalFile(path, "application/x-mpegURL ", true);
}
[HttpGet("transcode/{showSlug}-s{seasonNumber}e{episodeNumber}")]
[Authorize(Policy="Play")]
public async Task<IActionResult> 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}")] [HttpGet("transcode/{movieSlug}")]
[Authorize(Policy="Play")] [Authorize(Policy="Play")]
public async Task<IActionResult> Transcode(string movieSlug) public async Task<IActionResult> TranscodeMovie(string movieSlug)
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null || !System.IO.File.Exists(episode.Path)) if (episode == null || !System.IO.File.Exists(episode.Path))
return NotFound(); return NotFound();
string path = await _transcoder.Transcode(episode); string path = await _transcoder.Transcode(episode);
if (path != null) if (path == null)
return PhysicalFile(path, "application/x-mpegURL ", true);
return StatusCode(500); return StatusCode(500);
return PhysicalFile(path, "application/x-mpegURL ", true);
}
[HttpGet("transmux/{episodeLink}/segment/{chunk}")]
[Authorize(Policy="Play")]
public IActionResult GetTransmuxedChunk(string episodeLink, string chunk)
{
string path = Path.GetFullPath(Path.Combine(_transmuxPath, episodeLink));
path = Path.Combine(path, "segments", chunk);
return PhysicalFile(path, "video/MP2T");
}
[HttpGet("transcode/{episodeLink}/segment/{chunk}")]
[Authorize(Policy="Play")]
public IActionResult GetTranscodedChunk(string episodeLink, string chunk)
{
string path = Path.GetFullPath(Path.Combine(_transcodePath, episodeLink));
path = Path.Combine(path, "segments", chunk);
return PhysicalFile(path, "video/MP2T");
} }
} }
} }

@ -1 +1 @@
Subproject commit 59da0095a85914eabde9900973331a25a16088b3 Subproject commit d2d90e28cd697a20fdd2e1c28fdfc2038c7f1d7c