From ff5ecb474fcaf3876fb1facdadb3fd90a4bd7326 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 18 Feb 2024 17:31:25 +0100 Subject: [PATCH] Make transcoding logic similar for episodes and movies --- back/src/Kyoo.Core/Controllers/Transcoder.cs | 52 -------- back/src/Kyoo.Core/Views/Helper/Transcoder.cs | 126 ++++++++++++++++++ .../Kyoo.Core/Views/Resources/EpisodeApi.cs | 99 +------------- .../src/Kyoo.Core/Views/Resources/MovieApi.cs | 47 +++---- 4 files changed, 151 insertions(+), 173 deletions(-) delete mode 100644 back/src/Kyoo.Core/Controllers/Transcoder.cs create mode 100644 back/src/Kyoo.Core/Views/Helper/Transcoder.cs diff --git a/back/src/Kyoo.Core/Controllers/Transcoder.cs b/back/src/Kyoo.Core/Controllers/Transcoder.cs deleted file mode 100644 index dbb5d5ee..00000000 --- a/back/src/Kyoo.Core/Controllers/Transcoder.cs +++ /dev/null @@ -1,52 +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 . - -using System.Threading.Tasks; -using AspNetCore.Proxy; -using AspNetCore.Proxy.Options; -using Kyoo.Abstractions.Models.Utils; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Core.Controllers; - -public class Transcoder : Controller -{ - public Task Proxy(string route, string path) - { - HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder - .Instance.WithBeforeSend( - (ctx, req) => - { - req.Headers.Add("X-Path", path); - return Task.CompletedTask; - } - ) - .WithHandleFailure( - async (context, exception) => - { - context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - await context.Response.WriteAsJsonAsync( - new RequestError("Service unavailable") - ); - } - ) - .Build(); - return this.HttpProxyAsync($"http://transcoder:7666/{route}", proxyOptions); - } -} diff --git a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs new file mode 100644 index 00000000..776dba98 --- /dev/null +++ b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs @@ -0,0 +1,126 @@ +// 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 . + +using System.Threading.Tasks; +using AspNetCore.Proxy; +using AspNetCore.Proxy.Options; +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; + +namespace Kyoo.Core.Api; + +public abstract class TranscoderApi(IRepository repository, IThumbnailsManager thumbs) + : CrudThumbsApi(repository, thumbs) + where T : class, IResource, IThumbnails, IQuery +{ + private Task _Proxy(string route, string path) + { + HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder + .Instance.WithBeforeSend( + (ctx, req) => + { + req.Headers.Add("X-Path", path); + return Task.CompletedTask; + } + ) + .WithHandleFailure( + async (context, exception) => + { + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + await context.Response.WriteAsJsonAsync( + new RequestError("Service unavailable") + ); + } + ) + .Build(); + return this.HttpProxyAsync($"http://transcoder:7666/{route}", proxyOptions); + } + + protected abstract Task GetPath(Identifier identifier); + + /// + /// Direct stream + /// + /// + /// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or + /// transmuxing is done. + /// + /// The ID or slug of the . + /// The video file of this episode. + /// No episode with the given ID or slug could be found. + [HttpGet("{identifier:id}/direct")] + [PartialPermission(Kind.Play)] + [ProducesResponseType(StatusCodes.Status206PartialContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetDirectStream(Identifier identifier) + { + await _Proxy("/direct", await GetPath(identifier)); + } + + /// + /// Get master playlist + /// + /// + /// Get a master playlist containing all possible video qualities and audios available for this resource. + /// Note that the direct stream is missing (since the direct is not an hls stream) and + /// subtitles/fonts are not included to support more codecs than just webvtt. + /// + /// The ID or slug of the . + /// The master playlist of this episode. + /// No episode with the given ID or slug could be found. + [HttpGet("{identifier:id}/master.m3u8")] + [PartialPermission(Kind.Play)] + [ProducesResponseType(StatusCodes.Status206PartialContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetMaster(Identifier identifier) + { + await _Proxy("/master.m3u8", await GetPath(identifier)); + } + + [HttpGet("{identifier:id}/{quality}/index.m3u8")] + [PartialPermission(Kind.Play)] + public async Task GetVideoIndex(Identifier identifier, string quality) + { + await _Proxy($"/{quality}/index.m3u8", await GetPath(identifier)); + } + + [HttpGet("{identifier:id}/audio/{audio}/index.m3u8")] + [PartialPermission(Kind.Play)] + public async Task GetAudioIndex(Identifier identifier, string audio) + { + await _Proxy($"/audio/{audio}/index.m3u8", await GetPath(identifier)); + } + + [HttpGet("{identifier:id}/audio/{audio}/{segment}")] + [PartialPermission(Kind.Play)] + public async Task GetAudioSegment(Identifier identifier, string audio, string segment) + { + await _Proxy($"/audio/{audio}/{segment}", await GetPath(identifier)); + } + + [HttpGet("{identifier:id}/info")] + [PartialPermission(Kind.Play)] + public async Task GetInfo(Identifier identifier) + { + await _Proxy("/info", await GetPath(identifier)); + } +} diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs index 8d095c5c..8f96f203 100644 --- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -24,7 +24,6 @@ using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Authentication; -using Kyoo.Core.Controllers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using static Kyoo.Abstractions.Models.Utils.Constants; @@ -39,11 +38,8 @@ namespace Kyoo.Core.Api [ApiController] [PartialPermission(nameof(Episode))] [ApiDefinition("Episodes", Group = ResourcesGroup)] - public class EpisodeApi( - ILibraryManager libraryManager, - IThumbnailsManager thumbnails, - Transcoder transcoder - ) : CrudThumbsApi(libraryManager.Episodes, thumbnails) + public class EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails) + : TranscoderApi(libraryManager.Episodes, thumbnails) { /// /// Get episode's show @@ -191,95 +187,12 @@ namespace Kyoo.Core.Api await libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow()); } - /// - /// Direct stream - /// - /// - /// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or - /// transmuxing is done. - /// - /// The ID or slug of the . - /// The video file of this episode. - /// No episode with the given ID or slug could be found. - [HttpGet("{identifier:id}/direct")] - [PartialPermission(Kind.Play)] - [ProducesResponseType(StatusCodes.Status206PartialContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetDirectStream(Identifier identifier) + protected override async Task GetPath(Identifier identifier) { - string path = await identifier.Match( - async id => (await libraryManager.Episodes.Get(id)).Path, - async slug => (await libraryManager.Episodes.Get(slug)).Path + return await identifier.Match( + async id => (await Repository.Get(id)).Path, + async slug => (await Repository.Get(slug)).Path ); - await transcoder.Proxy("/direct", path); - } - - /// - /// Get master playlist - /// - /// - /// Get a master playlist containing all possible video qualities and audios available for this resource. - /// Note that the direct stream is missing (since the direct is not an hls stream) and - /// subtitles/fonts are not included to support more codecs than just webvtt. - /// - /// The ID or slug of the . - /// The master playlist of this episode. - /// No episode with the given ID or slug could be found. - [HttpGet("{identifier:id}/master.m3u8")] - [PartialPermission(Kind.Play)] - [ProducesResponseType(StatusCodes.Status206PartialContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetMaster(Identifier identifier) - { - string path = await identifier.Match( - async id => (await libraryManager.Episodes.Get(id)).Path, - async slug => (await libraryManager.Episodes.Get(slug)).Path - ); - await transcoder.Proxy("/master.m3u8", path); - } - - [HttpGet("{identifier:id}/{quality}/index.m3u8")] - [PartialPermission(Kind.Play)] - public async Task GetVideoIndex(Identifier identifier, string quality) - { - string path = await identifier.Match( - async id => (await libraryManager.Episodes.Get(id)).Path, - async slug => (await libraryManager.Episodes.Get(slug)).Path - ); - await transcoder.Proxy($"/{quality}/index.m3u8", path); - } - - [HttpGet("{identifier:id}/audio/{audio}/index.m3u8")] - [PartialPermission(Kind.Play)] - public async Task GetAudioIndex(Identifier identifier, string audio) - { - string path = await identifier.Match( - async id => (await libraryManager.Episodes.Get(id)).Path, - async slug => (await libraryManager.Episodes.Get(slug)).Path - ); - await transcoder.Proxy($"/audio/{audio}/index.m3u8", path); - } - - [HttpGet("{identifier:id}/audio/{audio}/{segment}")] - [PartialPermission(Kind.Play)] - public async Task GetAudioSegment(Identifier identifier, string audio, string segment) - { - string path = await identifier.Match( - async id => (await libraryManager.Episodes.Get(id)).Path, - async slug => (await libraryManager.Episodes.Get(slug)).Path - ); - await transcoder.Proxy($"/audio/{audio}/{segment}", path); - } - - [HttpGet("{identifier:id}/info")] - [PartialPermission(Kind.Play)] - public async Task GetInfo(Identifier identifier) - { - string path = await identifier.Match( - async id => (await libraryManager.Episodes.Get(id)).Path, - async slug => (await libraryManager.Episodes.Get(slug)).Path - ); - await transcoder.Proxy("/info", path); } } } diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs index b5b2ffcb..5c358029 100644 --- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -40,26 +40,9 @@ namespace Kyoo.Core.Api [ApiController] [PartialPermission(nameof(Show))] [ApiDefinition("Shows", Group = ResourcesGroup)] - public class MovieApi : CrudThumbsApi + public class MovieApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) + : TranscoderApi(libraryManager.Movies, thumbs) { - /// - /// The library manager used to modify or retrieve information in the data store. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// Create a new . - /// - /// - /// The library manager used to modify or retrieve information about the data store. - /// - /// The thumbnail manager used to retrieve images paths. - public MovieApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) - : base(libraryManager.Movies, thumbs) - { - _libraryManager = libraryManager; - } - // /// // /// Get staff // /// @@ -113,7 +96,7 @@ namespace Kyoo.Core.Api [FromQuery] Include fields ) { - return await _libraryManager.Studios.Get( + return await libraryManager.Studios.Get( identifier.IsContainedIn(x => x.Movies!), fields ); @@ -147,7 +130,7 @@ namespace Kyoo.Core.Api [FromQuery] Include fields ) { - ICollection resources = await _libraryManager.Collections.GetAll( + ICollection resources = await libraryManager.Collections.GetAll( Filter.And(filter, identifier.IsContainedIn(x => x.Movies)), sortBy, fields, @@ -156,7 +139,7 @@ namespace Kyoo.Core.Api if ( !resources.Any() - && await _libraryManager.Movies.GetOrDefault(identifier.IsSame()) == null + && await libraryManager.Movies.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); @@ -181,9 +164,9 @@ namespace Kyoo.Core.Api { Guid id = await identifier.Match( id => Task.FromResult(id), - async slug => (await _libraryManager.Movies.Get(slug)).Id + async slug => (await libraryManager.Movies.Get(slug)).Id ); - return await _libraryManager.WatchStatus.GetMovieStatus(id, User.GetIdOrThrow()); + return await libraryManager.WatchStatus.GetMovieStatus(id, User.GetIdOrThrow()); } /// @@ -216,9 +199,9 @@ namespace Kyoo.Core.Api { Guid id = await identifier.Match( id => Task.FromResult(id), - async slug => (await _libraryManager.Movies.Get(slug)).Id + async slug => (await libraryManager.Movies.Get(slug)).Id ); - return await _libraryManager.WatchStatus.SetMovieStatus( + return await libraryManager.WatchStatus.SetMovieStatus( id, User.GetIdOrThrow(), status, @@ -245,9 +228,17 @@ namespace Kyoo.Core.Api { Guid id = await identifier.Match( id => Task.FromResult(id), - async slug => (await _libraryManager.Movies.Get(slug)).Id + async slug => (await libraryManager.Movies.Get(slug)).Id + ); + await libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow()); + } + + protected override Task GetPath(Identifier identifier) + { + return identifier.Match( + async id => (await Repository.Get(id)).Path, + async slug => (await Repository.Get(slug)).Path ); - await _libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow()); } } }