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 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-03-20 01:49:21 -07:00
parent e452e36a63
commit 373939fedb
4 changed files with 50 additions and 17 deletions

View File

@@ -228,7 +228,7 @@ func main() {
func populateInitialState(cfg *config.Config, db *database.DB) { func populateInitialState(cfg *config.Config, db *database.DB) {
// Record CheapShark deals without posting // Record CheapShark deals without posting
if cfg.HasSource("cheapshark") { if cfg.HasSource("cheapshark") {
rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 10) rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 60)
if err != nil { if err != nil {
slog.Warn("first run: cheapshark fetch failed", "error", err) slog.Warn("first run: cheapshark fetch failed", "error", err)
} else { } else {
@@ -290,7 +290,7 @@ func checkCheapShark(cfg *config.Config, db *database.DB, mx *matrix.Client, con
slog.Debug("checking cheapshark deals") slog.Debug("checking cheapshark deals")
conv.EnsureRates() conv.EnsureRates()
rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 10) rawDeals, err := deals.FetchCheapSharkDeals(cfg.MaxPriceUSD, 60)
if err != nil { if err != nil {
slog.Error("cheapshark fetch failed", "error", err) slog.Error("cheapshark fetch failed", "error", err)
return return

View File

@@ -49,12 +49,12 @@ type cheapSharkRaw struct {
// FetchCheapSharkDeals fetches deals from CheapShark API. // FetchCheapSharkDeals fetches deals from CheapShark API.
func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, error) { 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", "https://www.cheapshark.com/api/1.0/deals?storeID=1,7,11,23&upperPrice=%d&sortBy=recent&desc=1&pageSize=%d",
int(maxPrice), pageSize, int(maxPrice), pageSize,
) )
resp, err := http.Get(url) resp, err := http.Get(reqURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("cheapshark request failed: %w", err) return nil, fmt.Errorf("cheapshark request failed: %w", err)
} }
@@ -71,11 +71,17 @@ func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, err
var deals []CheapSharkDeal var deals []CheapSharkDeal
for _, d := range raw { for _, d := range raw {
sale, _ := strconv.ParseFloat(d.SalePrice, 64) sale, err1 := strconv.ParseFloat(d.SalePrice, 64)
normal, _ := strconv.ParseFloat(d.NormalPrice, 64) normal, err2 := strconv.ParseFloat(d.NormalPrice, 64)
savings, _ := strconv.ParseFloat(d.Savings, 64) savings, err3 := strconv.ParseFloat(d.Savings, 64)
rating, _ := strconv.ParseFloat(d.DealRating, 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] storeName := storeNames[d.StoreID]
if storeName == "" { if storeName == "" {
storeName = "Unknown" storeName = "Unknown"
@@ -103,12 +109,12 @@ func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, err
// SearchCheapSharkDeals searches for current deals matching a title query. // SearchCheapSharkDeals searches for current deals matching a title query.
func SearchCheapSharkDeals(query string, maxResults int) ([]CheapSharkDeal, error) { 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", "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), maxResults, url.QueryEscape(query),
) )
resp, err := http.Get(url) resp, err := http.Get(reqURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("cheapshark search failed: %w", err) return nil, fmt.Errorf("cheapshark search failed: %w", err)
} }
@@ -125,9 +131,14 @@ func SearchCheapSharkDeals(query string, maxResults int) ([]CheapSharkDeal, erro
var results []CheapSharkDeal var results []CheapSharkDeal
for _, d := range raw { for _, d := range raw {
sale, _ := strconv.ParseFloat(d.SalePrice, 64) sale, err1 := strconv.ParseFloat(d.SalePrice, 64)
normal, _ := strconv.ParseFloat(d.NormalPrice, 64) normal, err2 := strconv.ParseFloat(d.NormalPrice, 64)
savings, _ := strconv.ParseFloat(d.Savings, 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 { if savings <= 0 {
continue continue

View File

@@ -3,6 +3,7 @@ package deals
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"time" "time"
) )
@@ -88,6 +89,10 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) {
if slug == "" && len(elem.CatalogNs.Mappings) > 0 { if slug == "" && len(elem.CatalogNs.Mappings) > 0 {
slug = elem.CatalogNs.Mappings[0].PageSlug 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) storeURL := fmt.Sprintf("https://store.epicgames.com/en-US/p/%s", slug)
// Check current free offers // Check current free offers
@@ -96,8 +101,12 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) {
if offer.DiscountSetting.DiscountPercentage != 0 { if offer.DiscountSetting.DiscountPercentage != 0 {
continue continue
} }
start, _ := time.Parse(time.RFC3339, offer.StartDate) start, errS := time.Parse(time.RFC3339, offer.StartDate)
end, _ := time.Parse(time.RFC3339, offer.EndDate) 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) { if start.After(now) || end.Before(now) {
continue continue
@@ -109,7 +118,7 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) {
Desc: elem.Description, Desc: elem.Description,
URL: storeURL, URL: storeURL,
Upcoming: false, Upcoming: false,
DedupID: fmt.Sprintf("epic-%s", elem.ID), DedupID: fmt.Sprintf("epic-current-%s", elem.ID),
} }
if !end.IsZero() { if !end.IsZero() {
game.EndDate = &end game.EndDate = &end
@@ -131,7 +140,7 @@ func FetchEpicFreeGames() ([]EpicFreeGame, error) {
Desc: elem.Description, Desc: elem.Description,
URL: storeURL, URL: storeURL,
Upcoming: true, Upcoming: true,
DedupID: fmt.Sprintf("epic-%s", elem.ID), DedupID: fmt.Sprintf("epic-upcoming-%s", elem.ID),
} }
if offer.EndDate != "" { if offer.EndDate != "" {
if end, err := time.Parse(time.RFC3339, offer.EndDate); err == nil { if end, err := time.Parse(time.RFC3339, offer.EndDate); err == nil {

View File

@@ -166,6 +166,12 @@ func LookupHistoricalLows(apiKey string, steamAppIDs []string) (map[string]bool,
continue 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 { var lookup struct {
Found bool `json:"found"` Found bool `json:"found"`
Game struct { Game struct {
@@ -195,7 +201,10 @@ func LookupHistoricalLows(apiKey string, steamAppIDs []string) (map[string]bool,
itadToSteam[itadID] = steamID 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) url := fmt.Sprintf("https://api.isthereanydeal.com/games/overview/v2?key=%s", apiKey)
resp, err := http.Post(url, "application/json", bytes.NewReader(body)) resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil { if err != nil {
@@ -203,6 +212,10 @@ func LookupHistoricalLows(apiKey string, steamAppIDs []string) (map[string]bool,
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return result, fmt.Errorf("itad overview returned status %d", resp.StatusCode)
}
var overview struct { var overview struct {
Prices []struct { Prices []struct {
ID string `json:"id"` ID string `json:"id"`