fix(requests): hydrate external ids before availability
This commit is contained in:
@@ -22,7 +22,7 @@ type PresenceMatch struct {
|
||||
}
|
||||
|
||||
type PresenceResolver interface {
|
||||
LookupTMDB(ctx context.Context, mediaType MediaType, tmdbIDs []int) (map[int]bool, error)
|
||||
Lookup(ctx context.Context, mediaType MediaType, candidates []PresenceCandidate) (map[int]PresenceMatch, error)
|
||||
}
|
||||
|
||||
type presenceItemLookup interface {
|
||||
|
||||
@@ -247,11 +247,13 @@ func (s *Service) CreateRequest(ctx context.Context, viewer Viewer, input Create
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
available, err := s.lookupAvailable(ctx, normalized.MediaType, []int{normalized.TMDBID})
|
||||
s.enrichExternalIDs(ctx, &normalized)
|
||||
|
||||
matches, err := s.lookupPresence(ctx, normalized.MediaType, []PresenceCandidate{createPresenceCandidate(normalized)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if available[normalized.TMDBID] {
|
||||
if matches[normalized.TMDBID].Available {
|
||||
return nil, ErrAlreadyAvailable
|
||||
}
|
||||
|
||||
@@ -271,8 +273,6 @@ func (s *Service) CreateRequest(ctx context.Context, viewer Viewer, input Create
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.enrichExternalIDs(ctx, &normalized)
|
||||
|
||||
id, err := idgen.NextID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -672,11 +672,86 @@ func (s *Service) enrichPage(ctx context.Context, viewer Viewer, raw *tmdb.Media
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) lookupAvailable(ctx context.Context, mediaType MediaType, ids []int) (map[int]bool, error) {
|
||||
func (s *Service) lookupPresence(ctx context.Context, mediaType MediaType, candidates []PresenceCandidate) (map[int]PresenceMatch, error) {
|
||||
if s.presence == nil {
|
||||
return map[int]bool{}, nil
|
||||
return map[int]PresenceMatch{}, nil
|
||||
}
|
||||
return s.presence.LookupTMDB(ctx, mediaType, ids)
|
||||
return s.presence.Lookup(ctx, mediaType, candidates)
|
||||
}
|
||||
|
||||
func availabilityBoolMap(matches map[int]PresenceMatch) map[int]bool {
|
||||
out := map[int]bool{}
|
||||
for id, match := range matches {
|
||||
out[id] = match.Available
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func requestPresenceCandidate(req Request) PresenceCandidate {
|
||||
candidate := PresenceCandidate{
|
||||
TMDBID: req.TMDBID,
|
||||
IMDbID: strings.TrimSpace(req.IMDbID),
|
||||
}
|
||||
if req.TVDBID != nil && *req.TVDBID > 0 {
|
||||
tvdbID := *req.TVDBID
|
||||
candidate.TVDBID = &tvdbID
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
func createPresenceCandidate(input CreateRequestInput) PresenceCandidate {
|
||||
candidate := PresenceCandidate{
|
||||
TMDBID: input.TMDBID,
|
||||
IMDbID: strings.TrimSpace(input.IMDbID),
|
||||
}
|
||||
if input.TVDBID != nil && *input.TVDBID > 0 {
|
||||
tvdbID := *input.TVDBID
|
||||
candidate.TVDBID = &tvdbID
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
func (s *Service) hydratePresenceCandidate(ctx context.Context, mediaType MediaType, candidate PresenceCandidate) PresenceCandidate {
|
||||
if candidate.TMDBID <= 0 {
|
||||
return candidate
|
||||
}
|
||||
client, ok := s.tmdb.(TMDBExternalIDClient)
|
||||
if !ok {
|
||||
return candidate
|
||||
}
|
||||
externalIDs, err := client.GetExternalIDs(ctx, tmdbMediaType(mediaType), candidate.TMDBID)
|
||||
if err != nil || externalIDs == nil {
|
||||
return candidate
|
||||
}
|
||||
if candidate.IMDbID == "" {
|
||||
candidate.IMDbID = strings.TrimSpace(externalIDs.IMDbID)
|
||||
}
|
||||
if candidate.TVDBID == nil && externalIDs.TVDBID > 0 {
|
||||
tvdbID := externalIDs.TVDBID
|
||||
candidate.TVDBID = &tvdbID
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
func tmdbMediaType(mediaType MediaType) string {
|
||||
if mediaType == MediaTypeSeries {
|
||||
return "tv"
|
||||
}
|
||||
return "movie"
|
||||
}
|
||||
|
||||
func (s *Service) lookupAvailable(ctx context.Context, mediaType MediaType, ids []int) (map[int]bool, error) {
|
||||
candidates := make([]PresenceCandidate, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id > 0 {
|
||||
candidates = append(candidates, s.hydratePresenceCandidate(ctx, mediaType, PresenceCandidate{TMDBID: id}))
|
||||
}
|
||||
}
|
||||
matches, err := s.lookupPresence(ctx, mediaType, candidates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return availabilityBoolMap(matches), nil
|
||||
}
|
||||
|
||||
func (s *Service) enrichExternalIDs(ctx context.Context, input *CreateRequestInput) {
|
||||
@@ -687,11 +762,7 @@ func (s *Service) enrichExternalIDs(ctx context.Context, input *CreateRequestInp
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
mediaType := "movie"
|
||||
if input.MediaType == MediaTypeSeries {
|
||||
mediaType = "tv"
|
||||
}
|
||||
externalIDs, err := client.GetExternalIDs(ctx, mediaType, input.TMDBID)
|
||||
externalIDs, err := client.GetExternalIDs(ctx, tmdbMediaType(input.MediaType), input.TMDBID)
|
||||
if err != nil || externalIDs == nil {
|
||||
return
|
||||
}
|
||||
@@ -864,11 +935,11 @@ func (s *Service) reconcileRequest(ctx context.Context, req Request) (reconcileC
|
||||
}
|
||||
|
||||
func (s *Service) requestAvailable(ctx context.Context, req Request) (bool, error) {
|
||||
available, err := s.lookupAvailable(ctx, req.MediaType, []int{req.TMDBID})
|
||||
matches, err := s.lookupPresence(ctx, req.MediaType, []PresenceCandidate{requestPresenceCandidate(req)})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return available[req.TMDBID], nil
|
||||
return matches[req.TMDBID].Available, nil
|
||||
}
|
||||
|
||||
func (s *Service) checkFulfillmentStatus(ctx context.Context, req Request) (FulfillmentStatus, error) {
|
||||
|
||||
@@ -229,6 +229,28 @@ func TestCreateRequestEnrichesSeriesTVDBID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRequestBlocksWhenHydratedTVDBIDIsAvailable(t *testing.T) {
|
||||
store := newFakeStore()
|
||||
store.settings.RequestsEnabled = true
|
||||
tmdbClient := &fakeTMDBClient{externalIDs: &tmdb.ExternalIDs{TVDBID: 420105, IMDbID: "tt18076310"}}
|
||||
presence := &fakePresence{byTVDB: map[MediaType]map[int]int{
|
||||
MediaTypeSeries: {420105: 201992},
|
||||
}}
|
||||
service := NewService(store, tmdbClient, presence)
|
||||
|
||||
_, err := service.CreateRequest(context.Background(), testViewer(1), CreateRequestInput{
|
||||
MediaType: MediaTypeSeries,
|
||||
TMDBID: 201992,
|
||||
Title: "The Rookie: Feds",
|
||||
})
|
||||
if !errors.Is(err, ErrAlreadyAvailable) {
|
||||
t.Fatalf("err = %v, want ErrAlreadyAvailable", err)
|
||||
}
|
||||
if len(store.created) != 0 {
|
||||
t.Fatalf("created requests = %d, want 0", len(store.created))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRequestNoActiveDuplicateCreatesRequest(t *testing.T) {
|
||||
store := newFakeStore()
|
||||
store.settings.RequestsEnabled = true
|
||||
@@ -250,6 +272,43 @@ func TestCreateRequestNoActiveDuplicateCreatesRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchMarksSeriesAvailableByHydratedTVDBID(t *testing.T) {
|
||||
store := newFakeStore()
|
||||
store.settings.RequestsEnabled = true
|
||||
tmdbClient := &fakeTMDBClient{
|
||||
page: &tmdb.MediaPage{
|
||||
Page: 1,
|
||||
Results: []tmdb.MediaResult{{
|
||||
ID: 201992,
|
||||
MediaType: "series",
|
||||
Title: "The Rookie: Feds",
|
||||
Year: 2022,
|
||||
}},
|
||||
},
|
||||
externalIDsByID: map[int]*tmdb.ExternalIDs{
|
||||
201992: {TVDBID: 420105, IMDbID: "tt18076310"},
|
||||
},
|
||||
}
|
||||
presence := &fakePresence{byTVDB: map[MediaType]map[int]int{
|
||||
MediaTypeSeries: {420105: 201992},
|
||||
}}
|
||||
service := NewService(store, tmdbClient, presence)
|
||||
|
||||
page, err := service.Search(context.Background(), testViewer(1), "rookie feds", MediaTypeSeries, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Search returned error: %v", err)
|
||||
}
|
||||
if got := page.Results[0].Availability; got != AvailabilityAvailable {
|
||||
t.Fatalf("availability = %q, want available", got)
|
||||
}
|
||||
if page.Results[0].Request.Reason != "already_available" {
|
||||
t.Fatalf("request reason = %q, want already_available", page.Results[0].Request.Reason)
|
||||
}
|
||||
if len(presence.got) != 1 || presence.got[0].TVDBID == nil || *presence.got[0].TVDBID != 420105 {
|
||||
t.Fatalf("presence candidates = %+v, want hydrated tvdb id", presence.got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchEnrichmentHidesOtherRequesterID(t *testing.T) {
|
||||
store := newFakeStore()
|
||||
store.settings.RequestsEnabled = true
|
||||
@@ -395,6 +454,31 @@ func TestReconcileRequestsCompletesFromCatalogPresence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileRequestsCompletesByStoredTVDBID(t *testing.T) {
|
||||
store := newFakeStore()
|
||||
tvdbID := 420105
|
||||
store.candidates = []*Request{{
|
||||
ID: "req-1",
|
||||
MediaType: MediaTypeSeries,
|
||||
TMDBID: 201992,
|
||||
TVDBID: &tvdbID,
|
||||
Status: StatusQueued,
|
||||
Outcome: OutcomeActive,
|
||||
}}
|
||||
presence := &fakePresence{byTVDB: map[MediaType]map[int]int{
|
||||
MediaTypeSeries: {420105: 201992},
|
||||
}}
|
||||
service := NewService(store, &fakeTMDBClient{}, presence)
|
||||
|
||||
result, err := service.ReconcileRequests(context.Background(), 100)
|
||||
if err != nil {
|
||||
t.Fatalf("ReconcileRequests returned error: %v", err)
|
||||
}
|
||||
if result.Completed != 1 {
|
||||
t.Fatalf("completed = %d, want 1", result.Completed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileRequestsMarksDownloadingFromAdapter(t *testing.T) {
|
||||
store := newFakeStore()
|
||||
qualityProfileID := 1
|
||||
@@ -843,15 +927,35 @@ func TestBrowseGenreMovieReturnsResults(t *testing.T) {
|
||||
|
||||
type fakePresence struct {
|
||||
available map[MediaType]map[int]bool
|
||||
byTVDB map[MediaType]map[int]int
|
||||
got []PresenceCandidate
|
||||
}
|
||||
|
||||
func (f *fakePresence) Lookup(_ context.Context, mediaType MediaType, candidates []PresenceCandidate) (map[int]PresenceMatch, error) {
|
||||
out := map[int]PresenceMatch{}
|
||||
f.got = append(f.got, candidates...)
|
||||
for _, candidate := range candidates {
|
||||
if f.available != nil && f.available[mediaType][candidate.TMDBID] {
|
||||
out[candidate.TMDBID] = PresenceMatch{Available: true, MatchedProvider: "tmdb"}
|
||||
continue
|
||||
}
|
||||
if candidate.TVDBID != nil && f.byTVDB != nil {
|
||||
if tmdbID, ok := f.byTVDB[mediaType][*candidate.TVDBID]; ok && tmdbID == candidate.TMDBID {
|
||||
out[candidate.TMDBID] = PresenceMatch{Available: true, MatchedProvider: "tvdb"}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *fakePresence) LookupTMDB(_ context.Context, mediaType MediaType, ids []int) (map[int]bool, error) {
|
||||
out := map[int]bool{}
|
||||
if f.available == nil {
|
||||
return out, nil
|
||||
}
|
||||
for _, id := range ids {
|
||||
if f.available[mediaType][id] {
|
||||
matches, err := f.Lookup(context.Background(), mediaType, []PresenceCandidate{{TMDBID: id}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matches[id].Available {
|
||||
out[id] = true
|
||||
}
|
||||
}
|
||||
@@ -861,6 +965,8 @@ func (f *fakePresence) LookupTMDB(_ context.Context, mediaType MediaType, ids []
|
||||
type fakeTMDBClient struct {
|
||||
page *tmdb.MediaPage
|
||||
externalIDs *tmdb.ExternalIDs
|
||||
externalIDsByID map[int]*tmdb.ExternalIDs
|
||||
externalIDCalls []int
|
||||
detail *tmdb.MediaDetail
|
||||
discoverPage *tmdb.MediaPage
|
||||
discoverErr error
|
||||
@@ -886,7 +992,11 @@ func (f *fakeTMDBClient) DiscoverPage(context.Context, string, tmdb.DiscoverPara
|
||||
return &tmdb.MediaPage{Results: []tmdb.MediaResult{}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeTMDBClient) GetExternalIDs(context.Context, string, int) (*tmdb.ExternalIDs, error) {
|
||||
func (f *fakeTMDBClient) GetExternalIDs(_ context.Context, _ string, id int) (*tmdb.ExternalIDs, error) {
|
||||
f.externalIDCalls = append(f.externalIDCalls, id)
|
||||
if f.externalIDsByID != nil {
|
||||
return f.externalIDsByID[id], nil
|
||||
}
|
||||
return f.externalIDs, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user