Files
cdnutterandClaude Opus 5.5 33053d1c5f Add refresh from source for entries and whole lists
- 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>
2026-09-25 12:41:22 -07:00

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
}