package api import ( "encoding/json" "fmt" "golang.org/x/net/html" "io" "log/slog" "net/http" "regexp" "strings" ) type SteamStruct struct { url string baseUrl string apiUrl string idPrefix string headers map[string]string deals DealsMap logger *slog.Logger } func NewSteamApi(logger *slog.Logger) SteamStruct { steam := SteamStruct{ url: "https://store.steampowered.com/search/results?force_infinite=1&maxprice=free&specials=1&category1=998", baseUrl: "https://store.steampowered.com/app/", apiUrl: "https://store.steampowered.com/api/appdetails?appids=", idPrefix: "steam-", headers: make(map[string]string), deals: make(map[string]Deal), logger: logger, } steam.headers["Accept-Language"] = "en" steam.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0" return steam } type steamApiBodyGame struct { Success bool `json:"success"` Data struct { Type string `json:"type"` Name string `json:"name"` IsFree bool `json:"is_free"` HeaderImage string `json:"header_image"` PriceOverview struct { Currency string `json:"currency"` Initial int `json:"initial"` Final int `json:"final"` DiscountPercent int `json:"discount_percent"` InitialFormatted string `json:"initial_formatted"` FinalFormatted string `json:"final_formatted"` } `json:"price_overview"` Platforms struct { Windows bool `json:"windows"` Mac bool `json:"mac"` Linux bool `json:"linux"` } `json:"platforms"` } `json:"data"` } type steamApiBody map[string]steamApiBodyGame func (e SteamStruct) Load() error { client := &http.Client{} reqStore, err := http.NewRequest("GET", e.url, nil) if err != nil { return err } for key, value := range e.headers { reqStore.Header.Set(key, value) } resStore, err := client.Do(reqStore) if err != nil { return err } bodyStore := html.NewTokenizer(resStore.Body) // could also search over each #search_resultsRows element instead of regex regexAppid, err := regexp.Compile(`https://store\.steampowered\.com/app/(\d+)`) if err != nil { return err } var appIDs []string func() { for { tt := bodyStore.Next() switch { case tt == html.ErrorToken: // file end or error return case tt == html.StartTagToken: t := bodyStore.Token() if t.Data != "a" { // only tag continue } for _, a := range t.Attr { if a.Key != "href" { continue } appID := regexAppid.FindStringSubmatch(a.Val) if len(appID) < 1 { continue } appIDs = append(appIDs, appID[1]) } } } }() for _, appID := range appIDs { reqApi, err := http.NewRequest("GET", fmt.Sprintf("%v%v", e.apiUrl, appID), nil) if err != nil { return err } for key, value := range e.headers { reqApi.Header.Set(key, value) } resApi, err := client.Do(reqApi) if err != nil { return err } bodyApi, err := io.ReadAll(resApi.Body) if err != nil { return err } var data steamApiBody err = json.Unmarshal(bodyApi, &data) if err != nil { return err } if game, ok := data[appID]; ok { if game.Data.Type != "game" { continue } image := game.Data.HeaderImage id := fmt.Sprintf("%v%v", e.idPrefix, appID) title := strings.TrimSpace(game.Data.Name) url := fmt.Sprintf("%v%v", e.baseUrl, appID) e.deals[id] = Deal{ Id: id, Title: title, Url: url, Image: image, } } } return nil } func (e SteamStruct) Get() []Deal { var deals []Deal for _, deal := range e.deals { deals = append(deals, deal) } return deals }