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>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-25 12:41:22 -07:00
1 parent 4ea7cbc984
commit 33053d1c5f
18 files changed
+852 -148

No files matched your search

+25 -3
View File
@@ -39,13 +39,28 @@ func (w *wikipedia) Search(ctx context.Context, query string) ([]Result, error)
var leadingYear = regexp.MustCompile(`^(1[5-9]\d\d|20\d\d)\s+`)
func (w *wikipedia) search(ctx context.Context, query string) ([]Result, error) {
return w.pages(ctx, url.Values{"generator": {"search"}, "gsrsearch": {query}, "gsrlimit": {"15"}})
}
// Lookup refreshes one page by its page ID.
func (w *wikipedia) Lookup(ctx context.Context, providerID string) (Result, error) {
res, err := w.pages(ctx, url.Values{"pageids": {providerID}})
if err != nil {
return Result{}, err
}
if len(res) == 0 {
return Result{}, ErrGone
}
return res[0], nil
}
// pages runs a query for pages (a search, or specific page IDs) and shapes
// them into results.
func (w *wikipedia) pages(ctx context.Context, which url.Values) ([]Result, error) {
q := url.Values{
"action": {"query"},
"format": {"json"},
"formatversion": {"2"},
"generator": {"search"},
"gsrsearch": {query},
"gsrlimit": {"15"},
"prop": {"pageimages|description|info|extracts"},
"piprop": {"thumbnail"},
"pithumbsize": {"800"},
@@ -56,10 +71,14 @@ func (w *wikipedia) search(ctx context.Context, query string) ([]Result, error)
"exsentences": {"3"},
"exlimit": {"max"},
}
for k, v := range which {
q[k] = v
}
var resp struct {
Query struct {
Pages []struct {
PageID int64 `json:"pageid"`
Missing bool `json:"missing"`
Title string `json:"title"`
Index int `json:"index"`
Description string `json:"description"`
@@ -78,6 +97,9 @@ func (w *wikipedia) search(ctx context.Context, query string) ([]Result, error)
sort.Slice(pages, func(i, j int) bool { return pages[i].Index < pages[j].Index })
out := make([]Result, 0, len(pages))
for _, p := range pages {
if p.Missing {
continue
}
title := p.Title
// "Hamilton (musical)" → "Hamilton"
if i := strings.LastIndex(title, " ("); i > 0 && strings.HasSuffix(title, ")") {