- 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
163 lines
4.8 KiB
Go
163 lines
4.8 KiB
Go
package markers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// ExternalIDs is the resolved identity for a media file at marker-fetch
|
|
// time. It bundles the external IDs needed by online providers (TMDB,
|
|
// IMDB, TVDB) with the kind-specific extras (season/episode for
|
|
// episodes). The zero value carries no IDs and signals "unresolvable".
|
|
type ExternalIDs struct {
|
|
Kind ItemKind
|
|
TmdbID string
|
|
ImdbID string
|
|
TvdbID string
|
|
SeasonNumber int
|
|
EpisodeNumber int
|
|
}
|
|
|
|
// HasAnyID reports whether at least one usable external identifier is
|
|
// present. Providers should refuse to issue requests when this is false.
|
|
func (e ExternalIDs) HasAnyID() bool {
|
|
return e.TmdbID != "" || e.ImdbID != "" || e.TvdbID != ""
|
|
}
|
|
|
|
// AsRequestMap exposes the external IDs in the Request.ExternalIDs shape.
|
|
func (e ExternalIDs) AsRequestMap() map[string]string {
|
|
out := make(map[string]string, 3)
|
|
if e.TmdbID != "" {
|
|
out[ExternalIDKeyTMDB] = e.TmdbID
|
|
}
|
|
if e.ImdbID != "" {
|
|
out[ExternalIDKeyIMDB] = e.ImdbID
|
|
}
|
|
if e.TvdbID != "" {
|
|
out[ExternalIDKeyTVDB] = e.TvdbID
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ExternalIDResolver maps an internal media file to the external IDs an
|
|
// online marker provider can query against. Implementations must handle
|
|
// both episodes (joined through episodes -> series) and movies (joined
|
|
// through media_items directly).
|
|
type ExternalIDResolver interface {
|
|
ResolveForFile(ctx context.Context, file *models.MediaFile) (ExternalIDs, error)
|
|
}
|
|
|
|
// DBExternalIDResolver issues a single query per file against the postgres
|
|
// pool. Episodes pull from `episodes`; movies fall back to `media_items`
|
|
// via the file's content_id.
|
|
type DBExternalIDResolver struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
// NewDBExternalIDResolver constructs a resolver backed by the supplied pool.
|
|
func NewDBExternalIDResolver(pool *pgxpool.Pool) *DBExternalIDResolver {
|
|
return &DBExternalIDResolver{pool: pool}
|
|
}
|
|
|
|
// ResolveForFile fetches the external IDs for the given file. Returns the
|
|
// zero ExternalIDs and a nil error when the file cannot be resolved (e.g.
|
|
// unmatched media); callers should treat that as "no online lookup possible".
|
|
func (r *DBExternalIDResolver) ResolveForFile(ctx context.Context, file *models.MediaFile) (ExternalIDs, error) {
|
|
if r == nil || r.pool == nil || file == nil {
|
|
return ExternalIDs{}, nil
|
|
}
|
|
|
|
episodeID := strings.TrimSpace(file.EpisodeID)
|
|
contentID := strings.TrimSpace(file.ContentID)
|
|
|
|
if episodeID != "" {
|
|
// TheIntroDB indexes episode markers by show + season/episode, so we
|
|
// prefer the series-level external IDs and fall back to the episode
|
|
// row only if the series isn't matched yet. The episode row's
|
|
// season/episode numbers are authoritative; the media_files copy
|
|
// can drift during multi-version moves.
|
|
var epTmdb, epImdb, epTvdb, showTmdb, showImdb, showTvdb string
|
|
var season, episode int
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT COALESCE(NULLIF(e.tmdb_id, ''), ''),
|
|
COALESCE(NULLIF(e.imdb_id, ''), ''),
|
|
COALESCE(NULLIF(e.tvdb_id, ''), ''),
|
|
COALESCE(e.season_number, 0),
|
|
COALESCE(e.episode_number, 0),
|
|
COALESCE(NULLIF(mi.tmdb_id, ''), ''),
|
|
COALESCE(NULLIF(mi.imdb_id, ''), ''),
|
|
COALESCE(NULLIF(mi.tvdb_id, ''), '')
|
|
FROM episodes e
|
|
LEFT JOIN media_items mi ON mi.content_id = e.series_id
|
|
WHERE e.content_id = $1`, episodeID).Scan(
|
|
&epTmdb, &epImdb, &epTvdb, &season, &episode,
|
|
&showTmdb, &showImdb, &showTvdb,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ExternalIDs{}, nil
|
|
}
|
|
return ExternalIDs{}, fmt.Errorf("resolve episode external ids: %w", err)
|
|
}
|
|
tmdb := showTmdb
|
|
if tmdb == "" {
|
|
tmdb = epTmdb
|
|
}
|
|
imdb := showImdb
|
|
if imdb == "" {
|
|
imdb = epImdb
|
|
}
|
|
tvdb := showTvdb
|
|
if tvdb == "" {
|
|
tvdb = epTvdb
|
|
}
|
|
if season <= 0 {
|
|
season = file.SeasonNumber
|
|
}
|
|
if episode <= 0 {
|
|
episode = file.EpisodeNumber
|
|
}
|
|
return ExternalIDs{
|
|
Kind: ItemKindEpisode,
|
|
TmdbID: tmdb,
|
|
ImdbID: imdb,
|
|
TvdbID: tvdb,
|
|
SeasonNumber: season,
|
|
EpisodeNumber: episode,
|
|
}, nil
|
|
}
|
|
|
|
if contentID == "" {
|
|
return ExternalIDs{}, nil
|
|
}
|
|
|
|
var tmdb, imdb, tvdb, itemType string
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT COALESCE(NULLIF(tmdb_id, ''), ''),
|
|
COALESCE(NULLIF(imdb_id, ''), ''),
|
|
COALESCE(NULLIF(tvdb_id, ''), ''),
|
|
COALESCE(type, '')
|
|
FROM media_items WHERE content_id = $1`, contentID).Scan(&tmdb, &imdb, &tvdb, &itemType)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ExternalIDs{}, nil
|
|
}
|
|
return ExternalIDs{}, fmt.Errorf("resolve movie external ids: %w", err)
|
|
}
|
|
if itemType != "movie" {
|
|
return ExternalIDs{}, nil
|
|
}
|
|
return ExternalIDs{
|
|
Kind: ItemKindMovie,
|
|
TmdbID: tmdb,
|
|
ImdbID: imdb,
|
|
TvdbID: tvdb,
|
|
}, nil
|
|
}
|