diff --git a/back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs b/back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs
new file mode 100644
index 00000000..e69177a1
--- /dev/null
+++ b/back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs
@@ -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 .
+
+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}$");
+
+ ///
+ 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);
+ }
+}
diff --git a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs
index 452aed44..5dd49309 100644
--- a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs
+++ b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs
@@ -76,6 +76,7 @@ public static class ServiceExtensions
services.Configure(x =>
{
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
+ x.ConstraintMap.Add("base64", typeof(Base64RouteConstraint));
});
services.AddResponseCompression(x =>
diff --git a/back/src/Kyoo.Core/Views/Content/ProxyApi.cs b/back/src/Kyoo.Core/Views/Content/ProxyApi.cs
deleted file mode 100644
index 28b36253..00000000
--- a/back/src/Kyoo.Core/Views/Content/ProxyApi.cs
+++ /dev/null
@@ -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 .
-
-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;
-
-///
-/// Proxy to other services
-///
-[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);
- }
-
- ///
- /// Transcoder proxy
- ///
- ///
- /// Simply proxy requests to the transcoder
- ///
- /// The path of the transcoder.
- /// The return value of the transcoder.
- [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 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}"));
- }
-}
diff --git a/back/src/Kyoo.Core/Views/Content/VideoApi.cs b/back/src/Kyoo.Core/Views/Content/VideoApi.cs
new file mode 100644
index 00000000..e3deb132
--- /dev/null
+++ b/back/src/Kyoo.Core/Views/Content/VideoApi.cs
@@ -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 .
+
+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;
+
+///
+/// Private routes of the transcoder.
+/// Url for these routes will be returned from /info or /master.m3u8 routes.
+/// This should not be called manually
+///
+[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");
+ }
+}
diff --git a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs
index 337f437b..43f25a8d 100644
--- a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs
+++ b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs
@@ -17,6 +17,7 @@
// along with Kyoo. If not, see .
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(IRepository repository) : CrudThumbsApi(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(IRepository 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 GetPath(Identifier identifier);
+
+ private async Task _GetPath64(Identifier identifier)
+ {
+ string path = await GetPath(identifier);
+ return Convert.ToBase64String(Encoding.UTF8.GetBytes(path));
+ }
///
/// Direct stream
@@ -76,11 +69,12 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp
/// No episode with the given ID or slug could be found.
[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 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");
}
///
@@ -96,39 +90,12 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp
/// No episode with the given ID or slug could be found.
[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 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");
}
///
@@ -144,7 +111,7 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp
[PartialPermission(Kind.Read)]
public async Task GetInfo(Identifier identifier)
{
- await _Proxy("/info", await GetPath(identifier));
+ await _Proxy($"{await _GetPath64(identifier)}/info");
}
///
@@ -160,7 +127,7 @@ public abstract class TranscoderApi(IRepository 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");
}
///
@@ -177,6 +144,6 @@ public abstract class TranscoderApi(IRepository 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");
}
}
diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
index 9fb268a0..b26c196c 100644
--- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
@@ -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 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;
}
}
diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs
index 1d95ab6f..274b4e31 100644
--- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs
@@ -221,12 +221,12 @@ public class MovieApi(ILibraryManager libraryManager) : TranscoderApi(lib
await libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow());
}
- protected override async Task<(string path, string route)> GetPath(Identifier identifier)
+ protected override async Task 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;
}
}
diff --git a/docker-compose.build.yml b/docker-compose.build.yml
index b03a6d87..4ff0419e 100644
--- a/docker-compose.build.yml
+++ b/docker-compose.build.yml
@@ -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']
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 4ee3f10a..6f2fa693 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -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']
diff --git a/docker-compose.yml b/docker-compose.yml
index 9185a6dd..fa10ead6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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']
diff --git a/front/packages/ui/src/player/components/scrubber.tsx b/front/packages/ui/src/player/components/scrubber.tsx
index e2757312..acc9670a 100644
--- a/front/packages/ui/src/player/components/scrubber.tsx
+++ b/front/packages/ui/src/player/components/scrubber.tsx
@@ -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],
diff --git a/front/packages/ui/src/player/video.web.tsx b/front/packages/ui/src/player/video.web.tsx
index a0454ab3..f0d01dce 100644
--- a/front/packages/ui/src/player/video.web.tsx
+++ b/front/packages/ui/src/player/video.web.tsx
@@ -359,7 +359,7 @@ export const AudiosMenu = ({
{hls.audioTracks.map((x, i) => (
setAudio(audios?.[i] ?? ({ index: i } as any))}
/>
diff --git a/transcoder/main.go b/transcoder/main.go
index e6fc54fb..152b0972 100644
--- a/transcoder/main.go
+++ b/transcoder/main.go
@@ -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"))
}
diff --git a/transcoder/src/extract.go b/transcoder/src/extract.go
index 55478dfc..cb6b77f7 100644
--- a/transcoder/src/extract.go
+++ b/transcoder/src/extract.go
@@ -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)
diff --git a/transcoder/src/filestream.go b/transcoder/src/filestream.go
index 984cafb1..f5fd2cfa 100644
--- a/transcoder/src/filestream.go
+++ b/transcoder/src/filestream.go
@@ -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
diff --git a/transcoder/src/info.go b/transcoder/src/info.go
index 75f4a80f..b79fdeb8 100644
--- a/transcoder/src/info.go
+++ b/transcoder/src/info.go
@@ -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 {
diff --git a/transcoder/src/settings.go b/transcoder/src/settings.go
index c6af123d..8039759b 100644
--- a/transcoder/src/settings.go
+++ b/transcoder/src/settings.go
@@ -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(),
}
diff --git a/transcoder/src/thumbnails.go b/transcoder/src/thumbnails.go
index ed8e24c5..fde155b9 100644
--- a/transcoder/src/thumbnails.go
+++ b/transcoder/src/thumbnails.go
@@ -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,
diff --git a/transcoder/src/transcoder.go b/transcoder/src/transcoder.go
index 53cec7b1..1dffecb6 100644
--- a/transcoder/src/transcoder.go
+++ b/transcoder/src/transcoder.go
@@ -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
}
diff --git a/transcoder/src/utils.go b/transcoder/src/utils.go
index 9fd84e8b..5277def3 100644
--- a/transcoder/src/utils.go
+++ b/transcoder/src/utils.go
@@ -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
-}
diff --git a/transcoder/utils.go b/transcoder/utils.go
index 4eaf7d4e..e68a6b93 100644
--- a/transcoder/utils.go
+++ b/transcoder/utils.go
@@ -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 {