Add get info route and video parsing

This commit is contained in:
Zoe Roux 2024-01-12 22:43:04 +01:00
parent 45091da5ac
commit 5c83162a29
9 changed files with 227 additions and 22 deletions

View File

@ -21,6 +21,7 @@ in
go
wgo
mediainfo
libmediainfo
ffmpeg
postgresql_15
eslint_d

View File

@ -1,4 +1,5 @@
FROM golang:1.20-alpine
RUN apk add --no-cache build-base ffmpeg libmediainfo-dev
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

View File

@ -1,4 +1,5 @@
FROM golang:1.20-alpine
RUN apk add --no-cache build-base ffmpeg libmediainfo-dev
RUN go install github.com/bokwoon95/wgo@latest
WORKDIR /app

View File

@ -2,14 +2,16 @@ module github.com/zoriya/kyoo/transcoder
go 1.20
require github.com/labstack/echo/v4 v4.11.4 // direct
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/labstack/echo/v4 v4.11.4 // direct
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/zelenin/go-mediainfo v1.0.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect

View File

@ -13,6 +13,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/zelenin/go-mediainfo v1.0.0 h1:P0rKGyrSwKBCj37Ul7TLkrhf5OVBPlh3fFe9ZqjiaKQ=
github.com/zelenin/go-mediainfo v1.0.0/go.mod h1:6jxR5y1gDozFdCD7NYq/kUjCo5nqeogYCal6wpv81nY=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=

View File

@ -54,6 +54,27 @@ func (h *Handler) GetMaster(c echo.Context) error {
return c.String(http.StatusOK, ret)
}
// Identify
//
// # Identify metadata about a file
//
// Path: /:resource/:slug/info
func GetInfo(c echo.Context) error {
resource := c.Param("resource")
slug := c.Param("slug")
path, err := GetPath(resource, slug)
if err != nil {
return err
}
ret, err := src.GetInfo(path)
if err != nil {
return err
}
return c.JSON(http.StatusOK, ret)
}
type Handler struct {
transcoder *src.Transcoder
}
@ -67,6 +88,7 @@ func main() {
e.GET("/:resource/:slug/direct", DirectStream)
e.GET("/:resource/:slug/master.m3u8", h.GetMaster)
e.GET("/:resource/:slug/info", GetInfo)
e.Logger.Fatal(e.Start(":7666"))
}

166
transcoder/src/info.go Normal file
View File

@ -0,0 +1,166 @@
package src
import (
"crypto/sha1"
"encoding/hex"
"path/filepath"
"strconv"
"github.com/zelenin/go-mediainfo"
)
type MediaInfo struct {
// The sha1 of the video file.
Sha string `json:"sha"`
/// The internal path of the video file.
Path string `json:"path"`
/// The extension currently used to store this video file
Extension string `json:"extension"`
/// The length of the media in seconds.
Length float32 `json:"length"`
/// The container of the video file of this episode.
Container string `json:"container"`
/// The video codec and infromations.
Video Video `json:"video"`
/// The list of audio tracks.
Audios []Audio `json:"audios"`
/// The list of subtitles tracks.
Subtitles []Subtitle `json:"subtitles"`
/// The list of fonts that can be used to display subtitles.
Fonts []string `json:"fonts"`
/// The list of chapters. See Chapter for more information.
Chapters []Chapter `json:"chapters"`
}
type Video struct {
/// The codec of this stream (defined as the RFC 6381).
Codec string `json:"codec"`
/// The language of this stream (as a ISO-639-2 language code)
Language string `json:"language,omitempty"`
/// The max quality of this video track.
Quality Quality `json:"quality"`
/// The width of the video stream
Width uint32 `json:"width"`
/// The height of the video stream
Height uint32 `json:"height"`
/// The average bitrate of the video in bytes/s
Bitrate uint32 `json:"bitrate"`
}
type Audio struct {
/// The index of this track on the media.
Index uint32 `json:"index"`
/// The title of the stream.
Title string `json:"title,omitempty"`
/// The language of this stream (as a ISO-639-2 language code)
Language string `json:"language,omitempty"`
/// The codec of this stream.
Codec string `json:"codec"`
/// Is this stream the default one of it's type?
IsDefault bool `json:"isDefault"`
/// Is this stream tagged as forced? (useful only for subtitles)
IsForced bool `json:"isForced"`
}
type Subtitle struct {
/// The index of this track on the media.
Index uint32 `json:"index"`
/// The title of the stream.
Title string `json:"title,omitempty"`
/// The language of this stream (as a ISO-639-2 language code)
Language string `json:"language,omitempty"`
/// The codec of this stream.
Codec string `json:"codec"`
/// The extension for the codec.
Extension string `json:"extension,omitempty"`
/// Is this stream the default one of it's type?
IsDefault bool `json:"isDefault"`
/// Is this stream tagged as forced? (useful only for subtitles)
IsForced bool `json:"isForced"`
/// The link to access this subtitle.
Link string `json:"link,omitempty"`
}
type Chapter struct {
/// The start time of the chapter (in second from the start of the episode).
StartTime float32 `json:"startTime"`
/// The end time of the chapter (in second from the start of the episode).
EndTime float32 `json:"endTime"`
/// The name of this chapter. This should be a human-readable name that could be presented to the user.
Name string `json:"name"`
// TODO: add a type field for Opening, Credits...
}
func ParseFloat(str string) float32 {
f, err := strconv.ParseFloat(str, 32)
if err != nil {
panic(err)
}
return float32(f)
}
func ParseUint(str string) uint32 {
i, err := strconv.ParseUint(str, 10, 32)
if err != nil {
panic(err)
}
return uint32(i)
}
// Stolen from the cmp.Or code that is not yet released
// Or returns the first of its arguments that is not equal to the zero value.
// If no argument is non-zero, it returns the zero value.
func Or[T comparable](vals ...T) T {
var zero T
for _, val := range vals {
if val != zero {
return val
}
}
return zero
}
func GetInfo(path string) (MediaInfo, error) {
mi, err := mediainfo.Open(path)
if err != nil {
return MediaInfo{}, err
}
defer mi.Close()
// TODO: extract
sha := mi.Parameter(mediainfo.StreamGeneral, 0, "UniqueID")
// Remove dummy values that some tools use.
if len(sha) <= 5 {
date := mi.Parameter(mediainfo.StreamGeneral, 0, "File_Modified_Date")
h := sha1.New()
h.Write([]byte(path))
h.Write([]byte(date))
sha = hex.EncodeToString(h.Sum(nil))
}
return MediaInfo{
Sha: sha,
Path: path,
// Remove leading .
Extension: filepath.Ext(path)[1:],
// convert seconds to ms
Length: ParseFloat(mi.Parameter(mediainfo.StreamGeneral, 0, "Duration")) / 1000,
Container: mi.Parameter(mediainfo.StreamGeneral, 0, "Format"),
Video: Video{
// This codec is not in the right format (does not include bitdepth...).
Codec: mi.Parameter(mediainfo.StreamVideo, 0, "Format"),
Language: mi.Parameter(mediainfo.StreamVideo, 0, "Language"),
Quality: QualityFromHeight(ParseUint(mi.Parameter(mediainfo.StreamVideo, 0, "Height"))),
Width: ParseUint(mi.Parameter(mediainfo.StreamVideo, 0, "Width")),
Height: ParseUint(mi.Parameter(mediainfo.StreamVideo, 0, "Height")),
Bitrate: ParseUint(
Or(
mi.Parameter(mediainfo.StreamVideo, 0, "BitRate"),
mi.Parameter(mediainfo.StreamVideo, 0, "OverallBitRate"),
),
),
},
}, nil
}

View File

@ -1,39 +1,49 @@
package src
type Quality int8
type Quality string
const (
P240 Quality = iota
P360
P480
P720
P1080
P1440
P4k
P8k
Original
P240 Quality = "240p"
P360 Quality = "360p"
P480 Quality = "480p"
P720 Quality = "720p"
P1080 Quality = "1080p"
P1440 Quality = "1440p"
P4k Quality = "4k"
P8k Quality = "8k"
Original Quality = "original"
)
func (q Quality) String() string {
func (q Quality) Height() uint32 {
switch q {
case P240:
return "240p"
return 240
case P360:
return "360p"
return 360
case P480:
return "480p"
return 480
case P720:
return "720p"
return 720
case P1080:
return "1080p"
return 1080
case P1440:
return "1440p"
return 1440
case P4k:
return "4k"
return 2160
case P8k:
return "8k"
return 4320
case Original:
return "Original"
panic("Original quality must be handled specially")
}
panic("Invalid quality value")
}
func QualityFromHeight(height uint32) Quality {
qualities := []Quality{P240, P360, P480, P720, P1080, P1440, P4k, P8k, Original}
for _, quality := range qualities {
if quality.Height() >= height {
return quality
}
}
return P240
}

View File

@ -41,7 +41,7 @@ func GetPath(resource string, slug string) (string, error) {
if res.StatusCode != 200 {
return "", echo.NewHTTPError(
http.StatusNotAcceptable,
http.StatusNotFound,
fmt.Sprintf("No %s found with the slug %s.", resource, slug),
)
}