diff --git a/transcoder/migrations/000001_init_db.up.sql b/transcoder/migrations/000001_init_db.up.sql index 06b2c08d..df33b222 100644 --- a/transcoder/migrations/000001_init_db.up.sql +++ b/transcoder/migrations/000001_init_db.up.sql @@ -8,11 +8,11 @@ create table info( size bigint not null, duration real not null, container varchar(256), - fonts text[], - ver_ffprobe integer, - ver_extract integer, - ver_thumbs integer, - ver_keyframes integer + fonts text[] not null, + ver_info integer not null, + ver_extract integer not null, + ver_thumbs integer not null, + ver_keyframes integer not null ); create table videos( diff --git a/transcoder/src/info.go b/transcoder/src/info.go index a9857984..59990f2a 100644 --- a/transcoder/src/info.go +++ b/transcoder/src/info.go @@ -20,6 +20,15 @@ import ( "gopkg.in/vansante/go-ffprobe.v2" ) +const InfoVersion = 1 + +type Versions struct { + Info int32 `db:"ver_info"` + Extract int32 `db:"ver_extract"` + Thumbs int32 `db:"ver_thumbs"` + Keyframes int32 `db:"ver_keyframes"` +} + type MediaInfo struct { // The sha1 of the video file. Sha string `json:"sha"` @@ -28,15 +37,19 @@ type MediaInfo struct { /// The extension currently used to store this video file Extension string `json:"extension"` /// The whole mimetype (defined as the RFC 6381). ex: `video/mp4; codecs="avc1.640028, mp4a.40.2"` - MimeCodec *string `json:"mimeCodec"` + MimeCodec *string `json:"mimeCodec" db:"mime_codec"` /// The file size of the video file. Size int64 `json:"size"` /// The length of the media in seconds. Duration float32 `json:"duration"` /// The container of the video file of this episode. Container *string `json:"container"` - /// The video codec and informations. - Video *Video `json:"video"` + /// Version of the metadata. This can be used to invalidate older metadata from db if the extraction code has changed. + Versions Versions + + // TODO: remove this + Video *Video + /// The list of videos if there are multiples. Videos []Video `json:"videos"` /// The list of audio tracks. @@ -50,14 +63,16 @@ type MediaInfo struct { } type Video struct { - /// The human readable codec name. - Codec string `json:"codec"` - /// The codec of this stream (defined as the RFC 6381). - MimeCodec *string `json:"mimeCodec"` + /// The index of this track on the media. + Index uint32 `json:"index" db:"idx"` /// The title of the stream. Title *string `json:"title"` /// The language of this stream (as a ISO-639-2 language code) Language *string `json:"language"` + /// The human readable codec name. + Codec string `json:"codec"` + /// The codec of this stream (defined as the RFC 6381). + MimeCodec *string `json:"mimeCodec" db:"mime_codec"` /// The max quality of this video track. Quality Quality `json:"quality"` /// The width of the video stream @@ -70,7 +85,7 @@ type Video struct { type Audio struct { /// The index of this track on the media. - Index uint32 `json:"index"` + Index uint32 `json:"index" db:"idx"` /// The title of the stream. Title *string `json:"title"` /// The language of this stream (as a IETF-BCP-47 language code) @@ -78,16 +93,14 @@ type Audio struct { /// The human readable codec name. Codec string `json:"codec"` /// The codec of this stream (defined as the RFC 6381). - MimeCodec *string `json:"mimeCodec"` + MimeCodec *string `json:"mimeCodec" db:"mime_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"` + IsDefault bool `json:"isDefault" db:"is_default"` } type Subtitle struct { /// The index of this track on the media. - Index uint32 `json:"index"` + Index uint32 `json:"index" db:"idx"` /// The title of the stream. Title *string `json:"title"` /// The language of this stream (as a IETF-BCP-47 language code) @@ -97,23 +110,38 @@ type Subtitle struct { /// The extension for the codec. Extension *string `json:"extension"` /// 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"` + IsDefault bool `json:"isDefault" db:"is_default"` + /// Is this stream tagged as forced? + IsForced bool `json:"isForced" db:"is_forced"` + /// Is this an external subtitle (as in stored in a different file) + IsExternal bool `json:"isExternal" db:"is_external"` + /// Where the subtitle is stored (either in library if IsExternal is true or in transcoder cache if false) + /// Null if the subtitle can't be extracted (unsupported format) + Path *string `json:"path"` /// The link to access this subtitle. Link *string `json:"link"` } type Chapter struct { /// The start time of the chapter (in second from the start of the episode). - StartTime float32 `json:"startTime"` + StartTime float32 `json:"startTime" db:"start_time"` /// The end time of the chapter (in second from the start of the episode). - EndTime float32 `json:"endTime"` + EndTime float32 `json:"endTime" db:"end_time"` /// 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... + /// The type value is used to mark special chapters (openning/credits...) + Type ChapterType } +type ChapterType string + +const ( + Content ChapterType = "content" + Recap ChapterType = "recap" + Intro ChapterType = "intro" + Credits ChapterType = "credits" +) + func ParseFloat(str string) float32 { f, err := strconv.ParseFloat(str, 32) if err != nil { @@ -209,7 +237,7 @@ func GetInfo(path string, sha string) (*MediaInfo, error) { } var val *MediaInfo - val, err = getInfo(path) + val, err = RetriveMediaInfo(path, sha) if err == nil { *mi.info = *val mi.info.Sha = sha @@ -247,7 +275,7 @@ func saveInfo[T any](save_path string, mi *T) error { return os.WriteFile(save_path, content, 0o644) } -func getInfo(path string) (*MediaInfo, error) { +func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) { defer printExecTime("mediainfo for %s", path)() ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second) @@ -259,15 +287,23 @@ func getInfo(path string) (*MediaInfo, error) { } ret := MediaInfo{ + Sha: sha, Path: path, // Remove leading . Extension: filepath.Ext(path)[1:], Size: ParseInt64(mi.Format.Size), Duration: float32(mi.Format.DurationSeconds), Container: OrNull(mi.Format.FormatName), + Versions: Versions{ + Info: InfoVersion, + Extract: 0, + Thumbs: 0, + Keyframes: 0, + }, Videos: MapStream(mi.Streams, ffprobe.StreamVideo, func(stream *ffprobe.Stream, i uint32) Video { lang, _ := language.Parse(stream.Tags.Language) return Video{ + Index: i, Codec: stream.CodecName, MimeCodec: GetMimeCodec(stream), Title: OrNull(stream.Tags.Title), @@ -289,15 +325,15 @@ func getInfo(path string) (*MediaInfo, error) { Codec: stream.CodecName, MimeCodec: GetMimeCodec(stream), IsDefault: stream.Disposition.Default != 0, - IsForced: stream.Disposition.Forced != 0, } }), Subtitles: MapStream(mi.Streams, ffprobe.StreamSubtitle, func(stream *ffprobe.Stream, i uint32) Subtitle { extension := OrNull(SubtitleExtensions[stream.CodecName]) - var link *string + var link string + var path string if extension != nil { - x := fmt.Sprintf("%s/%s/subtitle/%d.%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), i, *extension) - link = &x + link = fmt.Sprintf("%s/%s/subtitle/%d.%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), i, *extension) + path = fmt.Sprintf("%s/%s/sub/%d.%s", Settings.Metadata, sha, i, extension) } lang, _ := language.Parse(stream.Tags.Language) return Subtitle{ @@ -308,7 +344,8 @@ func getInfo(path string) (*MediaInfo, error) { Extension: extension, IsDefault: stream.Disposition.Default != 0, IsForced: stream.Disposition.Forced != 0, - Link: link, + Link: &link, + Path: &path, } }), Chapters: Map(mi.Chapters, func(c *ffprobe.Chapter, _ int) Chapter { @@ -316,6 +353,8 @@ func getInfo(path string) (*MediaInfo, error) { Name: c.Title(), StartTime: float32(c.StartTimeSeconds), EndTime: float32(c.EndTimeSeconds), + // TODO: detect content type + Type: Content, } }), Fonts: MapStream(mi.Streams, ffprobe.StreamAttachment, func(stream *ffprobe.Stream, i uint32) string { diff --git a/transcoder/utils.go b/transcoder/utils.go index a143cd78..bdd85ce2 100644 --- a/transcoder/utils.go +++ b/transcoder/utils.go @@ -14,11 +14,6 @@ import ( "github.com/zoriya/kyoo/transcoder/src" ) -// Encode the version in the hash path to update cached values. -// Older versions won't be deleted (needed to allow multiples versions of the transcoder to run at the same time) -// If the version changes a lot, we might want to automatically delete older versions. -var version = "v3-" - func GetPath(c echo.Context) (string, string, error) { key := c.Param("path") if key == "" {