- Wire introdb marker fetching into playback and Jellyfin compat - Persist and expose recap/preview markers alongside intro and credits - Add new playback/profile settings for recap and preview behavior
117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
package introdb
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/markers"
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
)
|
|
|
|
// Provider implements markers.Provider against the TheIntroDB API.
|
|
// Movies are looked up by TMDB or IMDB ID; episodes additionally need a
|
|
// season and episode number. The provider returns at most one Marker per
|
|
// segment kind — when TheIntroDB has multiple candidate ranges (multiple
|
|
// release versions), we pick the first usable one.
|
|
type Provider struct {
|
|
client *Client
|
|
}
|
|
|
|
// NewProvider constructs a Provider backed by the supplied client. Pass an
|
|
// already-configured *Client (from NewClient) so callers control the API
|
|
// key, base URL, and lifecycle.
|
|
func NewProvider(client *Client) *Provider {
|
|
return &Provider{client: client}
|
|
}
|
|
|
|
// ID satisfies markers.Provider; it returns the canonical provider tag
|
|
// stored in *_markers_provider columns and emitted in telemetry.
|
|
func (p *Provider) ID() string { return ProviderID }
|
|
|
|
// FetchMarkers issues a single GET /v3/media call and converts the
|
|
// response into a markers.Result. A nil Provider (or one with no usable
|
|
// IDs) returns an empty result rather than an error so callers can
|
|
// chain providers via the Registry.
|
|
func (p *Provider) FetchMarkers(ctx context.Context, req markers.Request) (markers.Result, error) {
|
|
if p == nil || p.client == nil {
|
|
return markers.Result{}, nil
|
|
}
|
|
|
|
tmdbID := strings.TrimSpace(req.ExternalIDs[markers.ExternalIDKeyTMDB])
|
|
imdbID := strings.TrimSpace(req.ExternalIDs[markers.ExternalIDKeyIMDB])
|
|
if tmdbID == "" && imdbID == "" {
|
|
return markers.Result{}, nil
|
|
}
|
|
|
|
durationMS := int64(req.Duration / time.Millisecond)
|
|
|
|
var resp *mediaResponse
|
|
var err error
|
|
switch req.Kind {
|
|
case markers.ItemKindEpisode:
|
|
if req.SeasonNumber <= 0 || req.EpisodeNumber <= 0 {
|
|
return markers.Result{}, nil
|
|
}
|
|
resp, err = p.client.FetchEpisode(ctx, tmdbID, imdbID, req.SeasonNumber, req.EpisodeNumber, durationMS)
|
|
case markers.ItemKindMovie:
|
|
resp, err = p.client.FetchMovie(ctx, tmdbID, imdbID, durationMS)
|
|
default:
|
|
return markers.Result{}, nil
|
|
}
|
|
if err != nil {
|
|
return markers.Result{}, err
|
|
}
|
|
if resp == nil {
|
|
return markers.Result{}, nil
|
|
}
|
|
|
|
result := markers.Result{
|
|
ProviderID: ProviderID,
|
|
SourceClass: models.MarkerSourceOnline,
|
|
Algorithm: Algorithm,
|
|
}
|
|
if m, ok := pickMarker(resp.Intro, markers.MarkerKindIntro, req.Duration, true); ok {
|
|
result.Markers = append(result.Markers, m)
|
|
}
|
|
if m, ok := pickMarker(resp.Credits, markers.MarkerKindCredits, req.Duration, false); ok {
|
|
result.Markers = append(result.Markers, m)
|
|
}
|
|
if m, ok := pickMarker(resp.Recap, markers.MarkerKindRecap, req.Duration, true); ok {
|
|
result.Markers = append(result.Markers, m)
|
|
}
|
|
if m, ok := pickMarker(resp.Preview, markers.MarkerKindPreview, req.Duration, false); ok {
|
|
result.Markers = append(result.Markers, m)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// pickMarker selects the first usable segment from a TheIntroDB response
|
|
// array. `requireEnd` is true for segments where the end timestamp is the
|
|
// load-bearing field (intro, recap) — they're allowed to start at 0 if
|
|
// `start_ms` is omitted. For trailing segments (credits, preview) the
|
|
// start is required but the end defaults to the file duration.
|
|
func pickMarker(stamps []segmentTimestamps, kind markers.MarkerKind, totalDuration time.Duration, requireEnd bool) (markers.Marker, bool) {
|
|
for _, s := range stamps {
|
|
start := time.Duration(0)
|
|
end := totalDuration
|
|
if s.StartMs != nil {
|
|
start = time.Duration(*s.StartMs) * time.Millisecond
|
|
}
|
|
if s.EndMs != nil {
|
|
end = time.Duration(*s.EndMs) * time.Millisecond
|
|
}
|
|
if requireEnd && s.EndMs == nil {
|
|
continue
|
|
}
|
|
if !requireEnd && s.StartMs == nil {
|
|
continue
|
|
}
|
|
if end <= start {
|
|
continue
|
|
}
|
|
return markers.Marker{Kind: kind, Start: start, End: end, Confidence: 0.9}, true
|
|
}
|
|
return markers.Marker{}, false
|
|
}
|