Games: Support searching with Year or FirstYear filters

This commit is contained in:
IRHM
2026-08-02 23:53:12 +01:00
committed by momi
parent dc2b9a1a63
commit 92edb75de9
3 changed files with 70 additions and 11 deletions
+1
View File
@@ -10,6 +10,7 @@ These changes are awaiting release:
- Import(text list): Support parsing a rating (out of 10, supporting decimals) in square brackets.
- Show `End Year` for TV Shows that have ended or been canceled.
- Add full release date and last release date to their respective year elements as titles (browser tooltip).
- Search: Support inline filters for Non-Multi searches (ex: You can now search `Spider Man year:2026` or `Spider Man fyear:2026` if you care strictly about the first release year).
## Changed
+16 -7
View File
@@ -11,6 +11,7 @@ import (
"github.com/sbondCo/Watcharr/config"
"github.com/sbondCo/Watcharr/database/entity"
"github.com/sbondCo/Watcharr/domain"
"github.com/sbondCo/Watcharr/media/igdb"
"github.com/sbondCo/Watcharr/media/tmdb"
"github.com/sbondCo/Watcharr/util"
"gorm.io/gorm"
@@ -80,7 +81,10 @@ func (s *Service) Search(
Page: pp.Page,
Adult: qfilters.Adult,
}
if err := s.searchMulti(sreq, &resp); err != nil {
greq := igdb.SearchOptions{
Query: query,
}
if err := s.searchMulti(sreq, greq, &resp); err != nil {
return resp, errors.New("multi search failed")
}
case domain.SearchTypeMovie:
@@ -119,7 +123,12 @@ func (s *Service) Search(
return resp, errors.New("person search failed")
}
case domain.SearchTypeGame:
if err := s.searchGame(r.Query, pp.Page, &resp); err != nil {
greq := igdb.SearchOptions{
Query: query,
Year: qfilters.Year,
PrimaryYear: qfilters.FirstYear,
}
if err := s.searchGame(greq, &resp); err != nil {
return resp, errors.New("game search failed")
}
}
@@ -132,6 +141,7 @@ func (s *Service) Search(
// SearchMulti is TMDB Multi search but with game data added to first page.
func (s *Service) searchMulti(
req tmdb.SearchUniversalOptions,
igdbReq igdb.SearchOptions,
resp *domain.SearchResponse,
) error {
slog.Debug("searchMulti: Running.", "req", req)
@@ -149,7 +159,7 @@ func (s *Service) searchMulti(
}
// IGDB (we will only get results for the first page)
if req.Page == 1 && s.cfg.TwitchEnabled() {
igdbRes, err := s.cfg.TWITCH.Search(req.Query)
igdbRes, err := s.cfg.TWITCH.Search(igdbReq)
if err != nil {
slog.Error("SearchMulti: Failed to search igdb!", "error", err)
return errors.New("content request failed")
@@ -278,12 +288,11 @@ func (s *Service) searchPeople(
}
func (s *Service) searchGame(
query string,
page int,
req igdb.SearchOptions,
resp *domain.SearchResponse,
) error {
slog.Debug("searchGame: Running.", "query", query, "page", page)
igdbRes, err := s.cfg.TWITCH.Search(query)
slog.Debug("searchGame: Running.", "req", req)
igdbRes, err := s.cfg.TWITCH.Search(req)
if err != nil {
slog.Error("searchGame: Failed to search igdb!", "error", err)
return errors.New("content request failed")
+53 -4
View File
@@ -183,19 +183,68 @@ func (i *IGDB) Init() error {
return nil
}
func (i *IGDB) Search(q string) (GameSearchResponse, error) {
slog.Debug("Search:", "query", q)
type SearchOptions struct {
Query string
Year int
PrimaryYear int
}
// Turn supported filters from the SearchOptions struct into a `where` we can
// send to APICalypse.
func (o SearchOptions) AsWhere() string {
f := []string{}
if o.Year != 0 {
y := strconv.Itoa(o.Year)
// If `any` release of the game was on this year.
f = append(f, "release_dates.y = "+y)
}
if o.PrimaryYear != 0 {
start := strconv.FormatInt(
time.
Date(o.PrimaryYear, time.January, 1, 0, 0, 0, 0, time.UTC).
Unix(),
10)
end := strconv.FormatInt(
time.
// We add one to the year and get the first day of the next year
// BUT because day=0, we go back one day and end up with the
// very last day of `PrimaryYear`.
Date(o.PrimaryYear+1, time.January, 0, 23, 59, 59, 0, time.UTC).
Unix(),
10)
// If `first` release of the game was on this year.
f = append(f, "first_release_date > "+start)
f = append(f, "first_release_date < "+end)
}
if len(f) <= 0 {
return ""
}
return "where " + strings.Join(f, " & ") + ";"
}
func (i *IGDB) Search(o SearchOptions) (GameSearchResponse, error) {
slog.Debug("Search:", "query", o.Query)
var resp GameSearchResponse
cacheKey := cache.CreateCacheKey("Search", q)
cacheKey := cache.CreateCacheKey(
"Search",
o.Query,
o.Year,
o.PrimaryYear)
if cache.GetCache(GameStore, cacheKey, &resp) {
slog.Debug("Search: Returning cache.")
return resp, nil
}
apiQuery := "fields " + fieldsForSearch + "; search \"" + o.Query + "\"; limit 40;"
whereQ := o.AsWhere()
if whereQ != "" {
apiQuery += whereQ
}
err := i.req(
igdbHost,
"/games",
map[string]string{},
"fields "+fieldsForSearch+"; search \""+q+"\"; limit 40;",
apiQuery,
&resp,
)
if err != nil {