Files
silo-server/internal/markers/introdb/provider.go
T
Silo Server Migration 04a7e8c82d Add online marker support for recap and preview segments
- 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
2026-05-23 23:48:25 -04:00

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
}