diff --git a/transcoder/migrations/000005_fingerprints.down.sql b/transcoder/migrations/000005_fingerprints.down.sql new file mode 100644 index 00000000..42ebcd05 --- /dev/null +++ b/transcoder/migrations/000005_fingerprints.down.sql @@ -0,0 +1,9 @@ +begin; + +alter table gocoder.chapters drop column match_accuracy; +alter table gocoder.chapters drop column fingerprint_id; +drop table gocoder.chapterprints; +drop table gocoder.fingerprints; +alter table gocoder.info drop column ver_fingerprint; + +commit; diff --git a/transcoder/migrations/000005_fingerprints.up.sql b/transcoder/migrations/000005_fingerprints.up.sql new file mode 100644 index 00000000..e95c45f5 --- /dev/null +++ b/transcoder/migrations/000005_fingerprints.up.sql @@ -0,0 +1,19 @@ +begin; + +alter table gocoder.info add column ver_fingerprint integer not null default 0; + +create table gocoder.fingerprints( + id integer not null primary key references gocoder.info(id) on delete cascade, + start_data text not null, + end_data text not null +); + +create table gocoder.chapterprints( + id serial primary key, + data text not null +); + +alter table gocoder.chapters add column fingerprint_id integer references gocoder.chapterprints(id) on delete set null; +alter table gocoder.chapters add column match_accuracy integer; + +commit; diff --git a/transcoder/src/api/metadata.go b/transcoder/src/api/metadata.go index a314fc19..473fd2ee 100644 --- a/transcoder/src/api/metadata.go +++ b/transcoder/src/api/metadata.go @@ -23,7 +23,7 @@ func RegisterMetadataHandlers(e *echo.Group, metadata *src.MetadataService) { h := mhandler{metadata} e.GET("/:path/info", h.GetInfo) - e.GET("/:path/prepare", h.Prepare) + e.POST("/:path/prepare", h.Prepare) e.GET("/:path/subtitle/:name", h.GetSubtitle) e.GET("/:path/attachment/:name", h.GetAttachment) e.GET("/:path/thumbnails.png", h.GetThumbnails) @@ -62,10 +62,11 @@ func (h *mhandler) GetInfo(c *echo.Context) error { Container: nil, MimeCodec: nil, Versions: src.Versions{ - Info: -1, - Extract: 0, - Thumbs: 0, - Keyframes: 0, + Info: -1, + Extract: 0, + Thumbs: 0, + Keyframes: 0, + Fingerprint: 0, }, Videos: make([]src.Video, 0), Audios: make([]src.Audio, 0), @@ -77,22 +78,34 @@ func (h *mhandler) GetInfo(c *echo.Context) error { return c.JSON(http.StatusOK, ret) } +type PrepareRequest struct { + // File path of the previous/next episodes (for audio fingerprinting). + NearEpisodes *string `json:"nearEpisodes"` +} + // @Summary Prepare metadata // -// @Description Starts metadata preparation in background (info, extract, thumbs, keyframes). +// @Description Starts metadata preparation in background (info, extract, thumbs, keyframes, chapter identification). // // @Tags metadata // @Param path path string true "Base64 of a video's path" format(base64) example(L3ZpZGVvL2J1YmJsZS5ta3YK) +// @Param body body PrepareRequest false "Adjacent episode paths for chapter detection" // // @Success 202 "Preparation started" -// @Router /:path/prepare [get] +// @Router /:path/prepare [post] func (h *mhandler) Prepare(c *echo.Context) error { path, sha, err := getPath(c) if err != nil { return err } - go func(path string, sha string) { + var req PrepareRequest + err = c.Bind(&req) + if err != nil { + return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error()) + } + + go func() { bgCtx := context.Background() info, err := h.metadata.GetMetadata(bgCtx, path, sha) @@ -113,7 +126,9 @@ func (h *mhandler) Prepare(c *echo.Context) error { fmt.Printf("failed to extract audio keyframes for %s (stream %d): %v\n", path, audio.Index, err) } } - }(path, sha) + + h.metadata.IdentifyChapters(bgCtx, info, req.NearEpisodes) + }() return c.NoContent(http.StatusAccepted) } diff --git a/transcoder/src/info.go b/transcoder/src/info.go index 5e166602..9e5c1b4b 100644 --- a/transcoder/src/info.go +++ b/transcoder/src/info.go @@ -20,10 +20,11 @@ import ( const InfoVersion = 4 type Versions struct { - Info int32 `json:"info" db:"ver_info"` - Extract int32 `json:"extract" db:"ver_extract"` - Thumbs int32 `json:"thumbs" db:"ver_thumbs"` - Keyframes int32 `json:"keyframes" db:"ver_keyframes"` + Info int32 `json:"info" db:"ver_info"` + Extract int32 `json:"extract" db:"ver_extract"` + Thumbs int32 `json:"thumbs" db:"ver_thumbs"` + Keyframes int32 `json:"keyframes" db:"ver_keyframes"` + Fingerprint int32 `json:"fingerprint" db:"ver_fingerprint"` } type MediaInfo struct { @@ -149,6 +150,10 @@ type Chapter struct { Name string `json:"name" db:"name"` /// The type value is used to mark special chapters (openning/credits...) Type ChapterType `json:"type" db:"type"` + /// Reference to the chapterprint used for fingerprint matching. + FingerprintId *int32 `json:"-" db:"fingerprint_id"` + /// Accuracy of the fingerprint match (0-100). + MatchAccuracy *int32 `json:"matchAccuracy,omitempty" db:"match_accuracy"` } type ChapterType string @@ -255,10 +260,11 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) { Duration: mi.Format.DurationSeconds, Container: OrNull(mi.Format.FormatName), Versions: Versions{ - Info: InfoVersion, - Extract: 0, - Thumbs: 0, - Keyframes: 0, + Info: InfoVersion, + Extract: 0, + Thumbs: 0, + Keyframes: 0, + Fingerprint: 0, }, Videos: MapStream(mi.Streams, ffprobe.StreamVideo, func(stream *ffprobe.Stream, i uint32) Video { lang, _ := language.Parse(stream.Tags.Language) diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index 4b972e9e..c80a97c6 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -180,6 +180,19 @@ func (s *MetadataService) GetMetadata(ctx context.Context, path string, sha stri } } + if ret.Versions.Fingerprint < FingerprintVersion && ret.Versions.Fingerprint != 0 { + tx, err := s.Database.Begin(bgCtx) + if err != nil { + return nil, err + } + tx.Exec(bgCtx, `delete from gocoder.fingerprints where id = $1`, ret.Id) + tx.Exec(bgCtx, `update gocoder.info set ver_fingerprint = 0 where id = $1`, ret.Id) + err = tx.Commit(bgCtx) + if err != nil { + fmt.Printf("error deleting old fingerprints from database: %v", err) + } + } + return ret, nil } @@ -192,7 +205,8 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri 'info', i.ver_info, 'extract', i.ver_extract, 'thumbs', i.ver_thumbs, - 'keyframes', i.ver_keyframes + 'keyframes', i.ver_keyframes, + 'fingerprint', i.ver_fingerprint ) as versions from gocoder.info as i where i.sha=$1 limit 1`, @@ -286,13 +300,14 @@ func (s *MetadataService) storeFreshMetadata(ctx context.Context, path string, s err = tx.QueryRow(ctx, ` insert into gocoder.info(sha, path, extension, mime_codec, size, duration, container, - fonts, ver_info, ver_extract, ver_thumbs, ver_keyframes) - values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + fonts, ver_info, ver_extract, ver_thumbs, ver_keyframes, ver_fingerprint) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) returning id `, // on conflict do not update versions of extract/thumbs/keyframes ret.Sha, ret.Path, ret.Extension, ret.MimeCodec, ret.Size, ret.Duration, ret.Container, ret.Fonts, ret.Versions.Info, ret.Versions.Extract, ret.Versions.Thumbs, ret.Versions.Keyframes, + ret.Versions.Fingerprint, ).Scan(&ret.Id) if err != nil { return set(ret, fmt.Errorf("failed to insert info: %w", err))