Move transcoder api to /video with path b64 encoded to prevent db hit

This commit is contained in:
Zoe Roux 2024-04-28 00:28:18 +02:00
parent f871f4e1bf
commit 59e1832c3e
No known key found for this signature in database
7 changed files with 191 additions and 154 deletions

View File

@ -0,0 +1,42 @@
// 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.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Kyoo.Core.Controllers;
public class Base64RouteConstraint : IRouteConstraint
{
static Regex Base64Reg = new("^[-A-Za-z0-9+/]*={0,3}$");
/// <inheritdoc />
public bool Match(
HttpContext? httpContext,
IRouter? route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
return values.TryGetValue(routeKey, out object? val)
&& val is string str
&& Base64Reg.IsMatch(str);
}
}

View File

@ -76,6 +76,7 @@ public static class ServiceExtensions
services.Configure<RouteOptions>(x =>
{
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
x.ConstraintMap.Add("base64", typeof(Base64RouteConstraint));
});
services.AddResponseCompression(x =>

View File

@ -1,95 +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 <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.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api;
/// <summary>
/// Proxy to other services
/// </summary>
[ApiController]
[Obsolete("Use /episode/id/master.m3u8 or routes like that")]
public class ProxyApi(ILibraryManager library) : Controller
{
private Task _Proxy(string route, (string path, string route) info)
{
HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder
.Instance.WithBeforeSend(
(ctx, req) =>
{
req.Headers.Add("X-Path", info.path);
req.Headers.Add("X-Route", info.route);
return Task.CompletedTask;
}
)
.WithHandleFailure(
async (context, exception) =>
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(
new RequestError("Service unavailable")
);
}
)
.Build();
return this.HttpProxyAsync($"{Transcoder.TranscoderUrl}{route}", proxyOptions);
}
/// <summary>
/// Transcoder proxy
/// </summary>
/// <remarks>
/// Simply proxy requests to the transcoder
/// </remarks>
/// <param name="rest">The path of the transcoder.</param>
/// <returns>The return value of the transcoder.</returns>
[Route("video/{type}/{id:id}/{**rest}")]
[Permission("video", Kind.Read)]
[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
)
{
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 _Proxy(rest + query.ToQueryString(), (path, $"{type}/{id}"));
}
}

View File

@ -0,0 +1,122 @@
// 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;
using System.Threading.Tasks;
using AspNetCore.Proxy;
using AspNetCore.Proxy.Options;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api;
/// <summary>
/// Private routes of the transcoder.
/// Url for these routes will be returned from /info or /master.m3u8 routes.
/// This should not be called manually
/// </summary>
[ApiController]
[Route("videos")]
[Route("video", Order = AlternativeRoute)]
[Permission("video", Kind.Read, Group = Group.Overall)]
[ApiDefinition("Video", Group = OtherGroup)]
public class VideoApi : Controller
{
public static string TranscoderUrl =
Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666";
private Task _Proxy(string route)
{
HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder
.Instance.WithHandleFailure(
async (context, exception) =>
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(
new RequestError("Service unavailable")
);
}
)
.Build();
return this.HttpProxyAsync($"{TranscoderUrl}/{route}", proxyOptions);
}
[HttpGet("{path:base64}/direct")]
[PartialPermission(Kind.Play)]
[ProducesResponseType(StatusCodes.Status206PartialContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetDirectStream(string path)
{
await _Proxy($"{path}/direct");
}
[HttpGet("{path:base64}/master.m3u8")]
[PartialPermission(Kind.Play)]
[ProducesResponseType(StatusCodes.Status206PartialContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetMaster(string path)
{
await _Proxy($"{path}/master.m3u8");
}
[HttpGet("{path:base64}/{quality}/index.m3u8")]
[PartialPermission(Kind.Play)]
public async Task GetVideoIndex(string path, string quality)
{
await _Proxy($"{path}/{quality}/index.m3u8");
}
[HttpGet("{path:base64}/{quality}/{segment}")]
[PartialPermission(Kind.Play)]
public async Task GetVideoSegment(string path, string quality, string segment)
{
await _Proxy($"{path}/{quality}/{segment}");
}
[HttpGet("{path:base64}/audio/{audio}/index.m3u8")]
[PartialPermission(Kind.Play)]
public async Task GetAudioIndex(string path, string audio)
{
await _Proxy($"{path}/audio/{audio}/index.m3u8");
}
[HttpGet("{path:base64}/audio/{audio}/{segment}")]
[PartialPermission(Kind.Play)]
public async Task GetAudioSegment(string path, string audio, string segment)
{
await _Proxy($"{path}/audio/{audio}/{segment}");
}
[HttpGet("{path:base64}/attachment/{name}")]
[PartialPermission(Kind.Play)]
public async Task GetAttachment(string path, string name)
{
await _Proxy($"{path}/attachment/{name}");
}
[HttpGet("{path:base64}/subtitle/{name}")]
[PartialPermission(Kind.Play)]
public async Task GetSubtitle(string path, string name)
{
await _Proxy($"{path}/subtitle/{name}");
}
}

View File

@ -17,6 +17,7 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Text;
using System.Threading.Tasks;
using AspNetCore.Proxy;
using AspNetCore.Proxy.Options;
@ -29,27 +30,13 @@ using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api;
public static class Transcoder
{
public static string TranscoderUrl =
Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666";
}
public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsApi<T>(repository)
where T : class, IResource, IThumbnails, IQuery
{
private Task _Proxy(string route, (string path, string route) info)
private Task _Proxy(string route)
{
HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder
.Instance.WithBeforeSend(
(ctx, req) =>
{
req.Headers.Add("X-Path", info.path);
req.Headers.Add("X-Route", info.route);
return Task.CompletedTask;
}
)
.WithHandleFailure(
.Instance.WithHandleFailure(
async (context, exception) =>
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
@ -59,10 +46,16 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
}
)
.Build();
return this.HttpProxyAsync($"{Transcoder.TranscoderUrl}{route}", proxyOptions);
return this.HttpProxyAsync($"{VideoApi.TranscoderUrl}/{route}", proxyOptions);
}
protected abstract Task<(string path, string route)> GetPath(Identifier identifier);
protected abstract Task<string> GetPath(Identifier identifier);
private async Task<string> _GetPath64(Identifier identifier)
{
string path = await GetPath(identifier);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(path));
}
/// <summary>
/// Direct stream
@ -76,11 +69,12 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
/// <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.Status302Found)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetDirectStream(Identifier identifier)
public async Task<IActionResult> GetDirectStream(Identifier identifier)
{
await _Proxy("/direct", await GetPath(identifier));
// TODO: Remove the /api and use a proxy rewrite instead.
return Redirect($"/api/video/{await _GetPath64(identifier)}/direct");
}
/// <summary>
@ -96,39 +90,12 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
/// <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.Status302Found)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetMaster(Identifier identifier)
public async Task<IActionResult> 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}/{quality}/{segment}")]
[PartialPermission(Kind.Play)]
public async Task GetVideoSegment(Identifier identifier, string quality, string segment)
{
await _Proxy($"/{quality}/{segment}", 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));
// TODO: Remove the /api and use a proxy rewrite instead.
return Redirect($"/api/video/{await _GetPath64(identifier)}/master.m3u8");
}
/// <summary>
@ -144,7 +111,7 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
[PartialPermission(Kind.Read)]
public async Task GetInfo(Identifier identifier)
{
await _Proxy("/info", await GetPath(identifier));
await _Proxy($"{await _GetPath64(identifier)}/info");
}
/// <summary>
@ -160,7 +127,7 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
[PartialPermission(Kind.Read)]
public async Task GetThumbnails(Identifier identifier)
{
await _Proxy("/thumbnails.png", await GetPath(identifier));
await _Proxy($"{await _GetPath64(identifier)}/thumbnails.png");
}
/// <summary>
@ -177,6 +144,6 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
[PartialPermission(Kind.Read)]
public async Task GetThumbnailsVtt(Identifier identifier)
{
await _Proxy("/thumbnails.vtt", await GetPath(identifier));
await _Proxy($"{await _GetPath64(identifier)}/thumbnails.vtt");
}
}

View File

@ -210,12 +210,12 @@ public class EpisodeApi(ILibraryManager libraryManager)
await libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow());
}
protected override async Task<(string path, string route)> GetPath(Identifier identifier)
protected override async Task<string> GetPath(Identifier identifier)
{
string path = await identifier.Match(
async id => (await Repository.Get(id)).Path,
async slug => (await Repository.Get(slug)).Path
);
return (path, $"/episodes/{identifier}");
return path;
}
}

View File

@ -221,12 +221,12 @@ public class MovieApi(ILibraryManager libraryManager) : TranscoderApi<Movie>(lib
await libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow());
}
protected override async Task<(string path, string route)> GetPath(Identifier identifier)
protected override async Task<string> GetPath(Identifier identifier)
{
string path = await identifier.Match(
async id => (await Repository.Get(id)).Path,
async slug => (await Repository.Get(slug)).Path
);
return (path, $"/movies/{identifier}");
return path;
}
}