mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05: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