mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 10:37: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 => | ||||
| 		{ | ||||
| 			x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint)); | ||||
| 			x.ConstraintMap.Add("base64", typeof(Base64RouteConstraint)); | ||||
| 		}); | ||||
| 
 | ||||
| 		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/>. | ||||
| 
 | ||||
| using System; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using AspNetCore.Proxy; | ||||
| using AspNetCore.Proxy.Options; | ||||
| @ -29,27 +30,13 @@ using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Kyoo.Core.Api; | ||||
| 
 | ||||
| public static class Transcoder | ||||
| { | ||||
| 	public static string TranscoderUrl = | ||||
| 		Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666"; | ||||
| } | ||||
| 
 | ||||
| public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsApi<T>(repository) | ||||
| 	where T : class, IResource, IThumbnails, IQuery | ||||
| { | ||||
| 	private Task _Proxy(string route, (string path, string route) info) | ||||
| 	private Task _Proxy(string route) | ||||
| 	{ | ||||
| 		HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder | ||||
| 			.Instance.WithBeforeSend( | ||||
| 				(ctx, req) => | ||||
| 				{ | ||||
| 					req.Headers.Add("X-Path", info.path); | ||||
| 					req.Headers.Add("X-Route", info.route); | ||||
| 					return Task.CompletedTask; | ||||
| 				} | ||||
| 			) | ||||
| 			.WithHandleFailure( | ||||
| 			.Instance.WithHandleFailure( | ||||
| 				async (context, exception) => | ||||
| 				{ | ||||
| 					context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; | ||||
| @ -59,10 +46,16 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp | ||||
| 				} | ||||
| 			) | ||||
| 			.Build(); | ||||
| 		return this.HttpProxyAsync($"{Transcoder.TranscoderUrl}{route}", proxyOptions); | ||||
| 		return this.HttpProxyAsync($"{VideoApi.TranscoderUrl}/{route}", proxyOptions); | ||||
| 	} | ||||
| 
 | ||||
| 	protected abstract Task<(string path, string route)> GetPath(Identifier identifier); | ||||
| 	protected abstract Task<string> GetPath(Identifier identifier); | ||||
| 
 | ||||
| 	private async Task<string> _GetPath64(Identifier identifier) | ||||
| 	{ | ||||
| 		string path = await GetPath(identifier); | ||||
| 		return Convert.ToBase64String(Encoding.UTF8.GetBytes(path)); | ||||
| 	} | ||||
| 
 | ||||
| 	/// <summary> | ||||
| 	/// Direct stream | ||||
| @ -76,11 +69,12 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp | ||||
| 	/// <response code="404">No episode with the given ID or slug could be found.</response> | ||||
| 	[HttpGet("{identifier:id}/direct")] | ||||
| 	[PartialPermission(Kind.Play)] | ||||
| 	[ProducesResponseType(StatusCodes.Status206PartialContent)] | ||||
| 	[ProducesResponseType(StatusCodes.Status302Found)] | ||||
| 	[ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
| 	public async Task GetDirectStream(Identifier identifier) | ||||
| 	public async Task<IActionResult> GetDirectStream(Identifier identifier) | ||||
| 	{ | ||||
| 		await _Proxy("/direct", await GetPath(identifier)); | ||||
| 		// TODO: Remove the /api and use a proxy rewrite instead. | ||||
| 		return Redirect($"/api/video/{await _GetPath64(identifier)}/direct"); | ||||
| 	} | ||||
| 
 | ||||
| 	/// <summary> | ||||
| @ -96,39 +90,12 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp | ||||
| 	/// <response code="404">No episode with the given ID or slug could be found.</response> | ||||
| 	[HttpGet("{identifier:id}/master.m3u8")] | ||||
| 	[PartialPermission(Kind.Play)] | ||||
| 	[ProducesResponseType(StatusCodes.Status206PartialContent)] | ||||
| 	[ProducesResponseType(StatusCodes.Status302Found)] | ||||
| 	[ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
| 	public async Task GetMaster(Identifier identifier) | ||||
| 	public async Task<IActionResult> GetMaster(Identifier identifier) | ||||
| 	{ | ||||
| 		await _Proxy("/master.m3u8", await GetPath(identifier)); | ||||
| 	} | ||||
| 
 | ||||
| 	[HttpGet("{identifier:id}/{quality}/index.m3u8")] | ||||
| 	[PartialPermission(Kind.Play)] | ||||
| 	public async Task GetVideoIndex(Identifier identifier, string quality) | ||||
| 	{ | ||||
| 		await _Proxy($"/{quality}/index.m3u8", await GetPath(identifier)); | ||||
| 	} | ||||
| 
 | ||||
| 	[HttpGet("{identifier:id}/{quality}/{segment}")] | ||||
| 	[PartialPermission(Kind.Play)] | ||||
| 	public async Task GetVideoSegment(Identifier identifier, string quality, string segment) | ||||
| 	{ | ||||
| 		await _Proxy($"/{quality}/{segment}", await GetPath(identifier)); | ||||
| 	} | ||||
| 
 | ||||
| 	[HttpGet("{identifier:id}/audio/{audio}/index.m3u8")] | ||||
| 	[PartialPermission(Kind.Play)] | ||||
| 	public async Task GetAudioIndex(Identifier identifier, string audio) | ||||
| 	{ | ||||
| 		await _Proxy($"/audio/{audio}/index.m3u8", await GetPath(identifier)); | ||||
| 	} | ||||
| 
 | ||||
| 	[HttpGet("{identifier:id}/audio/{audio}/{segment}")] | ||||
| 	[PartialPermission(Kind.Play)] | ||||
| 	public async Task GetAudioSegment(Identifier identifier, string audio, string segment) | ||||
| 	{ | ||||
| 		await _Proxy($"/audio/{audio}/{segment}", await GetPath(identifier)); | ||||
| 		// TODO: Remove the /api and use a proxy rewrite instead. | ||||
| 		return Redirect($"/api/video/{await _GetPath64(identifier)}/master.m3u8"); | ||||
| 	} | ||||
| 
 | ||||
| 	/// <summary> | ||||
| @ -144,7 +111,7 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp | ||||
| 	[PartialPermission(Kind.Read)] | ||||
| 	public async Task GetInfo(Identifier identifier) | ||||
| 	{ | ||||
| 		await _Proxy("/info", await GetPath(identifier)); | ||||
| 		await _Proxy($"{await _GetPath64(identifier)}/info"); | ||||
| 	} | ||||
| 
 | ||||
| 	/// <summary> | ||||
| @ -160,7 +127,7 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp | ||||
| 	[PartialPermission(Kind.Read)] | ||||
| 	public async Task GetThumbnails(Identifier identifier) | ||||
| 	{ | ||||
| 		await _Proxy("/thumbnails.png", await GetPath(identifier)); | ||||
| 		await _Proxy($"{await _GetPath64(identifier)}/thumbnails.png"); | ||||
| 	} | ||||
| 
 | ||||
| 	/// <summary> | ||||
| @ -177,6 +144,6 @@ public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsAp | ||||
| 	[PartialPermission(Kind.Read)] | ||||
| 	public async Task GetThumbnailsVtt(Identifier identifier) | ||||
| 	{ | ||||
| 		await _Proxy("/thumbnails.vtt", await GetPath(identifier)); | ||||
| 		await _Proxy($"{await _GetPath64(identifier)}/thumbnails.vtt"); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -210,12 +210,12 @@ public class EpisodeApi(ILibraryManager libraryManager) | ||||
| 		await libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow()); | ||||
| 	} | ||||
| 
 | ||||
| 	protected override async Task<(string path, string route)> GetPath(Identifier identifier) | ||||
| 	protected override async Task<string> GetPath(Identifier identifier) | ||||
| 	{ | ||||
| 		string path = await identifier.Match( | ||||
| 			async id => (await Repository.Get(id)).Path, | ||||
| 			async slug => (await Repository.Get(slug)).Path | ||||
| 		); | ||||
| 		return (path, $"/episodes/{identifier}"); | ||||
| 		return path; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -221,12 +221,12 @@ public class MovieApi(ILibraryManager libraryManager) : TranscoderApi<Movie>(lib | ||||
| 		await libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow()); | ||||
| 	} | ||||
| 
 | ||||
| 	protected override async Task<(string path, string route)> GetPath(Identifier identifier) | ||||
| 	protected override async Task<string> GetPath(Identifier identifier) | ||||
| 	{ | ||||
| 		string path = await identifier.Match( | ||||
| 			async id => (await Repository.Get(id)).Path, | ||||
| 			async slug => (await Repository.Get(slug)).Path | ||||
| 		); | ||||
| 		return (path, $"/movies/{identifier}"); | ||||
| 		return path; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -7,6 +7,8 @@ x-transcoder: &transcoder-base | ||||
|   restart: on-failure | ||||
|   env_file: | ||||
|     - ./.env | ||||
|   environment: | ||||
|     - GOCODER_PREFIX=/video | ||||
|   volumes: | ||||
|     - ${LIBRARY_ROOT}:/video:ro | ||||
|     - ${CACHE_ROOT}:/cache | ||||
| @ -95,6 +97,7 @@ services: | ||||
|           devices: | ||||
|             - capabilities: [gpu] | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=nvidia | ||||
|     profiles: ['nvidia'] | ||||
| 
 | ||||
| @ -103,6 +106,7 @@ services: | ||||
|     devices: | ||||
|       - /dev/dri:/dev/dri | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=vaapi | ||||
|       - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} | ||||
|     profiles: ['vaapi'] | ||||
| @ -112,6 +116,7 @@ services: | ||||
|     devices: | ||||
|       - /dev/dri:/dev/dri | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=qsv | ||||
|       - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} | ||||
|     profiles: ['qsv'] | ||||
|  | ||||
| @ -12,6 +12,8 @@ x-transcoder: &transcoder-base | ||||
|   cpus: 1 | ||||
|   env_file: | ||||
|     - ./.env | ||||
|   environment: | ||||
|     - GOCODER_PREFIX=/video | ||||
|   volumes: | ||||
|     - ./transcoder:/app | ||||
|     - ${LIBRARY_ROOT}:/video:ro | ||||
| @ -119,6 +121,7 @@ services: | ||||
|           devices: | ||||
|             - capabilities: [gpu] | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=nvidia | ||||
|     profiles: ['nvidia'] | ||||
| 
 | ||||
| @ -127,6 +130,7 @@ services: | ||||
|     devices: | ||||
|       - /dev/dri:/dev/dri | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=vaapi | ||||
|       - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} | ||||
|     profiles: ['vaapi'] | ||||
| @ -136,6 +140,7 @@ services: | ||||
|     devices: | ||||
|       - /dev/dri:/dev/dri | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=qsv | ||||
|       - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} | ||||
|     profiles: ['qsv'] | ||||
|  | ||||
| @ -7,6 +7,8 @@ x-transcoder: &transcoder-base | ||||
|   restart: unless-stopped | ||||
|   env_file: | ||||
|     - ./.env | ||||
|   environment: | ||||
|     - GOCODER_PREFIX=/video | ||||
|   volumes: | ||||
|     - ${LIBRARY_ROOT}:/video:ro | ||||
|     - ${CACHE_ROOT}:/cache | ||||
| @ -94,6 +96,7 @@ services: | ||||
|           devices: | ||||
|             - capabilities: [gpu] | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=nvidia | ||||
|     profiles: ['nvidia'] | ||||
| 
 | ||||
| @ -102,6 +105,7 @@ services: | ||||
|     devices: | ||||
|       - /dev/dri:/dev/dri | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=vaapi | ||||
|       - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} | ||||
|     profiles: ['vaapi'] | ||||
| @ -111,6 +115,7 @@ services: | ||||
|     devices: | ||||
|       - /dev/dri:/dev/dri | ||||
|     environment: | ||||
|       - GOCODER_PREFIX=/video | ||||
|       - GOCODER_HWACCEL=qsv | ||||
|       - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} | ||||
|     profiles: ['qsv'] | ||||
|  | ||||
| @ -68,7 +68,7 @@ export const useScrubber = (url: string) => { | ||||
| 			ret[i] = { | ||||
| 				from: parseTs(times[0]), | ||||
| 				to: parseTs(times[1]), | ||||
| 				url: imageFn("/video/" + url[0]), | ||||
| 				url: imageFn(url[0]), | ||||
| 				x: xywh[0], | ||||
| 				y: xywh[1], | ||||
| 				width: xywh[2], | ||||
|  | ||||
| @ -359,7 +359,7 @@ export const AudiosMenu = ({ | ||||
| 			{hls.audioTracks.map((x, i) => ( | ||||
| 				<Menu.Item | ||||
| 					key={i.toString()} | ||||
| 					label={audios?.[i].displayName ?? x.name} | ||||
| 					label={audios?.[i]?.displayName ?? x.name} | ||||
| 					selected={hls!.audioTrack === i} | ||||
| 					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 | ||||
| // transmuxing is done. | ||||
| // | ||||
| // Path: /direct | ||||
| // Path: /:path/direct | ||||
| func DirectStream(c echo.Context) error { | ||||
| 	path, err := GetPath(c) | ||||
| 	path, _, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		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 | ||||
| // 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 { | ||||
| 	client, err := GetClientId(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	ret, err := h.transcoder.GetMaster(path, client, GetRoute(c)) | ||||
| 	ret, err := h.transcoder.GetMaster(path, client, sha) | ||||
| 	if err != nil { | ||||
| 		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 | ||||
| // available. | ||||
| // | ||||
| // Path: /:quality/index.m3u8 | ||||
| // Path: /:path/:quality/index.m3u8 | ||||
| func (h *Handler) GetVideoIndex(c echo.Context) error { | ||||
| 	quality, err := src.QualityFromString(c.Param("quality")) | ||||
| 	if err != nil { | ||||
| @ -65,12 +65,12 @@ func (h *Handler) GetVideoIndex(c echo.Context) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	ret, err := h.transcoder.GetVideoIndex(path, quality, client, GetRoute(c)) | ||||
| 	ret, err := h.transcoder.GetVideoIndex(path, quality, client, sha) | ||||
| 	if err != nil { | ||||
| 		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 | ||||
| // available. | ||||
| // | ||||
| // Path: /audio/:audio/index.m3u8 | ||||
| // Path: /:path/audio/:audio/index.m3u8 | ||||
| func (h *Handler) GetAudioIndex(c echo.Context) error { | ||||
| 	audio, err := strconv.ParseInt(c.Param("audio"), 10, 32) | ||||
| 	if err != nil { | ||||
| @ -93,12 +93,12 @@ func (h *Handler) GetAudioIndex(c echo.Context) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		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 { | ||||
| 		return err | ||||
| 	} | ||||
| @ -109,7 +109,7 @@ func (h *Handler) GetAudioIndex(c echo.Context) error { | ||||
| // | ||||
| // 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 { | ||||
| 	quality, err := src.QualityFromString(c.Param("quality")) | ||||
| 	if err != nil { | ||||
| @ -123,12 +123,12 @@ func (h *Handler) GetVideoSegment(c echo.Context) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		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 { | ||||
| 		return err | ||||
| 	} | ||||
| @ -139,7 +139,7 @@ func (h *Handler) GetVideoSegment(c echo.Context) error { | ||||
| // | ||||
| // 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 { | ||||
| 	audio, err := strconv.ParseInt(c.Param("audio"), 10, 32) | ||||
| 	if err != nil { | ||||
| @ -153,12 +153,12 @@ func (h *Handler) GetAudioSegment(c echo.Context) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		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 { | ||||
| 		return err | ||||
| 	} | ||||
| @ -169,29 +169,20 @@ func (h *Handler) GetAudioSegment(c echo.Context) error { | ||||
| // | ||||
| // Identify metadata about a file. | ||||
| // | ||||
| // Path: /info | ||||
| // Path: /:path/info | ||||
| func (h *Handler) GetInfo(c echo.Context) error { | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	route := GetRoute(c) | ||||
| 	sha, err := src.GetHash(path) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	ret, err := src.GetInfo(path, sha, route) | ||||
| 	ret, err := src.GetInfo(path, sha) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	// Run extractors to have them in cache | ||||
| 	src.Extract(ret.Path, sha, route) | ||||
| 	go src.ExtractThumbnail( | ||||
| 		ret.Path, | ||||
| 		route, | ||||
| 		sha, | ||||
| 	) | ||||
| 	src.Extract(ret.Path, sha) | ||||
| 	go src.ExtractThumbnail(ret.Path, sha) | ||||
| 	return c.JSON(http.StatusOK, ret) | ||||
| } | ||||
| 
 | ||||
| @ -199,9 +190,9 @@ func (h *Handler) GetInfo(c echo.Context) error { | ||||
| // | ||||
| // Get a specific attachment. | ||||
| // | ||||
| // Path: /attachment/:name | ||||
| // Path: /:path/attachment/:name | ||||
| func (h *Handler) GetAttachment(c echo.Context) error { | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -210,12 +201,7 @@ func (h *Handler) GetAttachment(c echo.Context) error { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	route := GetRoute(c) | ||||
| 	sha, err := src.GetHash(path) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	wait, err := src.Extract(path, sha, route) | ||||
| 	wait, err := src.Extract(path, sha) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -229,9 +215,9 @@ func (h *Handler) GetAttachment(c echo.Context) error { | ||||
| // | ||||
| // Get a specific subtitle. | ||||
| // | ||||
| // Path: /subtitle/:name | ||||
| // Path: /:path/subtitle/:name | ||||
| func (h *Handler) GetSubtitle(c echo.Context) error { | ||||
| 	path, err := GetPath(c) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -240,12 +226,7 @@ func (h *Handler) GetSubtitle(c echo.Context) error { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	route := GetRoute(c) | ||||
| 	sha, err := src.GetHash(path) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	wait, err := src.Extract(path, sha, route) | ||||
| 	wait, err := src.Extract(path, sha) | ||||
| 	if err != nil { | ||||
| 		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. | ||||
| // | ||||
| // Path: /thumbnails.png | ||||
| // Path: /:path/thumbnails.png | ||||
| func (h *Handler) GetThumbnails(c echo.Context) error { | ||||
| 	path, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	sha, err := src.GetHash(path) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	out, err := src.ExtractThumbnail( | ||||
| 		path, | ||||
| 		GetRoute(c), | ||||
| 		sha, | ||||
| 	) | ||||
| 	out, err := src.ExtractThumbnail(path, sha) | ||||
| 	if err != nil { | ||||
| 		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. | ||||
| // 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 { | ||||
| 	path, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	sha, err := src.GetHash(path) | ||||
| 	path, sha, err := GetPath(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	out, err := src.ExtractThumbnail( | ||||
| 		path, | ||||
| 		GetRoute(c), | ||||
| 		sha, | ||||
| 	) | ||||
| 	out, err := src.ExtractThumbnail(path, sha) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -328,17 +293,17 @@ func main() { | ||||
| 		transcoder: transcoder, | ||||
| 	} | ||||
| 
 | ||||
| 	e.GET("/direct", DirectStream) | ||||
| 	e.GET("/master.m3u8", h.GetMaster) | ||||
| 	e.GET("/:quality/index.m3u8", h.GetVideoIndex) | ||||
| 	e.GET("/audio/:audio/index.m3u8", h.GetAudioIndex) | ||||
| 	e.GET("/:quality/:chunk", h.GetVideoSegment) | ||||
| 	e.GET("/audio/:audio/:chunk", h.GetAudioSegment) | ||||
| 	e.GET("/info", h.GetInfo) | ||||
| 	e.GET("/thumbnails.png", h.GetThumbnails) | ||||
| 	e.GET("/thumbnails.vtt", h.GetThumbnailsVtt) | ||||
| 	e.GET("/attachment/:name", h.GetAttachment) | ||||
| 	e.GET("/subtitle/:name", h.GetSubtitle) | ||||
| 	e.GET("/:path/direct", DirectStream) | ||||
| 	e.GET("/:path/master.m3u8", h.GetMaster) | ||||
| 	e.GET("/:path/:quality/index.m3u8", h.GetVideoIndex) | ||||
| 	e.GET("/:path/audio/:audio/index.m3u8", h.GetAudioIndex) | ||||
| 	e.GET("/:path/:quality/:chunk", h.GetVideoSegment) | ||||
| 	e.GET("/:path/audio/:audio/:chunk", h.GetAudioSegment) | ||||
| 	e.GET("/:path/info", h.GetInfo) | ||||
| 	e.GET("/:path/thumbnails.png", h.GetThumbnails) | ||||
| 	e.GET("/:path/thumbnails.vtt", h.GetThumbnailsVtt) | ||||
| 	e.GET("/:path/attachment/:name", h.GetAttachment) | ||||
| 	e.GET("/:path/subtitle/:name", h.GetSubtitle) | ||||
| 
 | ||||
| 	e.Logger.Fatal(e.Start(":7666")) | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,7 @@ import ( | ||||
| 
 | ||||
| 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{}) | ||||
| 	existing, created := extracted.GetOrSet(sha, ret) | ||||
| 	if !created { | ||||
| @ -18,7 +18,7 @@ func Extract(path string, sha string, route string) (<-chan struct{}, error) { | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer printExecTime("Starting extraction of %s", path)() | ||||
| 		info, err := GetInfo(path, sha, route) | ||||
| 		info, err := GetInfo(path, sha) | ||||
| 		if err != nil { | ||||
| 			extracted.Remove(sha) | ||||
| 			close(ret) | ||||
|  | ||||
| @ -19,7 +19,7 @@ type FileStream struct { | ||||
| 	audios    CMap[int32, *AudioStream] | ||||
| } | ||||
| 
 | ||||
| func NewFileStream(path string, sha string, route string) *FileStream { | ||||
| func NewFileStream(path string, sha string) *FileStream { | ||||
| 	ret := &FileStream{ | ||||
| 		Path:   path, | ||||
| 		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) | ||||
| 	go func() { | ||||
| 		defer ret.ready.Done() | ||||
| 		info, err := GetInfo(path, sha, route) | ||||
| 		info, err := GetInfo(path, sha) | ||||
| 		ret.Info = info | ||||
| 		if err != nil { | ||||
| 			ret.err = err | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| package src | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| @ -180,7 +181,7 @@ type MICache struct { | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| 	ret, _ := infos.GetOrCreate(sha, func() *MICache { | ||||
| @ -195,7 +196,7 @@ func GetInfo(path string, sha string, route string) (*MediaInfo, error) { | ||||
| 			} | ||||
| 
 | ||||
| 			var val *MediaInfo | ||||
| 			val, err = getInfo(path, route) | ||||
| 			val, err = getInfo(path) | ||||
| 			*mi.info = *val | ||||
| 			mi.info.Sha = sha | ||||
| 			mi.ready.Done() | ||||
| @ -231,7 +232,7 @@ func saveInfo[T any](save_path string, mi *T) error { | ||||
| 	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)() | ||||
| 
 | ||||
| 	mi, err := mediainfo.Open(path) | ||||
| @ -294,7 +295,7 @@ func getInfo(path string, route string) (*MediaInfo, error) { | ||||
| 			extension := OrNull(SubtitleExtensions[format]) | ||||
| 			var link *string | ||||
| 			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 | ||||
| 			} | ||||
| 			return Subtitle{ | ||||
| @ -319,7 +320,7 @@ func getInfo(path string, route string) (*MediaInfo, error) { | ||||
| 		Fonts: Map( | ||||
| 			attachments, | ||||
| 			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 { | ||||
|  | ||||
| @ -11,9 +11,10 @@ func GetEnvOr(env string, def string) string { | ||||
| } | ||||
| 
 | ||||
| type SettingsT struct { | ||||
| 	Outpath  string | ||||
| 	Metadata string | ||||
| 	HwAccel  HwAccelT | ||||
| 	Outpath     string | ||||
| 	Metadata    string | ||||
| 	RoutePrefix string | ||||
| 	HwAccel     HwAccelT | ||||
| } | ||||
| 
 | ||||
| type HwAccelT struct { | ||||
| @ -24,7 +25,8 @@ type HwAccelT struct { | ||||
| } | ||||
| 
 | ||||
| var Settings = SettingsT{ | ||||
| 	Outpath:  GetEnvOr("GOCODER_CACHE_ROOT", "/cache"), | ||||
| 	Metadata: GetEnvOr("GOCODER_METADATA_ROOT", "/metadata"), | ||||
| 	HwAccel:  DetectHardwareAccel(), | ||||
| 	Outpath:     GetEnvOr("GOCODER_CACHE_ROOT", "/cache"), | ||||
| 	Metadata:    GetEnvOr("GOCODER_METADATA_ROOT", "/metadata"), | ||||
| 	RoutePrefix: GetEnvOr("GOCODER_PREFIX", ""), | ||||
| 	HwAccel:     DetectHardwareAccel(), | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| package src | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"image/color" | ||||
| @ -27,14 +28,14 @@ type Thumbnail struct { | ||||
| 
 | ||||
| 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 := &Thumbnail{ | ||||
| 			path: fmt.Sprintf("%s/%s", Settings.Metadata, sha), | ||||
| 		} | ||||
| 		ret.ready.Add(1) | ||||
| 		go func() { | ||||
| 			extractThumbnail(path, ret.path, fmt.Sprintf("%s/thumbnails.png", route)) | ||||
| 			extractThumbnail(path, ret.path) | ||||
| 			ret.ready.Done() | ||||
| 		}() | ||||
| 		return ret | ||||
| @ -43,7 +44,7 @@ func ExtractThumbnail(path string, route string, sha string) (string, error) { | ||||
| 	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)() | ||||
| 	os.MkdirAll(out, 0o755) | ||||
| 	sprite_path := fmt.Sprintf("%s/sprite.png", out) | ||||
| @ -97,10 +98,11 @@ func extractThumbnail(path string, out string, name string) error { | ||||
| 		timestamps := ts | ||||
| 		ts += interval | ||||
| 		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(ts), | ||||
| 			name, | ||||
| 			Settings.RoutePrefix, | ||||
| 			base64.StdEncoding.EncodeToString([]byte(path)), | ||||
| 			x, | ||||
| 			y, | ||||
| 			width, | ||||
|  | ||||
| @ -33,14 +33,10 @@ func NewTranscoder() (*Transcoder, error) { | ||||
| 	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 | ||||
| 	ret, _ := t.streams.GetOrCreate(path, func() *FileStream { | ||||
| 		sha, err := GetHash(path) | ||||
| 		if err != nil { | ||||
| 			return nil | ||||
| 		} | ||||
| 		return NewFileStream(path, sha, route) | ||||
| 		return NewFileStream(path, sha) | ||||
| 	}) | ||||
| 	ret.ready.Wait() | ||||
| 	if err != nil || ret.err != nil { | ||||
| @ -50,8 +46,8 @@ func (t *Transcoder) getFileStream(path string, route string) (*FileStream, erro | ||||
| 	return ret, nil | ||||
| } | ||||
| 
 | ||||
| func (t *Transcoder) GetMaster(path string, client string, route string) (string, error) { | ||||
| 	stream, err := t.getFileStream(path, route) | ||||
| func (t *Transcoder) GetMaster(path string, client string, sha string) (string, error) { | ||||
| 	stream, err := t.getFileStream(path, sha) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @ -69,9 +65,9 @@ func (t *Transcoder) GetVideoIndex( | ||||
| 	path string, | ||||
| 	quality Quality, | ||||
| 	client string, | ||||
| 	route string, | ||||
| 	sha string, | ||||
| ) (string, error) { | ||||
| 	stream, err := t.getFileStream(path, route) | ||||
| 	stream, err := t.getFileStream(path, sha) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @ -89,9 +85,9 @@ func (t *Transcoder) GetAudioIndex( | ||||
| 	path string, | ||||
| 	audio int32, | ||||
| 	client string, | ||||
| 	route string, | ||||
| 	sha string, | ||||
| ) (string, error) { | ||||
| 	stream, err := t.getFileStream(path, route) | ||||
| 	stream, err := t.getFileStream(path, sha) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @ -109,9 +105,9 @@ func (t *Transcoder) GetVideoSegment( | ||||
| 	quality Quality, | ||||
| 	segment int32, | ||||
| 	client string, | ||||
| 	route string, | ||||
| 	sha string, | ||||
| ) (string, error) { | ||||
| 	stream, err := t.getFileStream(path, route) | ||||
| 	stream, err := t.getFileStream(path, sha) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @ -130,9 +126,9 @@ func (t *Transcoder) GetAudioSegment( | ||||
| 	audio int32, | ||||
| 	segment int32, | ||||
| 	client string, | ||||
| 	route string, | ||||
| 	sha string, | ||||
| ) (string, error) { | ||||
| 	stream, err := t.getFileStream(path, route) | ||||
| 	stream, err := t.getFileStream(path, sha) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| @ -1,11 +1,8 @@ | ||||
| package src | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/sha1" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| @ -18,15 +15,3 @@ func printExecTime(message string, args ...any) func() { | ||||
| 		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 | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/sha1" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"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 { | ||||
| 	Path string `json:"path"` | ||||
| } | ||||
| // Encode the version in the hash path to update cached values. | ||||
| // 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) { | ||||
| 	key := c.Request().Header.Get("X-Path") | ||||
| func GetPath(c echo.Context) (string, string, error) { | ||||
| 	key := c.Param("path") | ||||
| 	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 { | ||||
| 	return c.Request().Header.Get("X-Route") | ||||
| 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 version + sha, nil | ||||
| } | ||||
| 
 | ||||
| func SanitizePath(path string) error { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user