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) {
// 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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"`