Start to remove transcoder dependence on kyoo

This commit is contained in:
Zoe Roux
2024-02-18 17:19:24 +01:00
parent 19485a110a
commit 0a0939fa3d
9 changed files with 223 additions and 147 deletions
@@ -44,9 +44,14 @@ namespace Kyoo.Abstractions.Models.Permissions
Create,
/// <summary>
/// Allow the user to delete this kind od data.
/// Allow the user to delete this kind of data.
/// </summary>
Delete
Delete,
/// <summary>
/// Allow the user to play this file.
/// </summary>
Play,
}
/// <summary>
@@ -271,8 +271,8 @@ namespace Kyoo.Abstractions.Models
public VideoLinks Links =>
new()
{
Direct = $"/video/episode/{Slug}/direct",
Hls = $"/video/episode/{Slug}/master.m3u8",
Direct = $"/episode/{Slug}/direct",
Hls = $"/episode/{Slug}/master.m3u8",
};
/// <summary>
@@ -68,8 +68,7 @@ public class WatchStatusRepository : IWatchStatusRepository
DatabaseContext db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
WatchStatusRepository repo =
scope.ServiceProvider.GetRequiredService<WatchStatusRepository>();
List<Guid> users = await db
.ShowWatchStatus.IgnoreQueryFilters()
List<Guid> users = await db.ShowWatchStatus.IgnoreQueryFilters()
.Where(x => x.ShowId == ep.ShowId && x.Status == WatchStatus.Completed)
.Select(x => x.UserId)
.ToListAsync();
@@ -0,0 +1,52 @@
// 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 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);
}
}
+1 -2
View File
@@ -110,8 +110,7 @@ namespace Kyoo.Core
options.SuppressMapClientErrors = true;
options.InvalidModelStateResponseFactory = ctx =>
{
string[] errors = ctx
.ModelState.SelectMany(x => x.Value!.Errors)
string[] errors = ctx.ModelState.SelectMany(x => x.Value!.Errors)
.Select(x => x.ErrorMessage)
.ToArray();
return new BadRequestObjectResult(new RequestError(errors));
+107 -29
View File
@@ -24,6 +24,7 @@ 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;
@@ -38,26 +39,12 @@ namespace Kyoo.Core.Api
[ApiController]
[PartialPermission(nameof(Episode))]
[ApiDefinition("Episodes", Group = ResourcesGroup)]
public class EpisodeApi : CrudThumbsApi<Episode>
public class EpisodeApi(
ILibraryManager libraryManager,
IThumbnailsManager thumbnails,
Transcoder transcoder
) : CrudThumbsApi<Episode>(libraryManager.Episodes, thumbnails)
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="EpisodeApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
public EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails)
: base(libraryManager.Episodes, thumbnails)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get episode's show
/// </summary>
@@ -77,7 +64,7 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Show> fields
)
{
return await _libraryManager.Shows.Get(
return await libraryManager.Shows.Get(
identifier.IsContainedIn<Show, Episode>(x => x.Episodes!),
fields
);
@@ -104,15 +91,15 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Season> fields
)
{
Season? ret = await _libraryManager.Seasons.GetOrDefault(
Season? ret = await libraryManager.Seasons.GetOrDefault(
identifier.IsContainedIn<Season, Episode>(x => x.Episodes!),
fields
);
if (ret != null)
return ret;
Episode? episode = await identifier.Match(
id => _libraryManager.Episodes.GetOrDefault(id),
slug => _libraryManager.Episodes.GetOrDefault(slug)
id => libraryManager.Episodes.GetOrDefault(id),
slug => libraryManager.Episodes.GetOrDefault(slug)
);
return episode == null ? NotFound() : NoContent();
}
@@ -136,9 +123,9 @@ namespace Kyoo.Core.Api
{
Guid id = await identifier.Match(
id => Task.FromResult(id),
async slug => (await _libraryManager.Episodes.Get(slug)).Id
async slug => (await libraryManager.Episodes.Get(slug)).Id
);
return await _libraryManager.WatchStatus.GetEpisodeStatus(id, User.GetIdOrThrow());
return await libraryManager.WatchStatus.GetEpisodeStatus(id, User.GetIdOrThrow());
}
/// <summary>
@@ -170,9 +157,9 @@ namespace Kyoo.Core.Api
{
Guid id = await identifier.Match(
id => Task.FromResult(id),
async slug => (await _libraryManager.Episodes.Get(slug)).Id
async slug => (await libraryManager.Episodes.Get(slug)).Id
);
return await _libraryManager.WatchStatus.SetEpisodeStatus(
return await libraryManager.WatchStatus.SetEpisodeStatus(
id,
User.GetIdOrThrow(),
status,
@@ -199,9 +186,100 @@ namespace Kyoo.Core.Api
{
Guid id = await identifier.Match(
id => Task.FromResult(id),
async slug => (await _libraryManager.Episodes.Get(slug)).Id
async slug => (await libraryManager.Episodes.Get(slug)).Id
);
await _libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow());
await libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow());
}
/// <summary>
/// Direct stream
/// </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 ID or slug of the <see cref="Episode"/>.</param>
/// <returns>The video file of this episode.</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/direct")]
[PartialPermission(Kind.Play)]
[ProducesResponseType(StatusCodes.Status206PartialContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetDirectStream(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("/direct", path);
}
/// <summary>
/// Get master playlist
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <returns>The master playlist of this episode.</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[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);
}
}
}
+24 -22
View File
@@ -16,14 +16,14 @@
// 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;
using System.Collections.Generic;
using System.Threading.Tasks;
using AspNetCore.Proxy;
using AspNetCore.Proxy.Options;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Controllers;
using Kyoo.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api
@@ -32,20 +32,9 @@ namespace Kyoo.Core.Api
/// Proxy to other services
/// </summary>
[ApiController]
public class ProxyApi : Controller
[Obsolete("Use /episode/id/master.m3u8 or routes like that")]
public class ProxyApi(ILibraryManager library, Transcoder transcoder) : Controller
{
private readonly HttpProxyOptions _proxyOptions = HttpProxyOptionsBuilder
.Instance.WithHandleFailure(
async (context, exception) =>
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(
new RequestError("Service unavailable")
);
}
)
.Build();
/// <summary>
/// Transcoder proxy
/// </summary>
@@ -54,15 +43,28 @@ namespace Kyoo.Core.Api
/// </remarks>
/// <param name="rest">The path of the transcoder.</param>
/// <returns>The return value of the transcoder.</returns>
[Route("video/{**rest}")]
[Route("video/{type}/{id:id}/{**rest}")]
[Permission("video", Kind.Read)]
public Task Proxy(string rest, [FromQuery] Dictionary<string, string> query)
[Obsolete("Use /episode/id/master.m3u8 or routes like that")]
public async Task Proxy(
string type,
Identifier id,
string rest,
[FromQuery] Dictionary<string, string> query
)
{
// TODO: Use an env var to configure transcoder:7666.
return this.HttpProxyAsync(
$"http://transcoder:7666/{rest}" + query.ToQueryString(),
_proxyOptions
string path = await (
type is "movie" or "movies"
? id.Match(
async id => (await library.Movies.Get(id)).Path,
async slug => (await library.Movies.Get(slug)).Path
)
: id.Match(
async id => (await library.Episodes.Get(id)).Path,
async slug => (await library.Episodes.Get(slug)).Path
)
);
await transcoder.Proxy(rest + query.ToQueryString(), path);
}
}
}