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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user