From 373939fedbdfa23053bd2c0f3722c8ba96b2f82e Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:49:21 -0700 Subject: [PATCH] Improve error handling across deal sources and increase CheapShark page size Add proper error handling for price parsing, date parsing, and HTTP status codes in CheapShark, Epic, and ITAD integrations. Fix Epic dedup IDs to distinguish current vs upcoming offers. Bump CheapShark fetch page size from 10 to 60. Co-Authored-By: Claude Opus 4.6 --- cmd/pastel/main.go | 4 ++-- internal/deals/cheapshark.go | 31 +++++++++++++++++++++---------- internal/deals/epic.go | 17 +++++++++++++---- internal/deals/itad.go | 15 ++++++++++++++- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/cmd/pastel/main.go b/cmd/pastel/main.go index b97ba82..1fc94e4 100644 --- a/cmd/pastel/main.go +++ b/cmd/pastel/main.go @@ -228,7 +228,7 @@ func main() { func populateInitialState(cfg *config.Config, db *database.DB) { // Record CheapShark deals without posting if cfg.HasSource("cheapshark") { - rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 10) + rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 60) if err != nil { slog.Warn("first run: cheapshark fetch failed", "error", err) } else { @@ -290,7 +290,7 @@ func checkCheapShark(cfg *config.Config, db *database.DB, mx *matrix.Client, con slog.Debug("checking cheapshark deals") conv.EnsureRates() - rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 10) + rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 60) if err != nil { slog.Error("cheapshark fetch failed", "error", err) return diff --git a/internal/deals/cheapshark.go b/internal/deals/cheapshark.go index 70290d9..3ff7d41 100644 --- a/internal/deals/cheapshark.go +++ b/internal/deals/cheapshark.go @@ -49,12 +49,12 @@ type cheapSharkRaw struct { // FetchCheapSharkDeals fetches deals from CheapShark API. func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, error) { - url := fmt.Sprintf( + reqURL := fmt.Sprintf( "https://www.cheapshark.com/api/1.0/deals?storeID=1,7,11,23&upperPrice=%d&sortBy=recent&desc=1&pageSize=%d", int(maxPrice), pageSize, ) - resp, err := http.Get(url) + resp, err := http.Get(reqURL) if err != nil { return nil, fmt.Errorf("cheapshark request failed: %w", err) } @@ -71,11 +71,17 @@ func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, err var deals []CheapSharkDeal for _, d := range raw { - sale, _ := strconv.ParseFloat(d.SalePrice, 64) - normal, _ := strconv.ParseFloat(d.NormalPrice, 64) - savings, _ := strconv.ParseFloat(d.Savings, 64) + sale, err1 := strconv.ParseFloat(d.SalePrice, 64) + normal, err2 := strconv.ParseFloat(d.NormalPrice, 64) + savings, err3 := strconv.ParseFloat(d.Savings, 64) rating, _ := strconv.ParseFloat(d.DealRating, 64) + if err1 != nil || err2 != nil || err3 != nil { + slog.Warn("cheapshark: skipping deal with unparseable prices", "title", d.Title, + "salePrice", d.SalePrice, "normalPrice", d.NormalPrice, "savings", d.Savings) + continue + } + storeName := storeNames[d.StoreID] if storeName == "" { storeName = "Unknown" @@ -103,12 +109,12 @@ func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, err // SearchCheapSharkDeals searches for current deals matching a title query. func SearchCheapSharkDeals(query string, maxResults int) ([]CheapSharkDeal, error) { - url := fmt.Sprintf( + reqURL := fmt.Sprintf( "https://www.cheapshark.com/api/1.0/deals?storeID=1,7,11,23&sortBy=savings&desc=1&pageSize=%d&title=%s", maxResults, url.QueryEscape(query), ) - resp, err := http.Get(url) + resp, err := http.Get(reqURL) if err != nil { return nil, fmt.Errorf("cheapshark search failed: %w", err) } @@ -125,9 +131,14 @@ func SearchCheapSharkDeals(query string, maxResults int) ([]CheapSharkDeal, erro var results []CheapSharkDeal for _, d := range raw { - sale, _ := strconv.ParseFloat(d.SalePrice, 64) - normal, _ := strconv.ParseFloat(d.NormalPrice, 64) - savings, _ := strconv.ParseFloat(d.Savings, 64) + sale, err1 := strconv.ParseFloat(d.SalePrice, 64) + normal, err2 := strconv.ParseFloat(d.NormalPrice, 64) + savings, err3 := strconv.ParseFloat(d.Savings, 64) + + if err1 != nil || err2 != nil || err3 != nil { + slog.Warn("cheapshark: skipping search result with unparseable prices", "title", d.Title) + continue + } if savings <= 0 { continue diff --git a/internal/deals/epic.go b/internal/deals/epic.go index f2b9e58..70b105c 100644 --- a/internal/deals/epic.go +++ b/internal/deals/epic.go @@ -3,6 +3,7 @@ package deals import ( "encoding/json" "fmt" + "log/slog" "net/http" "time" ) @@ -88,6 +89,10 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) { if slug == "" && len(elem.CatalogNs.Mappings) > 0 { slug = elem.CatalogNs.Mappings[0].PageSlug } + if slug == "" { + slog.Warn("epic: skipping game with no slug", "title", elem.Title, "id", elem.ID) + continue + } storeURL := fmt.Sprintf("https://store.epicgames.com/en-US/p/%s", slug) // Check current free offers @@ -96,8 +101,12 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) { if offer.DiscountSetting.DiscountPercentage != 0 { continue } - start, _ := time.Parse(time.RFC3339, offer.StartDate) - end, _ := time.Parse(time.RFC3339, offer.EndDate) + start, errS := time.Parse(time.RFC3339, offer.StartDate) + end, errE := time.Parse(time.RFC3339, offer.EndDate) + if errS != nil || errE != nil { + slog.Warn("epic: failed to parse offer dates", "title", elem.Title, "start", offer.StartDate, "end", offer.EndDate) + continue + } if start.After(now) || end.Before(now) { continue @@ -109,7 +118,7 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) { Desc: elem.Description, URL: storeURL, Upcoming: false, - DedupID: fmt.Sprintf("epic-%s", elem.ID), + DedupID: fmt.Sprintf("epic-current-%s", elem.ID), } if !end.IsZero() { game.EndDate = &end @@ -131,7 +140,7 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) { Desc: elem.Description, URL: storeURL, Upcoming: true, - DedupID: fmt.Sprintf("epic-%s", elem.ID), + DedupID: fmt.Sprintf("epic-upcoming-%s", elem.ID), } if offer.EndDate != "" { if end, err := time.Parse(time.RFC3339, offer.EndDate); err == nil { diff --git a/internal/deals/itad.go b/internal/deals/itad.go index fdc199b..eda3dc8 100644 --- a/internal/deals/itad.go +++ b/internal/deals/itad.go @@ -166,6 +166,12 @@ func LookupHistoricalLows(apiKey string, steamAppIDs []string) (map[string]bool, continue } + if resp.StatusCode != http.StatusOK { + slog.Warn("itad lookup returned non-200", "appid", appID, "status", resp.StatusCode) + resp.Body.Close() + continue + } + var lookup struct { Found bool `json:"found"` Game struct { @@ -195,7 +201,10 @@ func LookupHistoricalLows(apiKey string, steamAppIDs []string) (map[string]bool, itadToSteam[itadID] = steamID } - body, _ := json.Marshal(gameIDs) + body, err := json.Marshal(gameIDs) + if err != nil { + return result, fmt.Errorf("itad overview marshal failed: %w", err) + } url := fmt.Sprintf("https://api.isthereanydeal.com/games/overview/v2?key=%s", apiKey) resp, err := http.Post(url, "application/json", bytes.NewReader(body)) if err != nil { @@ -203,6 +212,10 @@ func LookupHistoricalLows(apiKey string, steamAppIDs []string) (map[string]bool, } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return result, fmt.Errorf("itad overview returned status %d", resp.StatusCode) + } + var overview struct { Prices []struct { ID string `json:"id"`