mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-05-20 14:22:47 -04:00
Add swagger for transcoder & move routes to router module
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/zoriya/kyoo/transcoder/src"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
transcoder *src.Transcoder
|
||||
}
|
||||
|
||||
// @Summary Direct video
|
||||
//
|
||||
// @Description Retrieve the raw video stream, in the same container as the one on the server.
|
||||
// @Description No transcoding or transmuxing is done.
|
||||
//
|
||||
// @Tags streams
|
||||
// @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK)
|
||||
// @Param identifier path string false "anything, this can be used for the automatic file name when downloading from the browser" example(bubble.mkv)
|
||||
//
|
||||
// @Success 206 file "Video file (supports byte-requests)"
|
||||
// @Router /:path/direct [get]
|
||||
func DirectStream(c echo.Context) error {
|
||||
path, _, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.File(path)
|
||||
}
|
||||
|
||||
// @Summary Get master playlist
|
||||
//
|
||||
// @Description Get a master playlist containing all possible video qualities and audios available for this resource.
|
||||
// @Description Note that the direct stream is missing (since the direct is not an hls stream) and
|
||||
// @Description subtitles/fonts are not included to support more codecs than just webvtt.
|
||||
//
|
||||
// @Tags streams
|
||||
// @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK)
|
||||
//
|
||||
// @Success 200 file "Master playlist with all available stream qualities"
|
||||
// @Router /:path/master.m3u8 [get]
|
||||
func (h *handler) GetMaster(c echo.Context) error {
|
||||
client, err := GetClientId(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := h.transcoder.GetMaster(c.Request().Context(), path, client, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.String(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
// Transcode video
|
||||
//
|
||||
// Transcode the video to the selected quality.
|
||||
// This route can take a few seconds to respond since it will way for at least one segment to be
|
||||
// available.
|
||||
//
|
||||
// Path: /:path/:video/:quality/index.m3u8
|
||||
//
|
||||
// PRIVATE ROUTE (not documented in swagger, can change at any time)
|
||||
// Only reached via the master.m3u8.
|
||||
func (h *handler) GetVideoIndex(c echo.Context) error {
|
||||
video, err := strconv.ParseInt(c.Param("video"), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
quality, err := src.QualityFromString(c.Param("quality"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := GetClientId(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := h.transcoder.GetVideoIndex(c.Request().Context(), path, uint32(video), quality, client, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.String(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
// Transcode audio
|
||||
//
|
||||
// Get the selected audio
|
||||
// This route can take a few seconds to respond since it will way for at least one segment to be
|
||||
// available.
|
||||
//
|
||||
// Path: /:path/audio/:audio/index.m3u8
|
||||
//
|
||||
// PRIVATE ROUTE (not documented in swagger, can change at any time)
|
||||
// Only reached via the master.m3u8.
|
||||
func (h *handler) GetAudioIndex(c echo.Context) error {
|
||||
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := GetClientId(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := h.transcoder.GetAudioIndex(c.Request().Context(), path, uint32(audio), client, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.String(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
// Get transmuxed chunk
|
||||
//
|
||||
// Retrieve a chunk of a transmuxed video.
|
||||
//
|
||||
// Path: /:path/:video/:quality/segments-:chunk.ts
|
||||
//
|
||||
// PRIVATE ROUTE (not documented in swagger, can change at any time)
|
||||
// Only reached via the master.m3u8.
|
||||
func (h *handler) GetVideoSegment(c echo.Context) error {
|
||||
video, err := strconv.ParseInt(c.Param("video"), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
quality, err := src.QualityFromString(c.Param("quality"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
segment, err := ParseSegment(c.Param("chunk"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := GetClientId(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := h.transcoder.GetVideoSegment(
|
||||
c.Request().Context(),
|
||||
path,
|
||||
uint32(video),
|
||||
quality,
|
||||
segment,
|
||||
client,
|
||||
sha,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.File(ret)
|
||||
}
|
||||
|
||||
// Get audio chunk
|
||||
//
|
||||
// Retrieve a chunk of a transcoded audio.
|
||||
//
|
||||
// Path: /:path/audio/:audio/segments-:chunk.ts
|
||||
//
|
||||
// PRIVATE ROUTE (not documented in swagger, can change at any time)
|
||||
// Only reached via the master.m3u8.
|
||||
func (h *handler) GetAudioSegment(c echo.Context) error {
|
||||
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
segment, err := ParseSegment(c.Param("chunk"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := GetClientId(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := h.transcoder.GetAudioSegment(c.Request().Context(), path, uint32(audio), segment, client, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.File(ret)
|
||||
}
|
||||
|
||||
func RegisterStreamHandlers(e *echo.Group, transcoder *src.Transcoder) {
|
||||
h := handler{transcoder}
|
||||
|
||||
e.GET("/:path/direct", DirectStream)
|
||||
e.GET("/:path/direct/:identifier", DirectStream)
|
||||
e.GET("/:path/master.m3u8", h.GetMaster)
|
||||
e.GET("/:path/:video/:quality/index.m3u8", h.GetVideoIndex)
|
||||
e.GET("/:path/audio/:audio/index.m3u8", h.GetAudioIndex)
|
||||
e.GET("/:path/:video/:quality/:chunk", h.GetVideoSegment)
|
||||
e.GET("/:path/audio/:audio/:chunk", h.GetAudioSegment)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/zoriya/kyoo/transcoder/src"
|
||||
)
|
||||
|
||||
func GetPath(c echo.Context) (string, string, error) {
|
||||
key := c.Param("path")
|
||||
if key == "" {
|
||||
return "", "", echo.NewHTTPError(http.StatusBadRequest, "Missing resouce path.")
|
||||
}
|
||||
pathb, err := base64.RawURLEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return "", "", echo.NewHTTPError(http.StatusBadRequest, "Invalid path. Should be base64url (without padding) encoded.")
|
||||
}
|
||||
path := filepath.Clean(string(pathb))
|
||||
if !filepath.IsAbs(path) {
|
||||
return "", "", echo.NewHTTPError(http.StatusBadRequest, "Absolute path required.")
|
||||
}
|
||||
if !strings.HasPrefix(path, src.Settings.SafePath) {
|
||||
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 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
|
||||
}
|
||||
|
||||
func SanitizePath(path string) error {
|
||||
if strings.Contains(path, "/") || strings.Contains(path, "..") {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid parameter. Can't contains path delimiters or ..")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetClientId(c echo.Context) (string, error) {
|
||||
key := c.Request().Header.Get("X-CLIENT-ID")
|
||||
if key == "" {
|
||||
return "", echo.NewHTTPError(http.StatusBadRequest, "missing client id. Please specify the X-CLIENT-ID header to a guid constant for the lifetime of the player (but unique per instance)")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func ParseSegment(segment string) (int32, error) {
|
||||
var ret int32
|
||||
_, err := fmt.Sscanf(segment, "segment-%d.ts", &ret)
|
||||
if err != nil {
|
||||
return 0, echo.NewHTTPError(http.StatusBadRequest, "Could not parse segment.")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ErrorHandler(err error, c echo.Context) {
|
||||
code := http.StatusInternalServerError
|
||||
var message string
|
||||
if he, ok := err.(*echo.HTTPError); ok {
|
||||
code = he.Code
|
||||
message = fmt.Sprint(he.Message)
|
||||
} else {
|
||||
c.Logger().Error(err)
|
||||
message = "Internal server error"
|
||||
}
|
||||
c.JSON(code, struct {
|
||||
Errors []string `json:"errors"`
|
||||
}{Errors: []string{message}})
|
||||
}
|
||||
@@ -14,9 +14,11 @@ func GetEnvOr(env string, def string) string {
|
||||
}
|
||||
|
||||
type SettingsT struct {
|
||||
Outpath string
|
||||
SafePath string
|
||||
HwAccel HwAccelT
|
||||
Outpath string
|
||||
SafePath string
|
||||
JwksUrl string
|
||||
JwtIssuer string
|
||||
HwAccel HwAccelT
|
||||
}
|
||||
|
||||
type HwAccelT struct {
|
||||
@@ -29,7 +31,9 @@ type HwAccelT struct {
|
||||
|
||||
var Settings = SettingsT{
|
||||
// we manually add a folder to make sure we do not delete user data.
|
||||
Outpath: path.Join(GetEnvOr("GOCODER_CACHE_ROOT", "/cache"), "kyoo_cache"),
|
||||
SafePath: GetEnvOr("GOCODER_SAFE_PATH", "/video"),
|
||||
HwAccel: DetectHardwareAccel(),
|
||||
Outpath: path.Join(GetEnvOr("GOCODER_CACHE_ROOT", "/cache"), "kyoo_cache"),
|
||||
SafePath: GetEnvOr("GOCODER_SAFE_PATH", "/video"),
|
||||
JwksUrl: GetEnvOr("JWKS_URL", "http://auth:4568/.well-known/jwks.json"),
|
||||
JwtIssuer: GetEnvOr("JWT_ISSUER", "http://localhost:8901"),
|
||||
HwAccel: DetectHardwareAccel(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user