mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add get info route and video parsing
This commit is contained in:
parent
45091da5ac
commit
5c83162a29
@ -21,6 +21,7 @@ in
|
||||
go
|
||||
wgo
|
||||
mediainfo
|
||||
libmediainfo
|
||||
ffmpeg
|
||||
postgresql_15
|
||||
eslint_d
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
|
@ -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
166
transcoder/src/info.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user