mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-31 14:33:50 -04:00
Move metadata routes to it's own router
This commit is contained in:
parent
5d414bea16
commit
c340a9b559
@ -3,11 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
_ "github.com/zoriya/kyoo/transcoder/docs"
|
||||
|
||||
@ -40,175 +36,6 @@ func ErrorHandler(err error, c echo.Context) {
|
||||
}{Errors: []string{message}})
|
||||
}
|
||||
|
||||
// Identify
|
||||
//
|
||||
// Identify metadata about a file.
|
||||
//
|
||||
// Path: /:path/info
|
||||
func (h *Handler) GetInfo(c echo.Context) error {
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := h.metadata.GetMetadata(c.Request().Context(), path, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ret.SearchExternalSubtitles()
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't find external subtitles: %v", err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
// Get attachments
|
||||
//
|
||||
// Get a specific attachment.
|
||||
//
|
||||
// Path: /:path/attachment/:name
|
||||
func (h *Handler) GetAttachment(c echo.Context) (err error) {
|
||||
_, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := c.Param("name")
|
||||
if err := SanitizePath(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attachementStream, err := h.metadata.GetAttachment(c.Request().Context(), sha, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, attachementStream.Close, "failed to close attachment reader")
|
||||
|
||||
mimeType, err := guessMimeType(name, attachementStream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to guess mime type: %w", err)
|
||||
}
|
||||
|
||||
return c.Stream(200, mimeType, attachementStream)
|
||||
}
|
||||
|
||||
// Get subtitle
|
||||
//
|
||||
// Get a specific subtitle.
|
||||
//
|
||||
// Path: /:path/subtitle/:name
|
||||
func (h *Handler) GetSubtitle(c echo.Context) (err error) {
|
||||
_, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := c.Param("name")
|
||||
if err := SanitizePath(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subtitleStream, err := h.metadata.GetSubtitle(c.Request().Context(), sha, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, subtitleStream.Close, "failed to close subtitle reader")
|
||||
|
||||
mimeType, err := guessMimeType(name, subtitleStream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to guess mime type: %w", err)
|
||||
}
|
||||
|
||||
// Default the mime type to text/plain if it is not recognized
|
||||
if mimeType == "" {
|
||||
mimeType = "text/plain"
|
||||
}
|
||||
|
||||
return c.Stream(200, mimeType, subtitleStream)
|
||||
}
|
||||
|
||||
// Get thumbnail sprite
|
||||
//
|
||||
// Get a sprite file containing all the thumbnails of the show.
|
||||
//
|
||||
// Path: /:path/thumbnails.png
|
||||
func (h *Handler) GetThumbnails(c echo.Context) (err error) {
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sprite, err := h.metadata.GetThumbSprite(c.Request().Context(), path, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, sprite.Close, "failed to close thumbnail sprite reader")
|
||||
|
||||
return c.Stream(200, "image/png", sprite)
|
||||
}
|
||||
|
||||
// Get thumbnail vtt
|
||||
//
|
||||
// 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: /:path/:resource/:slug/thumbnails.vtt
|
||||
func (h *Handler) GetThumbnailsVtt(c echo.Context) (err error) {
|
||||
path, sha, err := GetPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtt, err := h.metadata.GetThumbVtt(c.Request().Context(), path, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, vtt.Close, "failed to close thumbnail vtt reader")
|
||||
|
||||
return c.Stream(200, "text/vtt", vtt)
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
transcoder *src.Transcoder
|
||||
metadata *src.MetadataService
|
||||
}
|
||||
|
||||
// Try to guess the mime type of a file based on its extension.
|
||||
// If the extension is not recognized, return an empty string.
|
||||
// If path is provided, it should contain a file extension (i.e. ".mp4").
|
||||
// If content is provided, it should be of type io.ReadSeeker. Instances of other types are ignored.
|
||||
// This implementation is based upon http.ServeContent.
|
||||
func guessMimeType(path string, content any) (string, error) {
|
||||
// This does not match a large number of different types that are likely in use.
|
||||
// TODO add telemetry to see what file extensions are used, then add logic
|
||||
// to detect the type based on the file extension.
|
||||
mimeType := ""
|
||||
|
||||
// First check the file extension, if there is one.
|
||||
ext := filepath.Ext(path)
|
||||
if ext != "" {
|
||||
if mimeType = mime.TypeByExtension(ext); mimeType != "" {
|
||||
return mimeType, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try reading the first few bytes of the file to guess the mime type.
|
||||
// Only do this if seeking is supported
|
||||
if reader, ok := content.(io.ReadSeeker); ok {
|
||||
// 512 bytes is the most that DetectContentType will consider, so no
|
||||
// need to read more than that.
|
||||
var buf [512]byte
|
||||
n, _ := io.ReadFull(reader, buf[:])
|
||||
mimeType = http.DetectContentType(buf[:n])
|
||||
|
||||
// Reset the reader to the beginning of the file
|
||||
_, err := reader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("mime type guesser failed to seek to beginning of file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return mimeType, nil
|
||||
}
|
||||
|
||||
// @title gocoder - Kyoo's transcoder
|
||||
// @version 1.0
|
||||
// @description Real time transcoder.
|
||||
@ -248,11 +75,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
h := Handler{
|
||||
transcoder: transcoder,
|
||||
metadata: metadata,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@ -278,13 +100,9 @@ func main() {
|
||||
// return keys.LookupKeyID(kid.(string))
|
||||
},
|
||||
}))
|
||||
g.GET("/:path/info", h.GetInfo)
|
||||
g.GET("/:path/thumbnails.png", h.GetThumbnails)
|
||||
g.GET("/:path/thumbnails.vtt", h.GetThumbnailsVtt)
|
||||
g.GET("/:path/attachment/:name", h.GetAttachment)
|
||||
g.GET("/:path/subtitle/:name", h.GetSubtitle)
|
||||
|
||||
api.RegisterStreamHandlers(g)
|
||||
api.RegisterStreamHandlers(g, transcoder)
|
||||
api.RegisterMetadataHandlers(g, metadata)
|
||||
api.RegisterPProfHandlers(e)
|
||||
|
||||
e.Logger.Fatal(e.Start(":7666"))
|
||||
|
213
transcoder/src/api/metadata.go
Normal file
213
transcoder/src/api/metadata.go
Normal file
@ -0,0 +1,213 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/zoriya/kyoo/transcoder/src"
|
||||
"github.com/zoriya/kyoo/transcoder/src/utils"
|
||||
)
|
||||
|
||||
type mhandler struct {
|
||||
metadata *src.MetadataService
|
||||
}
|
||||
|
||||
func RegisterMetadataHandlers(e *echo.Group, metadata *src.MetadataService) {
|
||||
h := mhandler{metadata}
|
||||
|
||||
e.GET("/:path/info", h.GetInfo)
|
||||
e.GET("/:path/subtitle/:name", h.GetSubtitle)
|
||||
e.GET("/:path/attachment/:name", h.GetAttachment)
|
||||
e.GET("/:path/thumbnails.png", h.GetThumbnails)
|
||||
e.GET("/:path/thumbnails.vtt", h.GetThumbnailsVtt)
|
||||
}
|
||||
|
||||
// @Summary Identify
|
||||
//
|
||||
// @Description Identify metadata about a file.
|
||||
//
|
||||
// @Tags metadata
|
||||
// @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK)
|
||||
//
|
||||
// @Success 200 {object} src.MediaInfo "Metadata info of the video."
|
||||
// @Router /:path/info [get]
|
||||
func (h *mhandler) GetInfo(c echo.Context) error {
|
||||
path, sha, err := getPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := h.metadata.GetMetadata(c.Request().Context(), path, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ret.SearchExternalSubtitles()
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't find external subtitles: %v", err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
// @Summary Get subtitle
|
||||
//
|
||||
// @Description Get a specific subtitle.
|
||||
//
|
||||
// @Tags metadata
|
||||
// @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK)
|
||||
// @Param name path string true "Name of the subtitle" example(en.srt)
|
||||
//
|
||||
// @Success 200 file "Requested subtitle"
|
||||
// @Router /:path/subtitle/:name [get]
|
||||
func (h *mhandler) GetSubtitle(c echo.Context) (err error) {
|
||||
_, sha, err := getPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := c.Param("name")
|
||||
if err := sanitizePath(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subtitleStream, err := h.metadata.GetSubtitle(c.Request().Context(), sha, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, subtitleStream.Close, "failed to close subtitle reader")
|
||||
|
||||
mimeType, err := guessMimeType(name, subtitleStream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to guess mime type: %w", err)
|
||||
}
|
||||
|
||||
// Default the mime type to text/plain if it is not recognized
|
||||
if mimeType == "" {
|
||||
mimeType = "text/plain"
|
||||
}
|
||||
|
||||
return c.Stream(200, mimeType, subtitleStream)
|
||||
}
|
||||
|
||||
// @Summary Get attachments
|
||||
//
|
||||
// @Description Get a specific attachment.
|
||||
//
|
||||
// @Tags metadata
|
||||
// @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK)
|
||||
// @Param name path string true "Name of the attachment" example(font.ttf)
|
||||
//
|
||||
// @Success 200 file "Requested attachment"
|
||||
// @Router /:path/attachment/:name [get]
|
||||
func (h *mhandler) GetAttachment(c echo.Context) (err error) {
|
||||
_, sha, err := getPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := c.Param("name")
|
||||
if err := sanitizePath(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attachementStream, err := h.metadata.GetAttachment(c.Request().Context(), sha, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, attachementStream.Close, "failed to close attachment reader")
|
||||
|
||||
mimeType, err := guessMimeType(name, attachementStream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to guess mime type: %w", err)
|
||||
}
|
||||
|
||||
return c.Stream(200, mimeType, attachementStream)
|
||||
}
|
||||
|
||||
// Try to guess the mime type of a file based on its extension.
|
||||
// If the extension is not recognized, return an empty string.
|
||||
// If path is provided, it should contain a file extension (i.e. ".mp4").
|
||||
// If content is provided, it should be of type io.ReadSeeker. Instances of other types are ignored.
|
||||
// This implementation is based upon http.ServeContent.
|
||||
func guessMimeType(path string, content any) (string, error) {
|
||||
// This does not match a large number of different types that are likely in use.
|
||||
// TODO add telemetry to see what file extensions are used, then add logic
|
||||
// to detect the type based on the file extension.
|
||||
mimeType := ""
|
||||
|
||||
// First check the file extension, if there is one.
|
||||
ext := filepath.Ext(path)
|
||||
if ext != "" {
|
||||
if mimeType = mime.TypeByExtension(ext); mimeType != "" {
|
||||
return mimeType, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try reading the first few bytes of the file to guess the mime type.
|
||||
// Only do this if seeking is supported
|
||||
if reader, ok := content.(io.ReadSeeker); ok {
|
||||
// 512 bytes is the most that DetectContentType will consider, so no
|
||||
// need to read more than that.
|
||||
var buf [512]byte
|
||||
n, _ := io.ReadFull(reader, buf[:])
|
||||
mimeType = http.DetectContentType(buf[:n])
|
||||
|
||||
// Reset the reader to the beginning of the file
|
||||
_, err := reader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("mime type guesser failed to seek to beginning of file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return mimeType, nil
|
||||
}
|
||||
|
||||
// @Summary Get thumbnail sprite
|
||||
//
|
||||
// @Description Get a sprite file containing all the thumbnails of the show.
|
||||
//
|
||||
// @Tags metadata
|
||||
// @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK)
|
||||
//
|
||||
// @Success 200 file "sprite"
|
||||
// @Router /:path/thumbnails.png [get]
|
||||
func (h *mhandler) GetThumbnails(c echo.Context) (err error) {
|
||||
path, sha, err := getPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sprite, err := h.metadata.GetThumbSprite(c.Request().Context(), path, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, sprite.Close, "failed to close thumbnail sprite reader")
|
||||
|
||||
return c.Stream(200, "image/png", sprite)
|
||||
}
|
||||
|
||||
// @Summary Get thumbnail vtt
|
||||
//
|
||||
// @Description Get a vtt file containing timing/position of thumbnails inside the sprite file.
|
||||
// @Description https://developer.bitmovin.com/playback/docs/webvtt-based-thumbnails for more info.
|
||||
//
|
||||
// @Tags metadata
|
||||
// @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK)
|
||||
//
|
||||
// @Success 200 file "sprite"
|
||||
// @Router /:path/thumbnails.vtt [get]
|
||||
func (h *mhandler) GetThumbnailsVtt(c echo.Context) (err error) {
|
||||
path, sha, err := getPath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtt, err := h.metadata.GetThumbVtt(c.Request().Context(), path, sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.CleanupWithErr(&err, vtt.Close, "failed to close thumbnail vtt reader")
|
||||
|
||||
return c.Stream(200, "text/vtt", vtt)
|
||||
}
|
57
transcoder/src/api/path.go
Normal file
57
transcoder/src/api/path.go
Normal file
@ -0,0 +1,57 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"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
|
||||
}
|
@ -1,15 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/zoriya/kyoo/transcoder/src"
|
||||
@ -222,49 +216,6 @@ func (h *shandler) GetAudioSegment(c echo.Context) error {
|
||||
return c.File(ret)
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
|
Loading…
x
Reference in New Issue
Block a user