Start to remove transcoder dependence on kyoo

This commit is contained in:
Zoe Roux 2024-02-18 17:19:24 +01:00
parent 19485a110a
commit 0a0939fa3d
9 changed files with 223 additions and 147 deletions

View File

@ -44,9 +44,14 @@ namespace Kyoo.Abstractions.Models.Permissions
Create, Create,
/// <summary> /// <summary>
/// Allow the user to delete this kind od data. /// Allow the user to delete this kind of data.
/// </summary> /// </summary>
Delete Delete,
/// <summary>
/// Allow the user to play this file.
/// </summary>
Play,
} }
/// <summary> /// <summary>

View File

@ -271,8 +271,8 @@ namespace Kyoo.Abstractions.Models
public VideoLinks Links => public VideoLinks Links =>
new() new()
{ {
Direct = $"/video/episode/{Slug}/direct", Direct = $"/episode/{Slug}/direct",
Hls = $"/video/episode/{Slug}/master.m3u8", Hls = $"/episode/{Slug}/master.m3u8",
}; };
/// <summary> /// <summary>

View File

@ -68,8 +68,7 @@ public class WatchStatusRepository : IWatchStatusRepository
DatabaseContext db = scope.ServiceProvider.GetRequiredService<DatabaseContext>(); DatabaseContext db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
WatchStatusRepository repo = WatchStatusRepository repo =
scope.ServiceProvider.GetRequiredService<WatchStatusRepository>(); scope.ServiceProvider.GetRequiredService<WatchStatusRepository>();
List<Guid> users = await db List<Guid> users = await db.ShowWatchStatus.IgnoreQueryFilters()
.ShowWatchStatus.IgnoreQueryFilters()
.Where(x => x.ShowId == ep.ShowId && x.Status == WatchStatus.Completed) .Where(x => x.ShowId == ep.ShowId && x.Status == WatchStatus.Completed)
.Select(x => x.UserId) .Select(x => x.UserId)
.ToListAsync(); .ToListAsync();

View File

@ -0,0 +1,52 @@
// 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.Threading.Tasks;
using AspNetCore.Proxy;
using AspNetCore.Proxy.Options;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Controllers;
public class Transcoder : Controller
{
public Task Proxy(string route, string path)
{
HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder
.Instance.WithBeforeSend(
(ctx, req) =>
{
req.Headers.Add("X-Path", path);
return Task.CompletedTask;
}
)
.WithHandleFailure(
async (context, exception) =>
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(
new RequestError("Service unavailable")
);
}
)
.Build();
return this.HttpProxyAsync($"http://transcoder:7666/{route}", proxyOptions);
}
}

View File

@ -110,8 +110,7 @@ namespace Kyoo.Core
options.SuppressMapClientErrors = true; options.SuppressMapClientErrors = true;
options.InvalidModelStateResponseFactory = ctx => options.InvalidModelStateResponseFactory = ctx =>
{ {
string[] errors = ctx string[] errors = ctx.ModelState.SelectMany(x => x.Value!.Errors)
.ModelState.SelectMany(x => x.Value!.Errors)
.Select(x => x.ErrorMessage) .Select(x => x.ErrorMessage)
.ToArray(); .ToArray();
return new BadRequestObjectResult(new RequestError(errors)); return new BadRequestObjectResult(new RequestError(errors));

View File

@ -24,6 +24,7 @@ using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Authentication; using Kyoo.Authentication;
using Kyoo.Core.Controllers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants; using static Kyoo.Abstractions.Models.Utils.Constants;
@ -38,26 +39,12 @@ namespace Kyoo.Core.Api
[ApiController] [ApiController]
[PartialPermission(nameof(Episode))] [PartialPermission(nameof(Episode))]
[ApiDefinition("Episodes", Group = ResourcesGroup)] [ApiDefinition("Episodes", Group = ResourcesGroup)]
public class EpisodeApi : CrudThumbsApi<Episode> public class EpisodeApi(
ILibraryManager libraryManager,
IThumbnailsManager thumbnails,
Transcoder transcoder
) : CrudThumbsApi<Episode>(libraryManager.Episodes, thumbnails)
{ {
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="EpisodeApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
public EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails)
: base(libraryManager.Episodes, thumbnails)
{
_libraryManager = libraryManager;
}
/// <summary> /// <summary>
/// Get episode's show /// Get episode's show
/// </summary> /// </summary>
@ -77,7 +64,7 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Show> fields [FromQuery] Include<Show> fields
) )
{ {
return await _libraryManager.Shows.Get( return await libraryManager.Shows.Get(
identifier.IsContainedIn<Show, Episode>(x => x.Episodes!), identifier.IsContainedIn<Show, Episode>(x => x.Episodes!),
fields fields
); );
@ -104,15 +91,15 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Season> fields [FromQuery] Include<Season> fields
) )
{ {
Season? ret = await _libraryManager.Seasons.GetOrDefault( Season? ret = await libraryManager.Seasons.GetOrDefault(
identifier.IsContainedIn<Season, Episode>(x => x.Episodes!), identifier.IsContainedIn<Season, Episode>(x => x.Episodes!),
fields fields
); );
if (ret != null) if (ret != null)
return ret; return ret;
Episode? episode = await identifier.Match( Episode? episode = await identifier.Match(
id => _libraryManager.Episodes.GetOrDefault(id), id => libraryManager.Episodes.GetOrDefault(id),
slug => _libraryManager.Episodes.GetOrDefault(slug) slug => libraryManager.Episodes.GetOrDefault(slug)
); );
return episode == null ? NotFound() : NoContent(); return episode == null ? NotFound() : NoContent();
} }
@ -136,9 +123,9 @@ namespace Kyoo.Core.Api
{ {
Guid id = await identifier.Match( Guid id = await identifier.Match(
id => Task.FromResult(id), id => Task.FromResult(id),
async slug => (await _libraryManager.Episodes.Get(slug)).Id async slug => (await libraryManager.Episodes.Get(slug)).Id
); );
return await _libraryManager.WatchStatus.GetEpisodeStatus(id, User.GetIdOrThrow()); return await libraryManager.WatchStatus.GetEpisodeStatus(id, User.GetIdOrThrow());
} }
/// <summary> /// <summary>
@ -170,9 +157,9 @@ namespace Kyoo.Core.Api
{ {
Guid id = await identifier.Match( Guid id = await identifier.Match(
id => Task.FromResult(id), id => Task.FromResult(id),
async slug => (await _libraryManager.Episodes.Get(slug)).Id async slug => (await libraryManager.Episodes.Get(slug)).Id
); );
return await _libraryManager.WatchStatus.SetEpisodeStatus( return await libraryManager.WatchStatus.SetEpisodeStatus(
id, id,
User.GetIdOrThrow(), User.GetIdOrThrow(),
status, status,
@ -199,9 +186,100 @@ namespace Kyoo.Core.Api
{ {
Guid id = await identifier.Match( Guid id = await identifier.Match(
id => Task.FromResult(id), id => Task.FromResult(id),
async slug => (await _libraryManager.Episodes.Get(slug)).Id async slug => (await libraryManager.Episodes.Get(slug)).Id
); );
await _libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow()); await libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow());
}
/// <summary>
/// Direct stream
/// </summary>
/// <remarks>
/// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or
/// transmuxing is done.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <returns>The video file of this episode.</returns>
/// <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.Status404NotFound)]
public async Task GetDirectStream(Identifier identifier)
{
string path = await identifier.Match(
async id => (await libraryManager.Episodes.Get(id)).Path,
async slug => (await libraryManager.Episodes.Get(slug)).Path
);
await transcoder.Proxy("/direct", path);
}
/// <summary>
/// Get master playlist
/// </summary>
/// <remarks>
/// Get a master playlist containing all possible video qualities and audios available for this resource.
/// 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.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <returns>The master playlist of this episode.</returns>
/// <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.Status404NotFound)]
public async Task GetMaster(Identifier identifier)
{
string path = await identifier.Match(
async id => (await libraryManager.Episodes.Get(id)).Path,
async slug => (await libraryManager.Episodes.Get(slug)).Path
);
await transcoder.Proxy("/master.m3u8", path);
}
[HttpGet("{identifier:id}/{quality}/index.m3u8")]
[PartialPermission(Kind.Play)]
public async Task GetVideoIndex(Identifier identifier, string quality)
{
string path = await identifier.Match(
async id => (await libraryManager.Episodes.Get(id)).Path,
async slug => (await libraryManager.Episodes.Get(slug)).Path
);
await transcoder.Proxy($"/{quality}/index.m3u8", path);
}
[HttpGet("{identifier:id}/audio/{audio}/index.m3u8")]
[PartialPermission(Kind.Play)]
public async Task GetAudioIndex(Identifier identifier, string audio)
{
string path = await identifier.Match(
async id => (await libraryManager.Episodes.Get(id)).Path,
async slug => (await libraryManager.Episodes.Get(slug)).Path
);
await transcoder.Proxy($"/audio/{audio}/index.m3u8", path);
}
[HttpGet("{identifier:id}/audio/{audio}/{segment}")]
[PartialPermission(Kind.Play)]
public async Task GetAudioSegment(Identifier identifier, string audio, string segment)
{
string path = await identifier.Match(
async id => (await libraryManager.Episodes.Get(id)).Path,
async slug => (await libraryManager.Episodes.Get(slug)).Path
);
await transcoder.Proxy($"/audio/{audio}/{segment}", path);
}
[HttpGet("{identifier:id}/info")]
[PartialPermission(Kind.Play)]
public async Task GetInfo(Identifier identifier)
{
string path = await identifier.Match(
async id => (await libraryManager.Episodes.Get(id)).Path,
async slug => (await libraryManager.Episodes.Get(slug)).Path
);
await transcoder.Proxy("/info", path);
} }
} }
} }

View File

@ -16,14 +16,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// 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.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNetCore.Proxy; using Kyoo.Abstractions.Controllers;
using AspNetCore.Proxy.Options;
using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Controllers;
using Kyoo.Utils; using Kyoo.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
@ -32,20 +32,9 @@ namespace Kyoo.Core.Api
/// Proxy to other services /// Proxy to other services
/// </summary> /// </summary>
[ApiController] [ApiController]
public class ProxyApi : Controller [Obsolete("Use /episode/id/master.m3u8 or routes like that")]
public class ProxyApi(ILibraryManager library, Transcoder transcoder) : Controller
{ {
private readonly HttpProxyOptions _proxyOptions = HttpProxyOptionsBuilder
.Instance.WithHandleFailure(
async (context, exception) =>
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(
new RequestError("Service unavailable")
);
}
)
.Build();
/// <summary> /// <summary>
/// Transcoder proxy /// Transcoder proxy
/// </summary> /// </summary>
@ -54,15 +43,28 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="rest">The path of the transcoder.</param> /// <param name="rest">The path of the transcoder.</param>
/// <returns>The return value of the transcoder.</returns> /// <returns>The return value of the transcoder.</returns>
[Route("video/{**rest}")] [Route("video/{type}/{id:id}/{**rest}")]
[Permission("video", Kind.Read)] [Permission("video", Kind.Read)]
public Task Proxy(string rest, [FromQuery] Dictionary<string, string> query) [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
)
{ {
// TODO: Use an env var to configure transcoder:7666. string path = await (
return this.HttpProxyAsync( type is "movie" or "movies"
$"http://transcoder:7666/{rest}" + query.ToQueryString(), ? id.Match(
_proxyOptions 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 transcoder.Proxy(rest + query.ToQueryString(), path);
} }
} }
} }

View File

@ -16,12 +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: /:resource/:slug/direct // Path: /direct
func DirectStream(c echo.Context) error { func DirectStream(c echo.Context) error {
resource := c.Param("resource") path, err := GetPath(c)
slug := c.Param("slug")
path, err := GetPath(resource, slug)
if err != nil { if err != nil {
return err return err
} }
@ -34,17 +31,13 @@ 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: /:resource/:slug/master.m3u8 // Path: /master.m3u8
func (h *Handler) GetMaster(c echo.Context) error { func (h *Handler) GetMaster(c echo.Context) error {
resource := c.Param("resource")
slug := c.Param("slug")
client, err := GetClientId(c) client, err := GetClientId(c)
if err != nil { if err != nil {
return err return err
} }
path, err := GetPath(c)
path, err := GetPath(resource, slug)
if err != nil { if err != nil {
return err return err
} }
@ -62,21 +55,17 @@ 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: /:resource/:slug/:quality/index.m3u8 // Path: /:quality/index.m3u8
func (h *Handler) GetVideoIndex(c echo.Context) error { func (h *Handler) GetVideoIndex(c echo.Context) error {
resource := c.Param("resource")
slug := c.Param("slug")
quality, err := src.QualityFromString(c.Param("quality")) quality, err := src.QualityFromString(c.Param("quality"))
if err != nil { if err != nil {
return err return err
} }
client, err := GetClientId(c) client, err := GetClientId(c)
if err != nil { if err != nil {
return err return err
} }
path, err := GetPath(c)
path, err := GetPath(resource, slug)
if err != nil { if err != nil {
return err return err
} }
@ -94,21 +83,17 @@ 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: /:resource/:slug/audio/:audio/index.m3u8 // Path: /audio/:audio/index.m3u8
func (h *Handler) GetAudioIndex(c echo.Context) error { func (h *Handler) GetAudioIndex(c echo.Context) error {
resource := c.Param("resource")
slug := c.Param("slug")
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32) audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
if err != nil { if err != nil {
return err return err
} }
client, err := GetClientId(c) client, err := GetClientId(c)
if err != nil { if err != nil {
return err return err
} }
path, err := GetPath(c)
path, err := GetPath(resource, slug)
if err != nil { if err != nil {
return err return err
} }
@ -124,10 +109,8 @@ func (h *Handler) GetAudioIndex(c echo.Context) error {
// //
// Retrieve a chunk of a transmuxed video. // Retrieve a chunk of a transmuxed video.
// //
// Path: /:resource/:slug/:quality/segments-:chunk.ts // Path: /:quality/segments-:chunk.ts
func (h *Handler) GetVideoSegment(c echo.Context) error { func (h *Handler) GetVideoSegment(c echo.Context) error {
resource := c.Param("resource")
slug := c.Param("slug")
quality, err := src.QualityFromString(c.Param("quality")) quality, err := src.QualityFromString(c.Param("quality"))
if err != nil { if err != nil {
return err return err
@ -136,13 +119,11 @@ func (h *Handler) GetVideoSegment(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
client, err := GetClientId(c) client, err := GetClientId(c)
if err != nil { if err != nil {
return err return err
} }
path, err := GetPath(c)
path, err := GetPath(resource, slug)
if err != nil { if err != nil {
return err return err
} }
@ -158,10 +139,8 @@ func (h *Handler) GetVideoSegment(c echo.Context) error {
// //
// Retrieve a chunk of a transcoded audio. // Retrieve a chunk of a transcoded audio.
// //
// Path: /:resource/:slug/audio/:audio/segments-:chunk.ts // Path: /audio/:audio/segments-:chunk.ts
func (h *Handler) GetAudioSegment(c echo.Context) error { func (h *Handler) GetAudioSegment(c echo.Context) error {
resource := c.Param("resource")
slug := c.Param("slug")
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32) audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
if err != nil { if err != nil {
return err return err
@ -170,13 +149,11 @@ func (h *Handler) GetAudioSegment(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
client, err := GetClientId(c) client, err := GetClientId(c)
if err != nil { if err != nil {
return err return err
} }
path, err := GetPath(c)
path, err := GetPath(resource, slug)
if err != nil { if err != nil {
return err return err
} }
@ -192,12 +169,9 @@ func (h *Handler) GetAudioSegment(c echo.Context) error {
// //
// Identify metadata about a file. // Identify metadata about a file.
// //
// Path: /:resource/:slug/info // Path: /info
func (h *Handler) GetInfo(c echo.Context) error { func (h *Handler) GetInfo(c echo.Context) error {
resource := c.Param("resource") path, err := GetPath(c)
slug := c.Param("slug")
path, err := GetPath(resource, slug)
if err != nil { if err != nil {
return err return err
} }
@ -208,10 +182,10 @@ func (h *Handler) GetInfo(c echo.Context) error {
} }
// Run extractors to have them in cache // Run extractors to have them in cache
h.extractor.RunExtractor(ret.Path, ret.Sha, &ret.Subtitles) h.extractor.RunExtractor(ret.Path, ret.Sha, &ret.Subtitles)
go h.thumbnails.ExtractThumbnail( // go h.thumbnails.ExtractThumbnail(
ret.Path, // ret.Path,
fmt.Sprintf("%s/%s/thumbnails.png", resource, slug), // fmt.Sprintf("%s/%s/thumbnails.png", resource, slug),
) // )
return c.JSON(http.StatusOK, ret) return c.JSON(http.StatusOK, ret)
} }
@ -340,13 +314,13 @@ func main() {
thumbnails: src.NewThumbnailsCreator(), thumbnails: src.NewThumbnailsCreator(),
} }
e.GET("/:resource/:slug/direct", DirectStream) e.GET("/direct", DirectStream)
e.GET("/:resource/:slug/master.m3u8", h.GetMaster) e.GET("/master.m3u8", h.GetMaster)
e.GET("/:resource/:slug/:quality/index.m3u8", h.GetVideoIndex) e.GET("/:quality/index.m3u8", h.GetVideoIndex)
e.GET("/:resource/:slug/audio/:audio/index.m3u8", h.GetAudioIndex) e.GET("/audio/:audio/index.m3u8", h.GetAudioIndex)
e.GET("/:resource/:slug/:quality/:chunk", h.GetVideoSegment) e.GET("/:quality/:chunk", h.GetVideoSegment)
e.GET("/:resource/:slug/audio/:audio/:chunk", h.GetAudioSegment) e.GET("/audio/:audio/:chunk", h.GetAudioSegment)
e.GET("/:resource/:slug/info", h.GetInfo) e.GET("/info", h.GetInfo)
e.GET("/:resource/:slug/thumbnails.png", h.GetThumbnails) e.GET("/:resource/:slug/thumbnails.png", h.GetThumbnails)
e.GET("/:resource/:slug/thumbnails.vtt", h.GetThumbnailsVtt) e.GET("/:resource/:slug/thumbnails.vtt", h.GetThumbnailsVtt)
e.GET("/:sha/attachment/:name", h.GetAttachment) e.GET("/:sha/attachment/:name", h.GetAttachment)

View File

@ -1,11 +1,8 @@
package main package main
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os"
"strings" "strings"
"time" "time"
@ -18,42 +15,12 @@ type Item struct {
Path string `json:"path"` Path string `json:"path"`
} }
func GetPath(resource string, slug string) (string, error) { func GetPath(c echo.Context) (string, error) {
url := os.Getenv("API_URL") key := c.Request().Header.Get("X-Path")
if url == "" {
url = "http://back:5000"
}
key := os.Getenv("KYOO_APIKEYS")
if key == "" { if key == "" {
return "", errors.New("missing api keys") return "", echo.NewHTTPError(http.StatusBadRequest, "Missing resouce path. You need to set the X-Path header.")
} }
key = strings.Split(key, ",")[0] return key, nil
req, err := http.NewRequest("GET", strings.Join([]string{url, resource, slug}, "/"), nil)
if err != nil {
return "", err
}
req.Header.Set("X-API-KEY", key)
res, err := client.Do(req)
if err != nil {
return "", err
}
if res.StatusCode != 200 {
return "", echo.NewHTTPError(
http.StatusNotFound,
fmt.Sprintf("No %s found with the slug %s.", resource, slug),
)
}
defer res.Body.Close()
ret := Item{}
err = json.NewDecoder(res.Body).Decode(&ret)
if err != nil {
return "", err
}
return ret.Path, nil
} }
func SanitizePath(path string) error { func SanitizePath(path string) error {