Add eBay Browse API integration with daily call quota

eBay marketplaces are now polled through eBay's official Buy > Browse API (client-credentials OAuth2) instead of an Apify scraper actor; Apify still handles Yahoo JP and Mercari. Browse API calls are tracked per day in a new ebay_api_usage table and capped (default 5000, configurable) on eBay's Pacific-time reset clock, so polling halts before the limit is hit. Credentials live in config.toml [ebay] and are overridable via /settings, which also surfaces the day's running call count.

Also carries the server.secure_cookies config plumbing (field, accessor, example) consumed by the following commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-14 12:10:39 -07:00
parent cfa01bd4ef
commit 1ae2c50b9a
12 changed files with 1092 additions and 262 deletions

94
internal/ebay/types.go Normal file
View File

@@ -0,0 +1,94 @@
package ebay
import "strings"
// SearchParams is the input to a single Browse API item_summary/search call.
// It is provider-specific and is carried as the opaque input payload of a
// scheduler plan, mirroring how Apify actor inputs are carried.
type SearchParams struct {
// MarketplaceID is an eBay marketplace identifier such as EBAY_US.
MarketplaceID string
// Query is the keyword search string. Required; the Browse API rejects
// an empty q.
Query string
// ListingType is Veola's vocabulary ("all", "bin"/"buy_it_now",
// "auction"); it is mapped to a buyingOptions filter.
ListingType string
// Limit caps the number of results requested (Browse API max is 200).
Limit int
}
// Listing is one normalized active eBay listing. The scheduler converts these
// into the shared apify.UnifiedResult shape so the rest of the pipeline
// (dedup, filter, alert) is provider-agnostic.
type Listing struct {
Title string
Price float64
Currency string
URL string
Store string
ImageURL string
}
// MarketplaceID maps a Veola marketplace string (e.g. "ebay.com",
// "ebay.co.uk") to an eBay Browse API marketplace identifier. Unknown or
// bare "ebay" values fall back to EBAY_US.
func MarketplaceID(marketplace string) string {
m := strings.ToLower(strings.TrimSpace(marketplace))
switch {
case strings.Contains(m, "ebay.co.uk"):
return "EBAY_GB"
case strings.Contains(m, "ebay.de"):
return "EBAY_DE"
case strings.Contains(m, "ebay.com.au"):
return "EBAY_AU"
case strings.Contains(m, "ebay.ca"):
return "EBAY_CA"
case strings.Contains(m, "ebay.fr"):
return "EBAY_FR"
case strings.Contains(m, "ebay.it"):
return "EBAY_IT"
case strings.Contains(m, "ebay.es"):
return "EBAY_ES"
case strings.Contains(m, "ebay.at"):
return "EBAY_AT"
case strings.Contains(m, "ebay.ch"):
return "EBAY_CH"
case strings.Contains(m, "ebay.ie"):
return "EBAY_IE"
case strings.Contains(m, "ebay.nl"):
return "EBAY_NL"
case strings.Contains(m, "ebay.com.hk"):
return "EBAY_HK"
case strings.Contains(m, "ebay.com.sg"):
return "EBAY_SG"
case strings.Contains(m, "ebay.com.my"):
return "EBAY_MY"
case strings.Contains(m, "ebay.ph"):
return "EBAY_PH"
case strings.Contains(m, "ebay.pl"):
return "EBAY_PL"
default:
// "ebay.com" and any bare/unknown eBay marketplace.
return "EBAY_US"
}
}
// IsEbayMarketplace reports whether a Veola marketplace string should be
// polled through the official eBay Browse API.
func IsEbayMarketplace(marketplace string) bool {
return strings.Contains(strings.ToLower(marketplace), "ebay")
}
// buyingOptionsFilter maps Veola's listing-type vocabulary to the Browse API
// "filter" query parameter. An empty string means no filter ("all").
func buyingOptionsFilter(listingType string) string {
switch strings.ToLower(strings.TrimSpace(listingType)) {
case "bin", "buy_it_now", "fixed_price":
return "buyingOptions:{FIXED_PRICE}"
case "auction":
return "buyingOptions:{AUCTION}"
default:
return ""
}
}