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 using multiple // sort strategies to ensure both new and high-value deals are captured. func FetchCheapSharkDeals(maxPrice float64, pageSize int) ([]CheapSharkDeal, error) { // Fetch recent deals and top-rated deals, then merge. // CheapShark's desc=0 (default) returns highest values first for savings/DealRating. queries := []string{ fmt.Sprintf( "https://www.cheapshark.com/api/1.0/deals?storeID=1,7,11,23&upperPrice=%d&sortBy=recent&pageSize=%d", int(maxPrice), pageSize, ), fmt.Sprintf( "https://www.cheapshark.com/api/1.0/deals?storeID=1,7,11,23&upperPrice=%d&sortBy=DealRating&pageSize=%d", int(maxPrice), pageSize, ), } seen := make(map[string]bool) var allDeals []CheapSharkDeal for _, reqURL := range queries { fetched, err := fetchCheapSharkPage(reqURL) if err != nil { slog.Warn("cheapshark: fetch failed for query", "url", reqURL, "error", err) continue } for _, d := range fetched { if seen[d.DedupID] { continue } seen[d.DedupID] = true allDeals = append(allDeals, d) } } if len(allDeals) == 0 { return nil, fmt.Errorf("cheapshark: all queries failed or returned no results") } return allDeals, nil } // fetchCheapSharkPage fetches and parses a single CheapShark API page. func fetchCheapSharkPage(reqURL string) ([]CheapSharkDeal, error) { 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 }