Save source descriptions, add "Why it's #N", fix IGDB studio names
Descriptions
- Items keep the source's description (TMDB/TheTVDB overview, IGDB
summary, Wikipedia intro) in a new items.description column
(migration 3); editable in the item form
- Deezer/MusicBrainz track counts and album names are now display-only
hints, not saved as descriptions
Why it's #N
- The owner's note is presented as "Why it's #N" in the editor and the
public detail view, above an "About" section with the description
IGDB developers
- IGDB credits internal divisions ("Nintendo EAD", "Nintendo EPD
Production Group No. 8"). A developer whose name shares its first word
with its parent company is credited to the parent ("Nintendo");
independently named studios keep their name (Retro Studios, Naughty
Dog, Landfall Games)
- Fall back to the publisher when IGDB lists no developer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3923444b3b
commit
7320e3f707
16 files changed
+207
-44
No files matched your search
+97
-16
@@ -2,6 +2,7 @@ package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// igdb searches the Internet Game Database, authenticating through Twitch.
|
||||
@@ -59,7 +61,7 @@ func (g *igdb) token(ctx context.Context, fresh bool) (string, error) {
|
||||
func (g *igdb) Search(ctx context.Context, query string) ([]Result, error) {
|
||||
q := strings.NewReplacer(`\`, ``, `"`, `\"`).Replace(query)
|
||||
body := `search "` + q + `";
|
||||
fields name,first_release_date,cover.image_id,summary,url,involved_companies.developer,involved_companies.company.name,platforms.abbreviation,platforms.name;
|
||||
fields name,first_release_date,cover.image_id,summary,url,involved_companies.developer,involved_companies.publisher,involved_companies.company.name,involved_companies.company.parent.name,involved_companies.company.parent.parent.name,involved_companies.company.parent.parent.parent.name,platforms.abbreviation,platforms.name;
|
||||
limit 25;`
|
||||
var games []struct {
|
||||
ID int64 `json:"id"`
|
||||
@@ -70,13 +72,8 @@ limit 25;`
|
||||
Cover struct {
|
||||
ImageID string `json:"image_id"`
|
||||
} `json:"cover"`
|
||||
InvolvedCompanies []struct {
|
||||
Developer bool `json:"developer"`
|
||||
Company struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"company"`
|
||||
} `json:"involved_companies"`
|
||||
Platforms []struct {
|
||||
InvolvedCompanies []igdbInvolved `json:"involved_companies"`
|
||||
Platforms []struct {
|
||||
Abbreviation string `json:"abbreviation"`
|
||||
Name string `json:"name"`
|
||||
} `json:"platforms"`
|
||||
@@ -104,12 +101,7 @@ limit 25;`
|
||||
}
|
||||
out := make([]Result, 0, len(games))
|
||||
for _, gm := range games {
|
||||
var devs, plats []string
|
||||
for _, ic := range gm.InvolvedCompanies {
|
||||
if ic.Developer && ic.Company.Name != "" {
|
||||
devs = append(devs, ic.Company.Name)
|
||||
}
|
||||
}
|
||||
var plats []string
|
||||
for _, p := range gm.Platforms {
|
||||
name := p.Abbreviation
|
||||
if name == "" {
|
||||
@@ -121,9 +113,9 @@ limit 25;`
|
||||
Provider: "igdb",
|
||||
ProviderID: strconv.FormatInt(gm.ID, 10),
|
||||
Title: gm.Name,
|
||||
Subtitle: strings.Join(devs, ", "),
|
||||
Subtitle: strings.Join(igdbStudios(gm.InvolvedCompanies), ", "),
|
||||
ExternalURL: gm.URL,
|
||||
Overview: truncate(gm.Summary, 600),
|
||||
Overview: truncate(gm.Summary, maxDescription),
|
||||
Extra: map[string]string{},
|
||||
}
|
||||
if gm.FirstReleaseDate > 0 {
|
||||
@@ -139,3 +131,92 @@ limit 25;`
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type igdbInvolved struct {
|
||||
Developer bool `json:"developer"`
|
||||
Publisher bool `json:"publisher"`
|
||||
Company igdbCompany `json:"company"`
|
||||
}
|
||||
|
||||
// igdbStudios names who made a game: its developers, or its publishers when
|
||||
// IGDB lists no developer, with internal divisions credited to their company.
|
||||
func igdbStudios(involved []igdbInvolved) []string {
|
||||
var devs, pubs []string
|
||||
for _, ic := range involved {
|
||||
if ic.Company.Name == "" {
|
||||
continue
|
||||
}
|
||||
if ic.Developer {
|
||||
devs = appendUnique(devs, studioName(&ic.Company))
|
||||
}
|
||||
if ic.Publisher {
|
||||
pubs = appendUnique(pubs, studioName(&ic.Company))
|
||||
}
|
||||
}
|
||||
if len(devs) == 0 {
|
||||
return pubs
|
||||
}
|
||||
return devs
|
||||
}
|
||||
|
||||
// igdbCompany is a company with as much of its parent chain as was
|
||||
// requested. IGDB sends an unexpanded parent as a bare ID, which is ignored.
|
||||
type igdbCompany struct {
|
||||
Name string
|
||||
Parent *igdbCompany
|
||||
}
|
||||
|
||||
func (c *igdbCompany) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 || b[0] != '{' {
|
||||
return nil
|
||||
}
|
||||
var raw struct {
|
||||
Name string `json:"name"`
|
||||
Parent *igdbCompany `json:"parent"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Name, c.Parent = raw.Name, raw.Parent
|
||||
if c.Parent != nil && c.Parent.Name == "" {
|
||||
c.Parent = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// studioName credits an internal division to the company it belongs to.
|
||||
// IGDB lists the exact team, so Super Mario Sunshine's developer is
|
||||
// "Nintendo EAD" and DK Bananza's is "Nintendo EPD Production Group No. 8".
|
||||
// When a studio's name starts with the same word as its parent's, it's a
|
||||
// division of that company and we show the parent ("Nintendo"). Studios
|
||||
// with their own identity keep their name: Retro Studios (owned by
|
||||
// Nintendo) or Naughty Dog (owned by Sony).
|
||||
func studioName(c *igdbCompany) string {
|
||||
for c.Parent != nil && sameFirstWord(c.Name, c.Parent.Name) {
|
||||
c = c.Parent
|
||||
}
|
||||
return c.Name
|
||||
}
|
||||
|
||||
func sameFirstWord(a, b string) bool {
|
||||
first := func(s string) string {
|
||||
f := strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
|
||||
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
|
||||
})
|
||||
if len(f) == 0 {
|
||||
return ""
|
||||
}
|
||||
return f[0]
|
||||
}
|
||||
fa := first(a)
|
||||
return fa != "" && fa == first(b)
|
||||
}
|
||||
|
||||
func appendUnique(list []string, s string) []string {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return append(list, s)
|
||||
}
|
||||
Reference in new issue
Block a user