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>
This commit is contained in:
committed by
GitHub
co-authored by
Claude Opus 4.8
parent
b7439884ec
commit
66a4146fc2
+62
-19
@@ -303,9 +303,21 @@ func resolveEnabledProvidersByPriority(
|
||||
priority int
|
||||
}
|
||||
|
||||
items := make([]ranked, len(caps))
|
||||
for i, c := range caps {
|
||||
items[i] = ranked{cap: c, priority: LookupDefaultPriority(ctx, pool, c.PluginInstallationID, contentLevel)}
|
||||
// Only providers that support contentLevel participate in the chain-less
|
||||
// fallback. A provider declaring a non-empty default_priority map that omits
|
||||
// this level is excluded outright (not merely ranked last), so a
|
||||
// single-purpose provider is never invoked for content it does not handle.
|
||||
items := make([]ranked, 0, len(caps))
|
||||
for _, c := range caps {
|
||||
metadataJSON := lookupCapabilityMetadata(ctx, pool, c.PluginInstallationID)
|
||||
if !providerSupportsLevel(metadataJSON, contentLevel) {
|
||||
slog.Debug("skipping metadata provider: does not declare support for content level",
|
||||
"installation_id", c.PluginInstallationID,
|
||||
"capability_id", c.CapabilityID,
|
||||
"content_level", contentLevel)
|
||||
continue
|
||||
}
|
||||
items = append(items, ranked{cap: c, priority: extractDefaultPriority(metadataJSON, contentLevel)})
|
||||
}
|
||||
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
@@ -326,9 +338,27 @@ func resolveEnabledProvidersByPriority(
|
||||
return buildProviders(ctx, sorted, resolver, pool), nil
|
||||
}
|
||||
|
||||
// LookupDefaultPriority queries plugin_capabilities for a provider's declared
|
||||
// default_priority at the given content level. Returns 0 if not found.
|
||||
func LookupDefaultPriority(ctx context.Context, pool *pgxpool.Pool, pluginInstallationID int, contentLevel string) int {
|
||||
// providerSupportsLevel reports whether a metadata provider should participate
|
||||
// in the chain-less global fallback for contentLevel. A provider that declares
|
||||
// a non-empty default_priority map is treated as enumerating the content levels
|
||||
// it supports: it is eligible only for levels present in that map with a
|
||||
// positive priority. A provider that declares no default_priority makes no
|
||||
// claim and stays eligible for every level (legacy behavior), ranked last.
|
||||
//
|
||||
// This is what keeps a single-purpose provider (e.g. an audiobook metadata
|
||||
// provider declaring only {"audiobook": N}) from being pulled into video
|
||||
// content levels when a library has no enabled chain entry for that level.
|
||||
func providerSupportsLevel(metadataJSON []byte, contentLevel string) bool {
|
||||
levels, declared := declaredPriorityLevels(metadataJSON)
|
||||
if !declared {
|
||||
return true
|
||||
}
|
||||
return levels[contentLevel] > 0
|
||||
}
|
||||
|
||||
// lookupCapabilityMetadata returns the raw plugin_capabilities.metadata JSON for
|
||||
// a provider's metadata_provider.v1 capability, or nil if absent.
|
||||
func lookupCapabilityMetadata(ctx context.Context, pool *pgxpool.Pool, pluginInstallationID int) []byte {
|
||||
var metadataJSON []byte
|
||||
err := pool.QueryRow(ctx,
|
||||
`SELECT metadata FROM plugin_capabilities
|
||||
@@ -337,21 +367,37 @@ func LookupDefaultPriority(ctx context.Context, pool *pgxpool.Pool, pluginInstal
|
||||
pluginInstallationID,
|
||||
).Scan(&metadataJSON)
|
||||
if err != nil {
|
||||
return 0
|
||||
return nil
|
||||
}
|
||||
return metadataJSON
|
||||
}
|
||||
|
||||
return extractDefaultPriority(metadataJSON, contentLevel)
|
||||
// LookupDefaultPriority queries plugin_capabilities for a provider's declared
|
||||
// default_priority at the given content level. Returns 0 if not found.
|
||||
func LookupDefaultPriority(ctx context.Context, pool *pgxpool.Pool, pluginInstallationID int, contentLevel string) int {
|
||||
return extractDefaultPriority(lookupCapabilityMetadata(ctx, pool, pluginInstallationID), contentLevel)
|
||||
}
|
||||
|
||||
// extractDefaultPriority parses the default_priority for a content level from
|
||||
// capability metadata JSON.
|
||||
func extractDefaultPriority(metadataJSON []byte, contentLevel string) int {
|
||||
levels, _ := declaredPriorityLevels(metadataJSON)
|
||||
if v, ok := levels[contentLevel]; ok && v > 0 {
|
||||
return int(v)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// declaredPriorityLevels parses a capability's default_priority map. The map may
|
||||
// sit at the top level or inside a "metadata" envelope (plugin capability
|
||||
// metadata wraps plugin-declared fields in a "metadata" sub-object). The second
|
||||
// return value is true only when a non-empty map was found, i.e. the provider
|
||||
// explicitly enumerates the content levels it supports.
|
||||
func declaredPriorityLevels(metadataJSON []byte) (map[string]float64, bool) {
|
||||
var meta map[string]json.RawMessage
|
||||
if err := json.Unmarshal(metadataJSON, &meta); err != nil {
|
||||
return 0
|
||||
return nil, false
|
||||
}
|
||||
// default_priority may be at the top level or nested inside a "metadata" sub-object
|
||||
// (plugin capability metadata wraps plugin-declared fields in a "metadata" envelope).
|
||||
dpRaw, ok := meta["default_priority"]
|
||||
if !ok {
|
||||
if innerRaw, innerOK := meta["metadata"]; innerOK {
|
||||
@@ -360,18 +406,15 @@ func extractDefaultPriority(metadataJSON []byte, contentLevel string) int {
|
||||
dpRaw, ok = inner["default_priority"]
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
var dpMap map[string]float64
|
||||
if err := json.Unmarshal(dpRaw, &dpMap); err != nil {
|
||||
return 0
|
||||
return nil, false
|
||||
}
|
||||
if v, ok := dpMap[contentLevel]; ok && v > 0 {
|
||||
return int(v)
|
||||
}
|
||||
return 0
|
||||
return dpMap, len(dpMap) > 0
|
||||
}
|
||||
|
||||
// ListEnabledMetadataCapabilities returns all metadata_provider.v1 capabilities
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user