mirror of
https://github.com/Dvorinka/SEEN.git
synced 2026-06-04 12:33:02 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
package igdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tdvorak/seen/backend/internal/config"
|
||||
"github.com/tdvorak/seen/backend/internal/services/catalog"
|
||||
)
|
||||
|
||||
var ErrIGDBCredentialsMissing = errors.New("igdb client credentials missing")
|
||||
|
||||
type Client struct {
|
||||
cfg config.IGDBConfig
|
||||
httpClient *http.Client
|
||||
|
||||
mu sync.Mutex
|
||||
accessToken string
|
||||
accessTokenExp time.Time
|
||||
}
|
||||
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
type gameResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Summary string `json:"summary"`
|
||||
FirstReleaseDate int64 `json:"first_release_date"`
|
||||
Rating float64 `json:"rating"`
|
||||
Genres []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"genres"`
|
||||
Platforms []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"platforms"`
|
||||
}
|
||||
|
||||
func NewClient(cfg config.IGDBConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 12 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Enabled() bool {
|
||||
return strings.TrimSpace(c.cfg.ClientID) != "" && strings.TrimSpace(c.cfg.ClientSecret) != ""
|
||||
}
|
||||
|
||||
func (c *Client) SearchGames(ctx context.Context, query string, limit int) ([]catalog.MediaItem, error) {
|
||||
if !c.Enabled() {
|
||||
return nil, ErrIGDBCredentialsMissing
|
||||
}
|
||||
|
||||
cleanQuery := strings.TrimSpace(query)
|
||||
if cleanQuery == "" {
|
||||
return []catalog.MediaItem{}, nil
|
||||
}
|
||||
|
||||
if limit < 1 {
|
||||
limit = 12
|
||||
}
|
||||
|
||||
token, err := c.token(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(
|
||||
"fields id,name,summary,first_release_date,rating,genres.name,platforms.name; search %q; limit %d;",
|
||||
cleanQuery,
|
||||
limit,
|
||||
)
|
||||
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
strings.TrimRight(c.cfg.BaseURL, "/")+"/games",
|
||||
strings.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Client-ID", c.cfg.ClientID)
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= http.StatusBadRequest {
|
||||
message, _ := io.ReadAll(io.LimitReader(response.Body, 2048))
|
||||
return nil, fmt.Errorf("igdb search failed: %s", strings.TrimSpace(string(message)))
|
||||
}
|
||||
|
||||
var payload []gameResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]catalog.MediaItem, 0, len(payload))
|
||||
for _, game := range payload {
|
||||
title := strings.TrimSpace(game.Name)
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
overview := strings.TrimSpace(game.Summary)
|
||||
if overview == "" {
|
||||
overview = fmt.Sprintf("%s is available through the live IGDB provider search.", title)
|
||||
}
|
||||
|
||||
items = append(items, catalog.MediaItem{
|
||||
ID: int(game.ID) + 900000,
|
||||
Provider: catalog.MediaProviderIGDB,
|
||||
ProviderID: int(game.ID),
|
||||
Title: title,
|
||||
Overview: overview,
|
||||
Type: catalog.MediaTypeGame,
|
||||
ReleaseDate: unixDate(game.FirstReleaseDate),
|
||||
Genres: genreNames(game.Genres),
|
||||
Platforms: platformNames(game.Platforms),
|
||||
Rating: normalizeRating(game.Rating),
|
||||
RuntimeMinutes: 0,
|
||||
ArtworkKey: fmt.Sprintf("igdb-%d", game.ID),
|
||||
})
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (c *Client) token(ctx context.Context) (string, error) {
|
||||
c.mu.Lock()
|
||||
if c.accessToken != "" && time.Now().Before(c.accessTokenExp.Add(-1*time.Minute)) {
|
||||
token := c.accessToken
|
||||
c.mu.Unlock()
|
||||
return token, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("client_id", c.cfg.ClientID)
|
||||
form.Set("client_secret", c.cfg.ClientSecret)
|
||||
form.Set("grant_type", "client_credentials")
|
||||
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
c.cfg.TokenURL,
|
||||
strings.NewReader(form.Encode()),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= http.StatusBadRequest {
|
||||
message, _ := io.ReadAll(io.LimitReader(response.Body, 2048))
|
||||
return "", fmt.Errorf("igdb token request failed: %s", strings.TrimSpace(string(message)))
|
||||
}
|
||||
|
||||
var payload tokenResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if payload.AccessToken == "" {
|
||||
return "", errors.New("igdb token response missing access token")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.accessToken = payload.AccessToken
|
||||
c.accessTokenExp = time.Now().Add(time.Duration(payload.ExpiresIn) * time.Second)
|
||||
token := c.accessToken
|
||||
c.mu.Unlock()
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func unixDate(value int64) string {
|
||||
if value <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return time.Unix(value, 0).UTC().Format("2006-01-02")
|
||||
}
|
||||
|
||||
func normalizeRating(value float64) float64 {
|
||||
if value > 10 {
|
||||
return value / 10
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func genreNames(values []struct {
|
||||
Name string `json:"name"`
|
||||
}) []string {
|
||||
if len(values) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
name := strings.TrimSpace(value.Name)
|
||||
if name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func platformNames(values []struct {
|
||||
Name string `json:"name"`
|
||||
}) []string {
|
||||
if len(values) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
name := strings.TrimSpace(value.Name)
|
||||
if name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
Reference in New Issue
Block a user