Files
silo-server/internal/metadata/plugin_provider_test.go
54cfc48ce3 fix(metadata): improve localized series matching (#490)
* fix(metadata): improve localized series matching

* test(metadata): strengthen consensus regressions

* fix(metadata): harden localized matching

---------

Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
2026-07-28 21:02:51 -04:00

440 lines
14 KiB
Go

package metadata
import (
"context"
"reflect"
"testing"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/structpb"
pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1"
)
const (
pluginInstallationIDSetting = "plugin_installation_id"
capabilityIDSetting = "capability_id"
)
type fakePluginMetadataClient struct {
searchResponse *pluginv1.SearchMetadataResponse
response *pluginv1.GetMetadataResponse
imagesResponse *pluginv1.GetImagesResponse
seasonsResp *pluginv1.GetSeasonsResponse
episodesResp *pluginv1.GetEpisodesResponse
searchReq *pluginv1.SearchMetadataRequest
getMetadataReq *pluginv1.GetMetadataRequest
getImagesReq *pluginv1.GetImagesRequest
getSeasonsReq *pluginv1.GetSeasonsRequest
getEpisodesReq *pluginv1.GetEpisodesRequest
}
func (f *fakePluginMetadataClient) Search(_ context.Context, req *pluginv1.SearchMetadataRequest) (*pluginv1.SearchMetadataResponse, error) {
f.searchReq = req
return f.searchResponse, nil
}
func (f *fakePluginMetadataClient) GetMetadata(_ context.Context, req *pluginv1.GetMetadataRequest) (*pluginv1.GetMetadataResponse, error) {
f.getMetadataReq = req
return f.response, nil
}
func (f *fakePluginMetadataClient) GetSeasons(_ context.Context, req *pluginv1.GetSeasonsRequest) (*pluginv1.GetSeasonsResponse, error) {
f.getSeasonsReq = req
return f.seasonsResp, nil
}
func (f *fakePluginMetadataClient) GetEpisodes(_ context.Context, req *pluginv1.GetEpisodesRequest) (*pluginv1.GetEpisodesResponse, error) {
f.getEpisodesReq = req
return f.episodesResp, nil
}
func (f *fakePluginMetadataClient) GetImages(_ context.Context, req *pluginv1.GetImagesRequest) (*pluginv1.GetImagesResponse, error) {
f.getImagesReq = req
return f.imagesResponse, nil
}
func (f *fakePluginMetadataClient) GetPersonDetail(_ context.Context, _ *pluginv1.GetPersonDetailRequest) (*pluginv1.GetPersonDetailResponse, error) {
return nil, nil
}
func (f *fakePluginMetadataClient) ResolveImageURL(context.Context, *pluginv1.ResolveImageURLRequest) (*pluginv1.ResolveImageURLResponse, error) {
return nil, nil
}
func (f *fakePluginMetadataClient) ResolveImageURLs(context.Context, *pluginv1.ResolveImageURLsRequest) (*pluginv1.ResolveImageURLsResponse, error) {
return nil, nil
}
func setPluginMetadataItemReleaseDate(t *testing.T, item *pluginv1.MetadataItem, value string) {
t.Helper()
field := item.ProtoReflect().Descriptor().Fields().ByName("release_date")
if field == nil {
t.Fatal("MetadataItem descriptor is missing release_date")
}
item.ProtoReflect().Set(field, protoreflect.ValueOfString(value))
}
//nolint:goconst // Keep the production-shaped protobuf fixture readable in place.
func TestPluginProviderSearch_MapsLocalizedTitlesAndCrossReferences(t *testing.T) {
const localizedJapanesePrimaryTitle = "十兵衛ちゃん -ラブリー眼帯の秘密-"
client := &fakePluginMetadataClient{
searchResponse: &pluginv1.SearchMetadataResponse{Results: []*pluginv1.ProviderSearchResult{
{
ProviderId: localizedTVDBID,
ItemType: anchoredItemTypeSeries,
Title: localizedJapanesePrimaryTitle,
OriginalTitle: localizedOriginalTitle,
Year: 1999,
TitleLanguage: "ja",
TitleIsFallback: true,
OriginalLanguage: "ja",
TitleAliases: []*pluginv1.TitleAlias{
{Title: localizedEnglishTitle, Language: "en", Kind: testAlternateAliasKind},
},
ProviderIds: mustStructFromStringMap(t, map[string]string{
testTMDBProvider: localizedTMDBID,
testIMDBProvider: "tt0213338",
}),
},
}},
}
provider, err := NewPluginProviderWithClientFactory(map[string]string{
pluginInstallationIDSetting: "1",
capabilityIDSetting: testTVDBProvider,
}, func(context.Context, int, string) (pluginMetadataClient, error) {
return client, nil
})
if err != nil {
t.Fatalf("NewPluginProviderWithClientFactory() error = %v", err)
}
results, err := provider.Search(context.Background(), SearchQuery{
Title: localizedEnglishTitle,
ContentType: anchoredItemTypeSeries,
Language: "en",
})
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if client.searchReq == nil || client.searchReq.GetQuery() != localizedEnglishTitle || client.searchReq.GetItemType() != anchoredItemTypeSeries || client.searchReq.GetLanguage() != "en" {
t.Fatalf("captured search request = %+v", client.searchReq)
}
if len(results) != 1 {
t.Fatalf("Search() returned %d results, want 1", len(results))
}
got := results[0]
if got.Name != localizedJapanesePrimaryTitle || got.OriginalTitle != localizedOriginalTitle || got.Year != 1999 {
t.Fatalf("localized identity fields = %+v", got)
}
if got.TitleLanguage != "ja" || !got.TitleIsFallback || got.OriginalLanguage != "ja" {
t.Fatalf("language/fallback fields = language:%q fallback:%v original:%q", got.TitleLanguage, got.TitleIsFallback, got.OriginalLanguage)
}
wantAliases := []TitleAlias{{Title: localizedEnglishTitle, Language: "en", Kind: testAlternateAliasKind, Provider: testTVDBProvider}}
if !reflect.DeepEqual(got.TitleAliases, wantAliases) {
t.Fatalf("TitleAliases = %#v, want %#v", got.TitleAliases, wantAliases)
}
if !reflect.DeepEqual(got.ProviderIDs, map[string]string{
testTVDBProvider: localizedTVDBID,
testTMDBProvider: localizedTMDBID,
testIMDBProvider: "tt0213338",
}) {
t.Fatalf("ProviderIDs = %#v", got.ProviderIDs)
}
}
func TestPluginProviderGetMetadata_MapsReleaseDate(t *testing.T) {
item := &pluginv1.MetadataItem{
ProviderId: "provider-1",
ItemType: "movie",
Title: "Example Movie",
ProviderIds: mustStructFromStringMap(t, map[string]string{
"tmdb": "123",
}),
}
setPluginMetadataItemReleaseDate(t, item, "2024-01-02")
provider, err := NewPluginProviderWithClientFactory(map[string]string{
"plugin_installation_id": "1",
"capability_id": "tmdb",
}, func(context.Context, int, string) (pluginMetadataClient, error) {
return &fakePluginMetadataClient{
response: &pluginv1.GetMetadataResponse{Item: item},
}, nil
})
if err != nil {
t.Fatalf("NewPluginProviderWithClientFactory() error = %v", err)
}
result, err := provider.GetMetadata(context.Background(), MetadataRequest{
ProviderIDs: map[string]string{"tmdb": "provider-1"},
ContentType: "movie",
})
if err != nil {
t.Fatalf("GetMetadata() error = %v", err)
}
if result == nil {
t.Fatal("expected metadata result")
}
if result.ReleaseDate != "2024-01-02" {
t.Fatalf("ReleaseDate = %q, want 2024-01-02", result.ReleaseDate)
}
}
func TestPluginProviderGetMetadata_MapsAirTime(t *testing.T) {
item := &pluginv1.MetadataItem{
ProviderId: "provider-1",
ItemType: "series",
Title: "Example Series",
AirTime: "20:00",
ProviderIds: mustStructFromStringMap(t, map[string]string{
"tvdb": "123",
}),
}
provider, err := NewPluginProviderWithClientFactory(map[string]string{
"plugin_installation_id": "1",
"capability_id": "tvdb",
}, func(context.Context, int, string) (pluginMetadataClient, error) {
return &fakePluginMetadataClient{
response: &pluginv1.GetMetadataResponse{Item: item},
}, nil
})
if err != nil {
t.Fatalf("NewPluginProviderWithClientFactory() error = %v", err)
}
result, err := provider.GetMetadata(context.Background(), MetadataRequest{
ProviderIDs: map[string]string{"tvdb": "provider-1"},
ContentType: "series",
})
if err != nil {
t.Fatalf("GetMetadata() error = %v", err)
}
if result == nil {
t.Fatal("expected metadata result")
}
if result.AirTime != "20:00" {
t.Fatalf("AirTime = %q, want 20:00", result.AirTime)
}
}
func TestPluginProviderGetMetadata_MapsKeywordsFromMetadata(t *testing.T) {
metadata, err := structpb.NewStruct(map[string]any{
"keywords": []any{"time loop", "heist", "time loop", "", 42},
})
if err != nil {
t.Fatalf("structpb.NewStruct() error = %v", err)
}
item := &pluginv1.MetadataItem{
ProviderId: "provider-1",
ItemType: "movie",
Title: "Example Movie",
Metadata: metadata,
ProviderIds: mustStructFromStringMap(t, map[string]string{
"tmdb": "123",
}),
}
provider, err := NewPluginProviderWithClientFactory(map[string]string{
"plugin_installation_id": "1",
"capability_id": "tmdb",
}, func(context.Context, int, string) (pluginMetadataClient, error) {
return &fakePluginMetadataClient{
response: &pluginv1.GetMetadataResponse{Item: item},
}, nil
})
if err != nil {
t.Fatalf("NewPluginProviderWithClientFactory() error = %v", err)
}
result, err := provider.GetMetadata(context.Background(), MetadataRequest{
ProviderIDs: map[string]string{"tmdb": "provider-1"},
ContentType: "movie",
})
if err != nil {
t.Fatalf("GetMetadata() error = %v", err)
}
if result == nil {
t.Fatal("expected metadata result")
}
want := []string{"time loop", "heist"}
if !reflect.DeepEqual(result.Keywords, want) {
t.Fatalf("Keywords = %#v, want %#v", result.Keywords, want)
}
}
func TestPluginProviderGetMetadata_ForwardsRequestContext(t *testing.T) {
client := &fakePluginMetadataClient{
response: &pluginv1.GetMetadataResponse{
Item: &pluginv1.MetadataItem{ProviderId: "provider-1"},
},
}
provider, err := NewPluginProviderWithClientFactory(map[string]string{
"plugin_installation_id": "1",
"capability_id": "tmdb",
}, func(context.Context, int, string) (pluginMetadataClient, error) {
return client, nil
})
if err != nil {
t.Fatalf("NewPluginProviderWithClientFactory() error = %v", err)
}
_, err = provider.GetMetadata(context.Background(), MetadataRequest{
ProviderIDs: map[string]string{
"tmdb": "provider-1",
"imdb": "tt1234567",
},
ContentType: "movie",
Language: "fr",
FilePath: "/media/movies/example.mkv",
})
if err != nil {
t.Fatalf("GetMetadata() error = %v", err)
}
if client.getMetadataReq == nil {
t.Fatal("expected GetMetadata request to be captured")
}
if client.getMetadataReq.GetProviderId() != "provider-1" {
t.Fatalf("ProviderId = %q, want provider-1", client.getMetadataReq.GetProviderId())
}
if client.getMetadataReq.GetItemType() != "movie" {
t.Fatalf("ItemType = %q, want movie", client.getMetadataReq.GetItemType())
}
if client.getMetadataReq.GetLanguage() != "fr" {
t.Fatalf("Language = %q, want fr", client.getMetadataReq.GetLanguage())
}
if client.getMetadataReq.GetFilePath() != "/media/movies/example.mkv" {
t.Fatalf("FilePath = %q, want /media/movies/example.mkv", client.getMetadataReq.GetFilePath())
}
assertStructStringMap(t, client.getMetadataReq.GetProviderIds(), map[string]string{
"tmdb": "provider-1",
"imdb": "tt1234567",
})
}
func TestPluginProviderAssetRequests_ForwardProviderContext(t *testing.T) {
client := &fakePluginMetadataClient{
imagesResponse: &pluginv1.GetImagesResponse{},
seasonsResp: &pluginv1.GetSeasonsResponse{},
episodesResp: &pluginv1.GetEpisodesResponse{},
}
provider, err := NewPluginProviderWithClientFactory(map[string]string{
"plugin_installation_id": "1",
"capability_id": "tmdb",
}, func(context.Context, int, string) (pluginMetadataClient, error) {
return client, nil
})
if err != nil {
t.Fatalf("NewPluginProviderWithClientFactory() error = %v", err)
}
_, err = provider.GetImages(context.Background(), ImageRequest{
ProviderIDs: map[string]string{
"tmdb": "provider-1",
"imdb": "tt1234567",
},
ContentType: "movie",
Language: "es",
})
if err != nil {
t.Fatalf("GetImages() error = %v", err)
}
if client.getImagesReq == nil {
t.Fatal("expected GetImages request to be captured")
}
if client.getImagesReq.GetLanguage() != "es" {
t.Fatalf("Language = %q, want es", client.getImagesReq.GetLanguage())
}
assertStructStringMap(t, client.getImagesReq.GetProviderIds(), map[string]string{
"tmdb": "provider-1",
"imdb": "tt1234567",
})
_, err = provider.GetSeasons(context.Background(), SeasonsRequest{
ProviderIDs: map[string]string{
"tmdb": "series-1",
"tvdb": "81189",
},
ContentType: "series",
})
if err != nil {
t.Fatalf("GetSeasons() error = %v", err)
}
if client.getSeasonsReq == nil {
t.Fatal("expected GetSeasons request to be captured")
}
if client.getSeasonsReq.GetSeriesProviderId() != "series-1" {
t.Fatalf("SeriesProviderId = %q, want series-1", client.getSeasonsReq.GetSeriesProviderId())
}
assertStructStringMap(t, client.getSeasonsReq.GetProviderIds(), map[string]string{
"tmdb": "series-1",
"tvdb": "81189",
})
_, err = provider.GetEpisodes(context.Background(), EpisodesRequest{
ProviderIDs: map[string]string{
"tmdb": "series-1",
"tvdb": "81189",
},
SeasonNumber: 2,
})
if err != nil {
t.Fatalf("GetEpisodes() error = %v", err)
}
if client.getEpisodesReq == nil {
t.Fatal("expected GetEpisodes request to be captured")
}
if client.getEpisodesReq.GetSeasonNumber() != 2 {
t.Fatalf("SeasonNumber = %d, want 2", client.getEpisodesReq.GetSeasonNumber())
}
assertStructStringMap(t, client.getEpisodesReq.GetProviderIds(), map[string]string{
"tmdb": "series-1",
"tvdb": "81189",
})
}
func mustStructFromStringMap(t *testing.T, values map[string]string) *structpb.Struct {
t.Helper()
asAny := make(map[string]any, len(values))
for key, value := range values {
asAny[key] = value
}
result, err := structpb.NewStruct(asAny)
if err != nil {
t.Fatalf("structpb.NewStruct() error = %v", err)
}
return result
}
func assertStructStringMap(t *testing.T, value *structpb.Struct, want map[string]string) {
t.Helper()
got := make(map[string]string, len(want))
if value != nil {
for key, raw := range value.AsMap() {
text, ok := raw.(string)
if ok {
got[key] = text
}
}
}
if len(got) != len(want) {
t.Fatalf("struct map length = %d, want %d (%#v)", len(got), len(want), got)
}
for key, wantValue := range want {
if got[key] != wantValue {
t.Fatalf("struct map[%q] = %q, want %q", key, got[key], wantValue)
}
}
}