The bot stopped posting because sortBy=recent only returned low-discount deals, and desc=1 was inverting the sort order for savings/DealRating queries. Now fetches both recent and top-rated deals, merges with dedup. Lowered default thresholds to 20% min discount, 7.0 min rating, and $45 max price. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 lines
6.1 KiB
Go
225 lines
6.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 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
|
|
}
|