// Package metadata resolves media items against external catalogues. // // Providers are optional by design: a library with no network still browses, // because a failed lookup is recorded and skipped, never surfaced as a scan or // request failure. package metadata import ( "context" "strings" "sync" ) // Query is what a provider is asked to match. type Query struct { Title string Author string ISBN string Kind string } // Candidate is one possible match returned by a provider. type Candidate struct { Provider string `json:"provider"` ExternalID string `json:"external_id"` Title string `json:"title"` Authors []string `json:"authors"` Year int `json:"year"` Publisher string `json:"publisher"` Language string `json:"language"` Description string `json:"description"` ISBN string `json:"isbn"` CoverURL string `json:"cover_url"` } // Provider is one external catalogue. type Provider interface { // Name is the stable identifier stored in provider_ids.provider. Name() string // Kinds lists the media kinds this provider can match. Kinds() []string // Search returns candidates, best first. An empty result is not an error. Search(ctx context.Context, q Query) ([]Candidate, error) } var ( mu sync.RWMutex providers []Provider ) // Register adds a provider. Later registrations are consulted later, so // registering a primary provider before a fallback gives the intended order. func Register(p Provider) { mu.Lock() defer mu.Unlock() providers = append(providers, p) } // For returns the providers that handle a media kind, in registration order. func For(kind string) []Provider { mu.RLock() defer mu.RUnlock() var out []Provider for _, p := range providers { for _, k := range p.Kinds() { if strings.EqualFold(k, kind) { out = append(out, p) break } } } return out }