Files
silo-server/internal/metadata/chain_test.go
T
66a4146fc2 fix(metadata): exclude providers from content levels they don't declare (#106)
ResolveChain falls back to every enabled metadata provider when a library
+ content-level has no enabled chain entry. That fallback was media-type
blind: a provider declaring default_priority only for an unrelated level
(e.g. an audiobook provider declaring {"audiobook": N}) was kept in the
list (merely sorted last) and invoked for video content levels.

In production this made silo.audiobook-metadata hammer external audiobook
APIs with anime/movie/series titles every scheduled enrichment pass
(MatchWorker, 30s) for the season/episode levels that had no enabled chain
entry. Disabling the chain entries did not help because the fallback never
consults them; only disabling the installation removed it from the global
set.

Treat a non-empty default_priority map as the provider enumerating the
content levels it supports: in resolveEnabledProvidersByPriority, exclude
providers whose declared map omits the requested level instead of ranking
them last. Providers that declare no default_priority make no claim and
stay eligible everywhere (legacy behavior).

Fixes #105

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:31:52 -04:00

60 lines
2.7 KiB
Go

package metadata
import "testing"
// Capability metadata as stored in plugin_capabilities.metadata: plugin-declared
// fields are wrapped in a "metadata" envelope.
const (
audiobookCapMetadata = `{"metadata":{"default_priority":{"audiobook":2}},"display_name":"Audiobook Metadata"}`
tmdbCapMetadata = `{"metadata":{"default_priority":{"movie":2,"season":3,"series":3,"episode":3}},"display_name":"TMDB"}`
tvdbCapMetadata = `{"metadata":{"default_priority":{"season":2,"series":2,"episode":2}},"display_name":"TVDB"}`
)
func TestProviderSupportsLevel(t *testing.T) {
cases := []struct {
name string
metadataJSON string
contentLevel string
want bool
}{
// An audiobook-only provider must NOT be pulled into video content
// levels by the chain-less global fallback. This is the regression
// that let silo.audiobook-metadata hammer audiobook APIs with
// anime/movie/series titles.
{"audiobook provider excluded from episode", audiobookCapMetadata, "episode", false},
{"audiobook provider excluded from series", audiobookCapMetadata, "series", false},
{"audiobook provider excluded from season", audiobookCapMetadata, "season", false},
{"audiobook provider excluded from movie", audiobookCapMetadata, "movie", false},
{"audiobook provider included for audiobook", audiobookCapMetadata, "audiobook", true},
// Providers that declare a level stay eligible for it.
{"tmdb included for movie", tmdbCapMetadata, "movie", true},
{"tmdb included for season", tmdbCapMetadata, "season", true},
{"tmdb included for episode", tmdbCapMetadata, "episode", true},
// tvdb declares no movie level -> excluded from the movie fallback.
{"tvdb excluded from movie", tvdbCapMetadata, "movie", false},
{"tvdb included for series", tvdbCapMetadata, "series", true},
// A provider that declares no default_priority makes no claim and stays
// eligible everywhere (legacy behavior), ranked last by priority 0.
{"no metadata is eligible", ``, "episode", true},
{"empty object is eligible", `{}`, "episode", true},
{"empty priority map is eligible", `{"metadata":{"default_priority":{}}}`, "episode", true},
{"malformed json is eligible (fail-open)", `{not json`, "episode", true},
// A declared level with non-positive priority is not supported.
{"zero priority level not supported", `{"metadata":{"default_priority":{"episode":0}}}`, "episode", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := providerSupportsLevel([]byte(tc.metadataJSON), tc.contentLevel)
if got != tc.want {
t.Fatalf("providerSupportsLevel(%q, %q) = %v, want %v",
tc.metadataJSON, tc.contentLevel, got, tc.want)
}
})
}
}