mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-07 10:14:13 -04:00
Move transcoder api to /video with path b64 encoded to prevent db hit (#441)
This commit is contained in:
commit
edf482de03
42
back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs
Normal file
42
back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -76,6 +76,7 @@ public static class ServiceExtensions
|
|||||||
services.Configure<RouteOptions>(x =>
|
services.Configure<RouteOptions>(x =>
|
||||||
{
|
{
|
||||||
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
|
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
|
||||||
|
x.ConstraintMap.Add("base64", typeof(Base64RouteConstraint));
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddResponseCompression(x =>
|
services.AddResponseCompression(x =>
|
||||||
|
@ -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}"));
|
|
||||||
}
|
|
||||||
}
|
|
136
back/src/Kyoo.Core/Views/Content/VideoApi.cs
Normal file
136
back/src/Kyoo.Core/Views/Content/VideoApi.cs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// 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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{path:base64}/thumbnails.png")]
|
||||||
|
[PartialPermission(Kind.Read)]
|
||||||
|
public async Task GetThumbnails(string path)
|
||||||
|
{
|
||||||
|
await _Proxy($"{path}/thumbnails.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{path:base64}/thumbnails.vtt")]
|
||||||
|
[PartialPermission(Kind.Read)]
|
||||||
|
public async Task GetThumbnailsVtt(string path)
|
||||||
|
{
|
||||||
|
await _Proxy($"{path}/thumbnails.vtt");
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AspNetCore.Proxy;
|
using AspNetCore.Proxy;
|
||||||
using AspNetCore.Proxy.Options;
|
using AspNetCore.Proxy.Options;
|
||||||
@ -29,27 +30,13 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace Kyoo.Core.Api;
|
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)
|
public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsApi<T>(repository)
|
||||||
where T : class, IResource, IThumbnails, IQuery
|
where T : class, IResource, IThumbnails, IQuery
|
||||||
{
|
{
|
||||||
private Task _Proxy(string route, (string path, string route) info)
|
private Task _Proxy(string route)
|
||||||
{
|
{
|
||||||
HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder
|
HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder
|
||||||
.Instance.WithBeforeSend(
|
.Instance.WithHandleFailure(
|
||||||
(ctx, req) =>
|
|
||||||
{
|
|
||||||
req.Headers.Add("X-Path", info.path);
|
|
||||||
req.Headers.Add("X-Route", info.route);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.WithHandleFailure(
|
|
||||||
async (context, exception) =>
|
async (context, exception) =>
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||||
@ -59,10 +46,16 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.Build();
|
.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>
|
/// <summary>
|
||||||
/// Direct stream
|
/// 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>
|
/// <response code="404">No episode with the given ID or slug could be found.</response>
|
||||||
[HttpGet("{identifier:id}/direct")]
|
[HttpGet("{identifier:id}/direct")]
|
||||||
[PartialPermission(Kind.Play)]
|
[PartialPermission(Kind.Play)]
|
||||||
[ProducesResponseType(StatusCodes.Status206PartialContent)]
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[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>
|
/// <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>
|
/// <response code="404">No episode with the given ID or slug could be found.</response>
|
||||||
[HttpGet("{identifier:id}/master.m3u8")]
|
[HttpGet("{identifier:id}/master.m3u8")]
|
||||||
[PartialPermission(Kind.Play)]
|
[PartialPermission(Kind.Play)]
|
||||||
[ProducesResponseType(StatusCodes.Status206PartialContent)]
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task GetMaster(Identifier identifier)
|
public async Task<IActionResult> GetMaster(Identifier identifier)
|
||||||
{
|
{
|
||||||
await _Proxy("/master.m3u8", await GetPath(identifier));
|
// TODO: Remove the /api and use a proxy rewrite instead.
|
||||||
}
|
return Redirect($"/api/video/{await _GetPath64(identifier)}/master.m3u8");
|
||||||
|
|
||||||
[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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -144,7 +111,7 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
|
|||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
public async Task GetInfo(Identifier identifier)
|
public async Task GetInfo(Identifier identifier)
|
||||||
{
|
{
|
||||||
await _Proxy("/info", await GetPath(identifier));
|
await _Proxy($"{await _GetPath64(identifier)}/info");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -160,7 +127,7 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
|
|||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
public async Task GetThumbnails(Identifier identifier)
|
public async Task GetThumbnails(Identifier identifier)
|
||||||
{
|
{
|
||||||
await _Proxy("/thumbnails.png", await GetPath(identifier));
|
await _Proxy($"{await _GetPath64(identifier)}/thumbnails.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -177,6 +144,6 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp
|
|||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
public async Task GetThumbnailsVtt(Identifier identifier)
|
public async Task GetThumbnailsVtt(Identifier identifier)
|
||||||
{
|
{
|
||||||
await _Proxy("/thumbnails.vtt", await GetPath(identifier));
|
await _Proxy($"{await _GetPath64(identifier)}/thumbnails.vtt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,12 +210,12 @@ public class EpisodeApi(ILibraryManager libraryManager)
|
|||||||
await libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow());
|
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(
|
string path = await identifier.Match(
|
||||||
async id => (await Repository.Get(id)).Path,
|
async id => (await Repository.Get(id)).Path,
|
||||||
async slug => (await Repository.Get(slug)).Path
|
async slug => (await Repository.Get(slug)).Path
|
||||||
);
|
);
|
||||||
return (path, $"/episodes/{identifier}");
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,12 +221,12 @@ public class MovieApi(ILibraryManager libraryManager) : TranscoderApi<Movie>(lib
|
|||||||
await libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow());
|
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(
|
string path = await identifier.Match(
|
||||||
async id => (await Repository.Get(id)).Path,
|
async id => (await Repository.Get(id)).Path,
|
||||||
async slug => (await Repository.Get(slug)).Path
|
async slug => (await Repository.Get(slug)).Path
|
||||||
);
|
);
|
||||||
return (path, $"/movies/{identifier}");
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ x-transcoder: &transcoder-base
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
volumes:
|
volumes:
|
||||||
- ${LIBRARY_ROOT}:/video:ro
|
- ${LIBRARY_ROOT}:/video:ro
|
||||||
- ${CACHE_ROOT}:/cache
|
- ${CACHE_ROOT}:/cache
|
||||||
@ -95,6 +97,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- capabilities: [gpu]
|
- capabilities: [gpu]
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=nvidia
|
- GOCODER_HWACCEL=nvidia
|
||||||
profiles: ['nvidia']
|
profiles: ['nvidia']
|
||||||
|
|
||||||
@ -103,6 +106,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=vaapi
|
- GOCODER_HWACCEL=vaapi
|
||||||
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
||||||
profiles: ['vaapi']
|
profiles: ['vaapi']
|
||||||
@ -112,6 +116,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=qsv
|
- GOCODER_HWACCEL=qsv
|
||||||
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
||||||
profiles: ['qsv']
|
profiles: ['qsv']
|
||||||
|
@ -12,6 +12,8 @@ x-transcoder: &transcoder-base
|
|||||||
cpus: 1
|
cpus: 1
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
volumes:
|
volumes:
|
||||||
- ./transcoder:/app
|
- ./transcoder:/app
|
||||||
- ${LIBRARY_ROOT}:/video:ro
|
- ${LIBRARY_ROOT}:/video:ro
|
||||||
@ -119,6 +121,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- capabilities: [gpu]
|
- capabilities: [gpu]
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=nvidia
|
- GOCODER_HWACCEL=nvidia
|
||||||
profiles: ['nvidia']
|
profiles: ['nvidia']
|
||||||
|
|
||||||
@ -127,6 +130,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=vaapi
|
- GOCODER_HWACCEL=vaapi
|
||||||
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
||||||
profiles: ['vaapi']
|
profiles: ['vaapi']
|
||||||
@ -136,6 +140,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=qsv
|
- GOCODER_HWACCEL=qsv
|
||||||
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
||||||
profiles: ['qsv']
|
profiles: ['qsv']
|
||||||
|
@ -7,6 +7,8 @@ x-transcoder: &transcoder-base
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
volumes:
|
volumes:
|
||||||
- ${LIBRARY_ROOT}:/video:ro
|
- ${LIBRARY_ROOT}:/video:ro
|
||||||
- ${CACHE_ROOT}:/cache
|
- ${CACHE_ROOT}:/cache
|
||||||
@ -94,6 +96,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- capabilities: [gpu]
|
- capabilities: [gpu]
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=nvidia
|
- GOCODER_HWACCEL=nvidia
|
||||||
profiles: ['nvidia']
|
profiles: ['nvidia']
|
||||||
|
|
||||||
@ -102,6 +105,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=vaapi
|
- GOCODER_HWACCEL=vaapi
|
||||||
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
||||||
profiles: ['vaapi']
|
profiles: ['vaapi']
|
||||||
@ -111,6 +115,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
environment:
|
environment:
|
||||||
|
- GOCODER_PREFIX=/video
|
||||||
- GOCODER_HWACCEL=qsv
|
- GOCODER_HWACCEL=qsv
|
||||||
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
|
||||||
profiles: ['qsv']
|
profiles: ['qsv']
|
||||||
|
@ -68,7 +68,7 @@ export const useScrubber = (url: string) => {
|
|||||||
ret[i] = {
|
ret[i] = {
|
||||||
from: parseTs(times[0]),
|
from: parseTs(times[0]),
|
||||||
to: parseTs(times[1]),
|
to: parseTs(times[1]),
|
||||||
url: imageFn("/video/" + url[0]),
|
url: imageFn(url[0]),
|
||||||
x: xywh[0],
|
x: xywh[0],
|
||||||
y: xywh[1],
|
y: xywh[1],
|
||||||
width: xywh[2],
|
width: xywh[2],
|
||||||
|
@ -359,7 +359,7 @@ export const AudiosMenu = ({
|
|||||||
{hls.audioTracks.map((x, i) => (
|
{hls.audioTracks.map((x, i) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={i.toString()}
|
key={i.toString()}
|
||||||
label={audios?.[i].displayName ?? x.name}
|
label={audios?.[i]?.displayName ?? x.name}
|
||||||
selected={hls!.audioTrack === i}
|
selected={hls!.audioTrack === i}
|
||||||
onSelect={() => setAudio(audios?.[i] ?? ({ index: i } as any))}
|
onSelect={() => setAudio(audios?.[i] ?? ({ index: i } as any))}
|
||||||
/>
|
/>
|
||||||
|
@ -16,9 +16,9 @@ import (
|
|||||||
// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or
|
// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or
|
||||||
// transmuxing is done.
|
// transmuxing is done.
|
||||||
//
|
//
|
||||||
// Path: /direct
|
// Path: /:path/direct
|
||||||
func DirectStream(c echo.Context) error {
|
func DirectStream(c echo.Context) error {
|
||||||
path, err := GetPath(c)
|
path, _, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -31,18 +31,18 @@ func DirectStream(c echo.Context) error {
|
|||||||
// Note that the direct stream is missing (since the direct is not an hls stream) and
|
// 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.
|
// subtitles/fonts are not included to support more codecs than just webvtt.
|
||||||
//
|
//
|
||||||
// Path: /master.m3u8
|
// Path: /:path/master.m3u8
|
||||||
func (h *Handler) GetMaster(c echo.Context) error {
|
func (h *Handler) GetMaster(c echo.Context) error {
|
||||||
client, err := GetClientId(c)
|
client, err := GetClientId(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := h.transcoder.GetMaster(path, client, GetRoute(c))
|
ret, err := h.transcoder.GetMaster(path, client, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ func (h *Handler) GetMaster(c echo.Context) error {
|
|||||||
// This route can take a few seconds to respond since it will way for at least one segment to be
|
// This route can take a few seconds to respond since it will way for at least one segment to be
|
||||||
// available.
|
// available.
|
||||||
//
|
//
|
||||||
// Path: /:quality/index.m3u8
|
// Path: /:path/:quality/index.m3u8
|
||||||
func (h *Handler) GetVideoIndex(c echo.Context) error {
|
func (h *Handler) GetVideoIndex(c echo.Context) error {
|
||||||
quality, err := src.QualityFromString(c.Param("quality"))
|
quality, err := src.QualityFromString(c.Param("quality"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -65,12 +65,12 @@ func (h *Handler) GetVideoIndex(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := h.transcoder.GetVideoIndex(path, quality, client, GetRoute(c))
|
ret, err := h.transcoder.GetVideoIndex(path, quality, client, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ func (h *Handler) GetVideoIndex(c echo.Context) error {
|
|||||||
// This route can take a few seconds to respond since it will way for at least one segment to be
|
// This route can take a few seconds to respond since it will way for at least one segment to be
|
||||||
// available.
|
// available.
|
||||||
//
|
//
|
||||||
// Path: /audio/:audio/index.m3u8
|
// Path: /:path/audio/:audio/index.m3u8
|
||||||
func (h *Handler) GetAudioIndex(c echo.Context) error {
|
func (h *Handler) GetAudioIndex(c echo.Context) error {
|
||||||
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
|
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -93,12 +93,12 @@ func (h *Handler) GetAudioIndex(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := h.transcoder.GetAudioIndex(path, int32(audio), client, GetRoute(c))
|
ret, err := h.transcoder.GetAudioIndex(path, int32(audio), client, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ func (h *Handler) GetAudioIndex(c echo.Context) error {
|
|||||||
//
|
//
|
||||||
// Retrieve a chunk of a transmuxed video.
|
// Retrieve a chunk of a transmuxed video.
|
||||||
//
|
//
|
||||||
// Path: /:quality/segments-:chunk.ts
|
// Path: /:path/:quality/segments-:chunk.ts
|
||||||
func (h *Handler) GetVideoSegment(c echo.Context) error {
|
func (h *Handler) GetVideoSegment(c echo.Context) error {
|
||||||
quality, err := src.QualityFromString(c.Param("quality"))
|
quality, err := src.QualityFromString(c.Param("quality"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -123,12 +123,12 @@ func (h *Handler) GetVideoSegment(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := h.transcoder.GetVideoSegment(path, quality, segment, client, GetRoute(c))
|
ret, err := h.transcoder.GetVideoSegment(path, quality, segment, client, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ func (h *Handler) GetVideoSegment(c echo.Context) error {
|
|||||||
//
|
//
|
||||||
// Retrieve a chunk of a transcoded audio.
|
// Retrieve a chunk of a transcoded audio.
|
||||||
//
|
//
|
||||||
// Path: /audio/:audio/segments-:chunk.ts
|
// Path: /:path/audio/:audio/segments-:chunk.ts
|
||||||
func (h *Handler) GetAudioSegment(c echo.Context) error {
|
func (h *Handler) GetAudioSegment(c echo.Context) error {
|
||||||
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
|
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -153,12 +153,12 @@ func (h *Handler) GetAudioSegment(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := h.transcoder.GetAudioSegment(path, int32(audio), segment, client, GetRoute(c))
|
ret, err := h.transcoder.GetAudioSegment(path, int32(audio), segment, client, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -169,29 +169,20 @@ func (h *Handler) GetAudioSegment(c echo.Context) error {
|
|||||||
//
|
//
|
||||||
// Identify metadata about a file.
|
// Identify metadata about a file.
|
||||||
//
|
//
|
||||||
// Path: /info
|
// Path: /:path/info
|
||||||
func (h *Handler) GetInfo(c echo.Context) error {
|
func (h *Handler) GetInfo(c echo.Context) error {
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
route := GetRoute(c)
|
ret, err := src.GetInfo(path, sha)
|
||||||
sha, err := src.GetHash(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ret, err := src.GetInfo(path, sha, route)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Run extractors to have them in cache
|
// Run extractors to have them in cache
|
||||||
src.Extract(ret.Path, sha, route)
|
src.Extract(ret.Path, sha)
|
||||||
go src.ExtractThumbnail(
|
go src.ExtractThumbnail(ret.Path, sha)
|
||||||
ret.Path,
|
|
||||||
route,
|
|
||||||
sha,
|
|
||||||
)
|
|
||||||
return c.JSON(http.StatusOK, ret)
|
return c.JSON(http.StatusOK, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,9 +190,9 @@ func (h *Handler) GetInfo(c echo.Context) error {
|
|||||||
//
|
//
|
||||||
// Get a specific attachment.
|
// Get a specific attachment.
|
||||||
//
|
//
|
||||||
// Path: /attachment/:name
|
// Path: /:path/attachment/:name
|
||||||
func (h *Handler) GetAttachment(c echo.Context) error {
|
func (h *Handler) GetAttachment(c echo.Context) error {
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -210,12 +201,7 @@ func (h *Handler) GetAttachment(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
route := GetRoute(c)
|
wait, err := src.Extract(path, sha)
|
||||||
sha, err := src.GetHash(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
wait, err := src.Extract(path, sha, route)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -229,9 +215,9 @@ func (h *Handler) GetAttachment(c echo.Context) error {
|
|||||||
//
|
//
|
||||||
// Get a specific subtitle.
|
// Get a specific subtitle.
|
||||||
//
|
//
|
||||||
// Path: /subtitle/:name
|
// Path: /:path/subtitle/:name
|
||||||
func (h *Handler) GetSubtitle(c echo.Context) error {
|
func (h *Handler) GetSubtitle(c echo.Context) error {
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -240,12 +226,7 @@ func (h *Handler) GetSubtitle(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
route := GetRoute(c)
|
wait, err := src.Extract(path, sha)
|
||||||
sha, err := src.GetHash(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
wait, err := src.Extract(path, sha, route)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -259,22 +240,14 @@ func (h *Handler) GetSubtitle(c echo.Context) error {
|
|||||||
//
|
//
|
||||||
// Get a sprite file containing all the thumbnails of the show.
|
// Get a sprite file containing all the thumbnails of the show.
|
||||||
//
|
//
|
||||||
// Path: /thumbnails.png
|
// Path: /:path/thumbnails.png
|
||||||
func (h *Handler) GetThumbnails(c echo.Context) error {
|
func (h *Handler) GetThumbnails(c echo.Context) error {
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sha, err := src.GetHash(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := src.ExtractThumbnail(
|
out, err := src.ExtractThumbnail(path, sha)
|
||||||
path,
|
|
||||||
GetRoute(c),
|
|
||||||
sha,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -287,22 +260,14 @@ func (h *Handler) GetThumbnails(c echo.Context) error {
|
|||||||
// Get a vtt file containing timing/position of thumbnails inside the sprite file.
|
// Get a vtt file containing timing/position of thumbnails inside the sprite file.
|
||||||
// https://developer.bitmovin.com/playback/docs/webvtt-based-thumbnails for more info.
|
// https://developer.bitmovin.com/playback/docs/webvtt-based-thumbnails for more info.
|
||||||
//
|
//
|
||||||
// Path: /:resource/:slug/thumbnails.vtt
|
// Path: /:path/:resource/:slug/thumbnails.vtt
|
||||||
func (h *Handler) GetThumbnailsVtt(c echo.Context) error {
|
func (h *Handler) GetThumbnailsVtt(c echo.Context) error {
|
||||||
path, err := GetPath(c)
|
path, sha, err := GetPath(c)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sha, err := src.GetHash(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := src.ExtractThumbnail(
|
out, err := src.ExtractThumbnail(path, sha)
|
||||||
path,
|
|
||||||
GetRoute(c),
|
|
||||||
sha,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -328,17 +293,17 @@ func main() {
|
|||||||
transcoder: transcoder,
|
transcoder: transcoder,
|
||||||
}
|
}
|
||||||
|
|
||||||
e.GET("/direct", DirectStream)
|
e.GET("/:path/direct", DirectStream)
|
||||||
e.GET("/master.m3u8", h.GetMaster)
|
e.GET("/:path/master.m3u8", h.GetMaster)
|
||||||
e.GET("/:quality/index.m3u8", h.GetVideoIndex)
|
e.GET("/:path/:quality/index.m3u8", h.GetVideoIndex)
|
||||||
e.GET("/audio/:audio/index.m3u8", h.GetAudioIndex)
|
e.GET("/:path/audio/:audio/index.m3u8", h.GetAudioIndex)
|
||||||
e.GET("/:quality/:chunk", h.GetVideoSegment)
|
e.GET("/:path/:quality/:chunk", h.GetVideoSegment)
|
||||||
e.GET("/audio/:audio/:chunk", h.GetAudioSegment)
|
e.GET("/:path/audio/:audio/:chunk", h.GetAudioSegment)
|
||||||
e.GET("/info", h.GetInfo)
|
e.GET("/:path/info", h.GetInfo)
|
||||||
e.GET("/thumbnails.png", h.GetThumbnails)
|
e.GET("/:path/thumbnails.png", h.GetThumbnails)
|
||||||
e.GET("/thumbnails.vtt", h.GetThumbnailsVtt)
|
e.GET("/:path/thumbnails.vtt", h.GetThumbnailsVtt)
|
||||||
e.GET("/attachment/:name", h.GetAttachment)
|
e.GET("/:path/attachment/:name", h.GetAttachment)
|
||||||
e.GET("/subtitle/:name", h.GetSubtitle)
|
e.GET("/:path/subtitle/:name", h.GetSubtitle)
|
||||||
|
|
||||||
e.Logger.Fatal(e.Start(":7666"))
|
e.Logger.Fatal(e.Start(":7666"))
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
var extracted = NewCMap[string, <-chan struct{}]()
|
var extracted = NewCMap[string, <-chan struct{}]()
|
||||||
|
|
||||||
func Extract(path string, sha string, route string) (<-chan struct{}, error) {
|
func Extract(path string, sha string) (<-chan struct{}, error) {
|
||||||
ret := make(chan struct{})
|
ret := make(chan struct{})
|
||||||
existing, created := extracted.GetOrSet(sha, ret)
|
existing, created := extracted.GetOrSet(sha, ret)
|
||||||
if !created {
|
if !created {
|
||||||
@ -18,7 +18,7 @@ func Extract(path string, sha string, route string) (<-chan struct{}, error) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer printExecTime("Starting extraction of %s", path)()
|
defer printExecTime("Starting extraction of %s", path)()
|
||||||
info, err := GetInfo(path, sha, route)
|
info, err := GetInfo(path, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
extracted.Remove(sha)
|
extracted.Remove(sha)
|
||||||
close(ret)
|
close(ret)
|
||||||
|
@ -19,7 +19,7 @@ type FileStream struct {
|
|||||||
audios CMap[int32, *AudioStream]
|
audios CMap[int32, *AudioStream]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileStream(path string, sha string, route string) *FileStream {
|
func NewFileStream(path string, sha string) *FileStream {
|
||||||
ret := &FileStream{
|
ret := &FileStream{
|
||||||
Path: path,
|
Path: path,
|
||||||
Out: fmt.Sprintf("%s/%s", Settings.Outpath, sha),
|
Out: fmt.Sprintf("%s/%s", Settings.Outpath, sha),
|
||||||
@ -30,7 +30,7 @@ func NewFileStream(path string, sha string, route string) *FileStream {
|
|||||||
ret.ready.Add(1)
|
ret.ready.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer ret.ready.Done()
|
defer ret.ready.Done()
|
||||||
info, err := GetInfo(path, sha, route)
|
info, err := GetInfo(path, sha)
|
||||||
ret.Info = info
|
ret.Info = info
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.err = err
|
ret.err = err
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package src
|
package src
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -180,7 +181,7 @@ type MICache struct {
|
|||||||
|
|
||||||
var infos = NewCMap[string, *MICache]()
|
var infos = NewCMap[string, *MICache]()
|
||||||
|
|
||||||
func GetInfo(path string, sha string, route string) (*MediaInfo, error) {
|
func GetInfo(path string, sha string) (*MediaInfo, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
ret, _ := infos.GetOrCreate(sha, func() *MICache {
|
ret, _ := infos.GetOrCreate(sha, func() *MICache {
|
||||||
@ -195,7 +196,7 @@ func GetInfo(path string, sha string, route string) (*MediaInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var val *MediaInfo
|
var val *MediaInfo
|
||||||
val, err = getInfo(path, route)
|
val, err = getInfo(path)
|
||||||
*mi.info = *val
|
*mi.info = *val
|
||||||
mi.info.Sha = sha
|
mi.info.Sha = sha
|
||||||
mi.ready.Done()
|
mi.ready.Done()
|
||||||
@ -231,7 +232,7 @@ func saveInfo[T any](save_path string, mi *T) error {
|
|||||||
return os.WriteFile(save_path, content, 0o644)
|
return os.WriteFile(save_path, content, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInfo(path string, route string) (*MediaInfo, error) {
|
func getInfo(path string) (*MediaInfo, error) {
|
||||||
defer printExecTime("mediainfo for %s", path)()
|
defer printExecTime("mediainfo for %s", path)()
|
||||||
|
|
||||||
mi, err := mediainfo.Open(path)
|
mi, err := mediainfo.Open(path)
|
||||||
@ -294,7 +295,7 @@ func getInfo(path string, route string) (*MediaInfo, error) {
|
|||||||
extension := OrNull(SubtitleExtensions[format])
|
extension := OrNull(SubtitleExtensions[format])
|
||||||
var link *string
|
var link *string
|
||||||
if extension != nil {
|
if extension != nil {
|
||||||
x := fmt.Sprintf("%s/subtitle/%d.%s", route, i, *extension)
|
x := fmt.Sprintf("%s/%s/subtitle/%d.%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), i, *extension)
|
||||||
link = &x
|
link = &x
|
||||||
}
|
}
|
||||||
return Subtitle{
|
return Subtitle{
|
||||||
@ -319,7 +320,7 @@ func getInfo(path string, route string) (*MediaInfo, error) {
|
|||||||
Fonts: Map(
|
Fonts: Map(
|
||||||
attachments,
|
attachments,
|
||||||
func(font string, _ int) string {
|
func(font string, _ int) string {
|
||||||
return fmt.Sprintf("%s/attachment/%s", route, font)
|
return fmt.Sprintf("%s/%s/attachment/%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), font)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
if len(ret.Videos) > 0 {
|
if len(ret.Videos) > 0 {
|
||||||
|
@ -13,6 +13,7 @@ func GetEnvOr(env string, def string) string {
|
|||||||
type SettingsT struct {
|
type SettingsT struct {
|
||||||
Outpath string
|
Outpath string
|
||||||
Metadata string
|
Metadata string
|
||||||
|
RoutePrefix string
|
||||||
HwAccel HwAccelT
|
HwAccel HwAccelT
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,5 +27,6 @@ type HwAccelT struct {
|
|||||||
var Settings = SettingsT{
|
var Settings = SettingsT{
|
||||||
Outpath: GetEnvOr("GOCODER_CACHE_ROOT", "/cache"),
|
Outpath: GetEnvOr("GOCODER_CACHE_ROOT", "/cache"),
|
||||||
Metadata: GetEnvOr("GOCODER_METADATA_ROOT", "/metadata"),
|
Metadata: GetEnvOr("GOCODER_METADATA_ROOT", "/metadata"),
|
||||||
|
RoutePrefix: GetEnvOr("GOCODER_PREFIX", ""),
|
||||||
HwAccel: DetectHardwareAccel(),
|
HwAccel: DetectHardwareAccel(),
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package src
|
package src
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
@ -27,14 +28,14 @@ type Thumbnail struct {
|
|||||||
|
|
||||||
var thumbnails = NewCMap[string, *Thumbnail]()
|
var thumbnails = NewCMap[string, *Thumbnail]()
|
||||||
|
|
||||||
func ExtractThumbnail(path string, route string, sha string) (string, error) {
|
func ExtractThumbnail(path string, sha string) (string, error) {
|
||||||
ret, _ := thumbnails.GetOrCreate(sha, func() *Thumbnail {
|
ret, _ := thumbnails.GetOrCreate(sha, func() *Thumbnail {
|
||||||
ret := &Thumbnail{
|
ret := &Thumbnail{
|
||||||
path: fmt.Sprintf("%s/%s", Settings.Metadata, sha),
|
path: fmt.Sprintf("%s/%s", Settings.Metadata, sha),
|
||||||
}
|
}
|
||||||
ret.ready.Add(1)
|
ret.ready.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
extractThumbnail(path, ret.path, fmt.Sprintf("%s/thumbnails.png", route))
|
extractThumbnail(path, ret.path)
|
||||||
ret.ready.Done()
|
ret.ready.Done()
|
||||||
}()
|
}()
|
||||||
return ret
|
return ret
|
||||||
@ -43,7 +44,7 @@ func ExtractThumbnail(path string, route string, sha string) (string, error) {
|
|||||||
return ret.path, nil
|
return ret.path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractThumbnail(path string, out string, name string) error {
|
func extractThumbnail(path string, out string) error {
|
||||||
defer printExecTime("extracting thumbnails for %s", path)()
|
defer printExecTime("extracting thumbnails for %s", path)()
|
||||||
os.MkdirAll(out, 0o755)
|
os.MkdirAll(out, 0o755)
|
||||||
sprite_path := fmt.Sprintf("%s/sprite.png", out)
|
sprite_path := fmt.Sprintf("%s/sprite.png", out)
|
||||||
@ -97,10 +98,11 @@ func extractThumbnail(path string, out string, name string) error {
|
|||||||
timestamps := ts
|
timestamps := ts
|
||||||
ts += interval
|
ts += interval
|
||||||
vtt += fmt.Sprintf(
|
vtt += fmt.Sprintf(
|
||||||
"%s --> %s\n%s#xywh=%d,%d,%d,%d\n\n",
|
"%s --> %s\n%s/%s/thumbnails.png#xywh=%d,%d,%d,%d\n\n",
|
||||||
tsToVttTime(timestamps),
|
tsToVttTime(timestamps),
|
||||||
tsToVttTime(ts),
|
tsToVttTime(ts),
|
||||||
name,
|
Settings.RoutePrefix,
|
||||||
|
base64.StdEncoding.EncodeToString([]byte(path)),
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
|
@ -33,14 +33,10 @@ func NewTranscoder() (*Transcoder, error) {
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transcoder) getFileStream(path string, route string) (*FileStream, error) {
|
func (t *Transcoder) getFileStream(path string, sha string) (*FileStream, error) {
|
||||||
var err error
|
var err error
|
||||||
ret, _ := t.streams.GetOrCreate(path, func() *FileStream {
|
ret, _ := t.streams.GetOrCreate(path, func() *FileStream {
|
||||||
sha, err := GetHash(path)
|
return NewFileStream(path, sha)
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return NewFileStream(path, sha, route)
|
|
||||||
})
|
})
|
||||||
ret.ready.Wait()
|
ret.ready.Wait()
|
||||||
if err != nil || ret.err != nil {
|
if err != nil || ret.err != nil {
|
||||||
@ -50,8 +46,8 @@ func (t *Transcoder) getFileStream(path string, route string) (*FileStream, erro
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transcoder) GetMaster(path string, client string, route string) (string, error) {
|
func (t *Transcoder) GetMaster(path string, client string, sha string) (string, error) {
|
||||||
stream, err := t.getFileStream(path, route)
|
stream, err := t.getFileStream(path, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -69,9 +65,9 @@ func (t *Transcoder) GetVideoIndex(
|
|||||||
path string,
|
path string,
|
||||||
quality Quality,
|
quality Quality,
|
||||||
client string,
|
client string,
|
||||||
route string,
|
sha string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
stream, err := t.getFileStream(path, route)
|
stream, err := t.getFileStream(path, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -89,9 +85,9 @@ func (t *Transcoder) GetAudioIndex(
|
|||||||
path string,
|
path string,
|
||||||
audio int32,
|
audio int32,
|
||||||
client string,
|
client string,
|
||||||
route string,
|
sha string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
stream, err := t.getFileStream(path, route)
|
stream, err := t.getFileStream(path, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -109,9 +105,9 @@ func (t *Transcoder) GetVideoSegment(
|
|||||||
quality Quality,
|
quality Quality,
|
||||||
segment int32,
|
segment int32,
|
||||||
client string,
|
client string,
|
||||||
route string,
|
sha string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
stream, err := t.getFileStream(path, route)
|
stream, err := t.getFileStream(path, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -130,9 +126,9 @@ func (t *Transcoder) GetAudioSegment(
|
|||||||
audio int32,
|
audio int32,
|
||||||
segment int32,
|
segment int32,
|
||||||
client string,
|
client string,
|
||||||
route string,
|
sha string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
stream, err := t.getFileStream(path, route)
|
stream, err := t.getFileStream(path, sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
package src
|
package src
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,15 +15,3 @@ func printExecTime(message string, args ...any) func() {
|
|||||||
log.Printf("%s finished in %s", msg, time.Since(start))
|
log.Printf("%s finished in %s", msg, time.Since(start))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHash(path string) (string, error) {
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
h := sha1.New()
|
|
||||||
h.Write([]byte(path))
|
|
||||||
h.Write([]byte(info.ModTime().String()))
|
|
||||||
sha := hex.EncodeToString(h.Sum(nil))
|
|
||||||
return sha, nil
|
|
||||||
}
|
|
||||||
|
@ -1,30 +1,60 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/zoriya/kyoo/transcoder/src"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client = &http.Client{Timeout: 10 * time.Second}
|
var safe_path = src.GetEnvOr("GOCODER_SAFE_PATH", "/video")
|
||||||
|
|
||||||
type Item struct {
|
// Encode the version in the hash path to update cached values.
|
||||||
Path string `json:"path"`
|
// Older versions won't be deleted (needed to allow multiples versions of the transcoder to run at the same time)
|
||||||
}
|
// If the version changes a lot, we might want to automatically delete older versions.
|
||||||
|
var version = "v1."
|
||||||
|
|
||||||
func GetPath(c echo.Context) (string, error) {
|
func GetPath(c echo.Context) (string, string, error) {
|
||||||
key := c.Request().Header.Get("X-Path")
|
key := c.Param("path")
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return "", echo.NewHTTPError(http.StatusBadRequest, "Missing resouce path. You need to set the X-Path header.")
|
return "", "", echo.NewHTTPError(http.StatusBadRequest, "Missing resouce path.")
|
||||||
}
|
}
|
||||||
return key, nil
|
pathb, err := base64.StdEncoding.DecodeString(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", echo.NewHTTPError(http.StatusBadRequest, "Invalid path. Should be base64 encoded.")
|
||||||
|
}
|
||||||
|
path := string(pathb)
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
return "", "", echo.NewHTTPError(http.StatusBadRequest, "Absolute path required.")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, safe_path) {
|
||||||
|
return "", "", echo.NewHTTPError(http.StatusBadRequest, "Selected path is not marked as safe.")
|
||||||
|
}
|
||||||
|
hash, err := getHash(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", echo.NewHTTPError(http.StatusNotFound, "File does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRoute(c echo.Context) string {
|
func getHash(path string) (string, error) {
|
||||||
return c.Request().Header.Get("X-Route")
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write([]byte(path))
|
||||||
|
h.Write([]byte(info.ModTime().String()))
|
||||||
|
sha := hex.EncodeToString(h.Sum(nil))
|
||||||
|
return version + sha, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SanitizePath(path string) error {
|
func SanitizePath(path string) error {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user