fix(requests): bound external id hydration

This commit is contained in:
Silo Server Migration
2026-05-24 23:04:04 -04:00
parent e470760ab4
commit 8a8ed6998d
2 changed files with 103 additions and 1 deletions
+46 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/Silo-Server/silo-server/internal/idgen"
"github.com/Silo-Server/silo-server/internal/metadata/tmdb"
"golang.org/x/sync/errgroup"
)
type TMDBClient interface {
@@ -22,6 +23,8 @@ type TMDBExternalIDClient interface {
GetExternalIDs(ctx context.Context, mediaType string, id int) (*tmdb.ExternalIDs, error)
}
const externalIDHydrationConcurrency = 4
type SecretResolver interface {
Get(ctx context.Context, key string) (string, error)
}
@@ -733,6 +736,44 @@ func (s *Service) hydratePresenceCandidate(ctx context.Context, mediaType MediaT
return candidate
}
func (s *Service) hydratePresenceCandidates(ctx context.Context, mediaType MediaType, candidates []PresenceCandidate) []PresenceCandidate {
if len(candidates) == 0 {
return candidates
}
if _, ok := s.tmdb.(TMDBExternalIDClient); !ok {
return candidates
}
hydrated := append([]PresenceCandidate(nil), candidates...)
if externalIDHydrationConcurrency <= 1 {
for i := range hydrated {
if ctx.Err() != nil {
return hydrated
}
hydrated[i] = s.hydratePresenceCandidate(ctx, mediaType, hydrated[i])
}
return hydrated
}
group, groupCtx := errgroup.WithContext(ctx)
group.SetLimit(externalIDHydrationConcurrency)
for i := range hydrated {
if groupCtx.Err() != nil {
break
}
i := i
group.Go(func() error {
if err := groupCtx.Err(); err != nil {
return err
}
hydrated[i] = s.hydratePresenceCandidate(groupCtx, mediaType, hydrated[i])
return nil
})
}
_ = group.Wait()
return hydrated
}
func tmdbMediaType(mediaType MediaType) string {
if mediaType == MediaTypeSeries {
return "tv"
@@ -741,12 +782,16 @@ func tmdbMediaType(mediaType MediaType) string {
}
func (s *Service) lookupAvailable(ctx context.Context, mediaType MediaType, ids []int) (map[int]bool, error) {
if s.presence == nil {
return map[int]bool{}, nil
}
candidates := make([]PresenceCandidate, 0, len(ids))
for _, id := range ids {
if id > 0 {
candidates = append(candidates, s.hydratePresenceCandidate(ctx, mediaType, PresenceCandidate{TMDBID: id}))
candidates = append(candidates, PresenceCandidate{TMDBID: id})
}
}
candidates = s.hydratePresenceCandidates(ctx, mediaType, candidates)
matches, err := s.lookupPresence(ctx, mediaType, candidates)
if err != nil {
return nil, err
+57
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"strings"
"sync"
"testing"
"time"
@@ -309,6 +310,59 @@ func TestSearchMarksSeriesAvailableByHydratedTVDBID(t *testing.T) {
}
}
func TestSearchWithNilPresenceDoesNotHydrateExternalIDs(t *testing.T) {
store := newFakeStore()
store.settings.RequestsEnabled = true
tmdbClient := &fakeTMDBClient{page: &tmdb.MediaPage{Results: []tmdb.MediaResult{{
ID: 201992,
MediaType: "series",
Title: "The Rookie: Feds",
}}}}
service := NewService(store, tmdbClient, nil)
_, err := service.Search(context.Background(), testViewer(1), "rookie feds", MediaTypeSeries, 1)
if err != nil {
t.Fatalf("Search returned error: %v", err)
}
if len(tmdbClient.externalIDCalls) != 0 {
t.Fatalf("external ID calls = %v, want none", tmdbClient.externalIDCalls)
}
}
func TestSearchHydratesMultipleResultsBeforePresenceLookup(t *testing.T) {
store := newFakeStore()
store.settings.RequestsEnabled = true
tmdbClient := &fakeTMDBClient{
page: &tmdb.MediaPage{Results: []tmdb.MediaResult{
{ID: 201992, MediaType: "series", Title: "The Rookie: Feds"},
{ID: 1399, MediaType: "series", Title: "Game of Thrones"},
}},
externalIDsByID: map[int]*tmdb.ExternalIDs{
201992: {TVDBID: 420105, IMDbID: "tt18076310"},
1399: {TVDBID: 121361, IMDbID: "tt0944947"},
},
}
presence := &fakePresence{}
service := NewService(store, tmdbClient, presence)
_, err := service.Search(context.Background(), testViewer(1), "series", MediaTypeSeries, 1)
if err != nil {
t.Fatalf("Search returned error: %v", err)
}
if len(presence.got) != 2 {
t.Fatalf("presence candidates = %d, want 2", len(presence.got))
}
got := map[int]int{}
for _, candidate := range presence.got {
if candidate.TVDBID != nil {
got[candidate.TMDBID] = *candidate.TVDBID
}
}
if got[201992] != 420105 || got[1399] != 121361 {
t.Fatalf("hydrated tvdb ids = %+v", got)
}
}
func TestSearchEnrichmentHidesOtherRequesterID(t *testing.T) {
store := newFakeStore()
store.settings.RequestsEnabled = true
@@ -963,6 +1017,7 @@ func (f *fakePresence) LookupTMDB(_ context.Context, mediaType MediaType, ids []
}
type fakeTMDBClient struct {
mu sync.Mutex
page *tmdb.MediaPage
externalIDs *tmdb.ExternalIDs
externalIDsByID map[int]*tmdb.ExternalIDs
@@ -993,7 +1048,9 @@ func (f *fakeTMDBClient) DiscoverPage(context.Context, string, tmdb.DiscoverPara
}
func (f *fakeTMDBClient) GetExternalIDs(_ context.Context, _ string, id int) (*tmdb.ExternalIDs, error) {
f.mu.Lock()
f.externalIDCalls = append(f.externalIDCalls, id)
f.mu.Unlock()
if f.externalIDsByID != nil {
return f.externalIDsByID[id], nil
}