- Every provider can look an entry up again by its ID (TMDB, TheTVDB,
IGDB, Deezer, MusicBrainz, Wikipedia); search and lookup share parsing
- POST /api/items/{id}/refresh re-applies title, subtitle, year,
description, link, details and artwork. Kept: the "why" note, rank,
theater production/venue/cast and a theater entry's year, the Deezer
preview match. Empty source fields don't erase existing values
- New items.image_custom column (migration 4; existing uploads marked)
records artwork the owner chose; refresh keeps it unless asked
- Refused for custom lists and hand-added entries; entries the source
dropped are reported, and a refresh whose title shares no words with
the current one is rejected as a likely bad ID
- Editor: refresh button per row (asks about the owner's own artwork),
and "Refresh all" with a confirm step, replace-my-art checkbox,
progress with Stop, and a summary of problems
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.4 KiB
Go
76 lines
2.4 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// Looker is implemented by providers that can fetch one entry again by its
|
|
// ID, to refresh metadata that changed at the source.
|
|
type Looker interface {
|
|
Lookup(ctx context.Context, providerID string) (Result, error)
|
|
}
|
|
|
|
// ErrGone means the source no longer has the entry (deleted or merged).
|
|
var ErrGone = errors.New("the source no longer has this entry")
|
|
|
|
// ErrNoSource means the entry wasn't added from a service that can be
|
|
// refreshed.
|
|
var ErrNoSource = errors.New("no source to refresh from")
|
|
|
|
// Lookup fetches an entry again from the service it came from. The
|
|
// provider is the one saved with the entry, whatever the current setting.
|
|
func (r *Registry) Lookup(ctx context.Context, category, provider, providerID string) (Result, error) {
|
|
var l Looker
|
|
c := r.creds(ctx)
|
|
switch provider {
|
|
case "tmdb":
|
|
if c.TMDBKey == "" {
|
|
return Result{}, NotConfiguredError{"TMDB", "Add a TMDB API key under Settings → Integrations."}
|
|
}
|
|
kind, _, _ := strings.Cut(providerID, ":")
|
|
l = &tmdb{r: r, key: c.TMDBKey, kind: kind}
|
|
case "tvdb":
|
|
if c.TVDBKey == "" {
|
|
return Result{}, NotConfiguredError{"TheTVDB", "Add a TheTVDB API key under Settings → Integrations."}
|
|
}
|
|
l = &tvdb{r: r, key: c.TVDBKey, pin: c.TVDBPin}
|
|
case "igdb":
|
|
if c.IGDBClientID == "" || c.IGDBClientSecret == "" {
|
|
return Result{}, NotConfiguredError{"IGDB", "Add your Twitch client ID and secret under Settings → Integrations."}
|
|
}
|
|
l = &igdb{r: r, id: c.IGDBClientID, secret: c.IGDBClientSecret}
|
|
case "deezer":
|
|
l = &deezer{r: r, kind: category}
|
|
case "musicbrainz":
|
|
l = &musicbrainz{r: r, kind: category}
|
|
case "wikipedia":
|
|
l = &wikipedia{r: r, stage: category == "theater"}
|
|
default:
|
|
return Result{}, ErrNoSource
|
|
}
|
|
return l.Lookup(ctx, providerID)
|
|
}
|
|
|
|
// SameThing reports whether a refreshed title plausibly names the same
|
|
// entry: the titles share at least one word, ignoring bracketed qualifiers
|
|
// like "(Remastered)". A refresh that comes back with an unrelated title
|
|
// usually means a bad or reused ID, and shouldn't overwrite the entry.
|
|
func SameThing(a, b string) bool {
|
|
wa, wb := strings.Fields(baseTitle(a)), strings.Fields(baseTitle(b))
|
|
if len(wa) == 0 || len(wb) == 0 {
|
|
return true
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, w := range wa {
|
|
seen[w] = true
|
|
}
|
|
for _, w := range wb {
|
|
if seen[w] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|