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>
189 lines
5.1 KiB
Go
189 lines
5.1 KiB
Go
package deals
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
)
|
|
|
|
var storeNames = map[string]string{
|
|
"1": "Steam",
|
|
"7": "GOG",
|
|
"11": "Humble Store",
|
|
"23": "GreenManGaming",
|
|
}
|
|
|
|
type CheapSharkDeal struct {
|
|
DealID string
|
|
GameID string
|
|
Title string
|
|
SalePrice float64
|
|
NormalPrice float64
|
|
Savings float64
|
|
DealRating float64
|
|
StoreID string
|
|
StoreName string
|
|
LastChange int64
|
|
SteamAppID string
|
|
DealURL string
|
|
IsHistLow bool
|
|
DedupID string
|
|
}
|
|
|
|
type cheapSharkRaw struct {
|
|
DealID string `json:"dealID"`
|
|
GameID string `json:"gameID"`
|
|
Title string `json:"title"`
|
|
SalePrice string `json:"salePrice"`
|
|
NormalPrice string `json:"normalPrice"`
|
|
Savings string `json:"savings"`
|
|
DealRating string `json:"dealRating"`
|
|
StoreID string `json:"storeID"`
|
|
LastChange int64 `json:"lastChange"`
|
|
SteamAppID string `json:"steamAppID"`
|
|
}
|
|
|
|
// FetchCheapSharkDeals fetches deals from CheapShark API.
|
|
func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, error) {
|
|
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(reqURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cheapshark request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("cheapshark returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var raw []cheapSharkRaw
|
|
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
|
return nil, fmt.Errorf("cheapshark decode failed: %w", err)
|
|
}
|
|
|
|
var deals []CheapSharkDeal
|
|
for _, d := range raw {
|
|
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"
|
|
}
|
|
|
|
deals = append(deals, CheapSharkDeal{
|
|
DealID: d.DealID,
|
|
GameID: d.GameID,
|
|
Title: d.Title,
|
|
SalePrice: sale,
|
|
NormalPrice: normal,
|
|
Savings: savings,
|
|
DealRating: rating,
|
|
StoreID: d.StoreID,
|
|
StoreName: storeName,
|
|
LastChange: d.LastChange,
|
|
SteamAppID: d.SteamAppID,
|
|
DealURL: fmt.Sprintf("https://www.cheapshark.com/redirect?dealID=%s", d.DealID),
|
|
DedupID: fmt.Sprintf("cheapshark-%s-%d", d.GameID, d.LastChange),
|
|
})
|
|
}
|
|
|
|
return deals, nil
|
|
}
|
|
|
|
// SearchCheapSharkDeals searches for current deals matching a title query.
|
|
func SearchCheapSharkDeals(query string, maxResults int) ([]CheapSharkDeal, error) {
|
|
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(reqURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cheapshark search failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("cheapshark returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var raw []cheapSharkRaw
|
|
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
|
return nil, fmt.Errorf("cheapshark decode failed: %w", err)
|
|
}
|
|
|
|
var results []CheapSharkDeal
|
|
for _, d := range raw {
|
|
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
|
|
}
|
|
|
|
storeName := storeNames[d.StoreID]
|
|
if storeName == "" {
|
|
storeName = "Unknown"
|
|
}
|
|
|
|
results = append(results, CheapSharkDeal{
|
|
DealID: d.DealID,
|
|
GameID: d.GameID,
|
|
Title: d.Title,
|
|
SalePrice: sale,
|
|
NormalPrice: normal,
|
|
Savings: savings,
|
|
StoreID: d.StoreID,
|
|
StoreName: storeName,
|
|
DealURL: fmt.Sprintf("https://www.cheapshark.com/redirect?dealID=%s", d.DealID),
|
|
})
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// FilterCheapSharkDeals filters deals by rating, discount, and price thresholds.
|
|
func FilterCheapSharkDeals(deals []CheapSharkDeal, minRating float64, minDiscount int, maxPrice float64) []CheapSharkDeal {
|
|
var filtered []CheapSharkDeal
|
|
for _, d := range deals {
|
|
discount := int(math.Floor(d.Savings))
|
|
if discount < minDiscount {
|
|
slog.Debug("cheapshark: skipping (low discount)", "title", d.Title, "discount", discount)
|
|
continue
|
|
}
|
|
if d.DealRating < minRating && d.DealRating != 0 {
|
|
slog.Debug("cheapshark: skipping (low rating)", "title", d.Title, "rating", d.DealRating)
|
|
continue
|
|
}
|
|
if d.SalePrice > maxPrice {
|
|
slog.Debug("cheapshark: skipping (too expensive)", "title", d.Title, "price", d.SalePrice)
|
|
continue
|
|
}
|
|
filtered = append(filtered, d)
|
|
}
|
|
return filtered
|
|
}
|