Help us shape the future of Go! Take the 2026 Go Developer Survey

cardmarket

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 20 Imported by: 0

README

go-cardmarket

A small Go client for the Cardmarket API (catalog & pricing), plus the two tools that read its published catalog files. It handles OAuth 1.0a request signing, rate-limit backoff, and resilient HTTP via retryablehttp.

Entities are named as Cardmarket's own documentation names them — Product, Expansion, Article — so a field can be looked up in one place rather than translated first.


Install

go get github.com/mtgban/go-cardmarket

Quick start

package main

import (
    "context"
    "fmt"

    "github.com/mtgban/go-cardmarket"
)

func main() {
    c := cardmarket.NewClient("<APP_TOKEN>", "<APP_SECRET>")

    expansions, err := c.Expansions(context.TODO(), cardmarket.GamePokemon)
    if err != nil {
        // <handle err>
    }
    fmt.Println("expansions:", len(expansions))
}

Two transports, two naming conventions

Cardmarket publishes its catalog twice over, and the method name says which one you are reading:

  • Bare entity names (Product, Expansions, Articles, …) are the signed API at apiv2.cardmarket.com. They need app credentials and count against a daily request allowance.
  • Download* (DownloadPriceGuide, DownloadProductListSingles, …) are the static files at downloads.s3.cardmarket.com. No credentials, no allowance, a whole game per request.

Prefer the downloads wherever they answer the question. Walking a game through the API is thousands of requests; the same data as a file is one.


Authentication

NewClient(appToken, appSecret) wires an http.RoundTripper that signs every request with OAuth 1.0a (HMAC-SHA1), as Cardmarket's auth documentation describes. Only the app token and secret are needed for the public catalog requests; the access-token pair stays empty.

RequestNo() reports how many requests the client has made, which is what to watch against the daily allowance.


API helpers

  • Expansions
    • Expansions(ctx, game) ([]Expansion, error) — every expansion of a game
    • ExpansionSingles(ctx, expansionID) ([]Product, error) — every single card in one
  • Products
    • Product(ctx, productID) (*Product, error)
  • Articles (listings)
    • Articles(ctx, productID, query, page, maxResults) (articles []Article, total int, capped bool, err error)

query is an ArticleQuery, and what goes in it is yours to choose. This package spells the filters and picks none of them: what condition is worth pricing, and which sellers are worth reading, are judgements about your own use, not facts about the marketplace.

articles, total, _, err := c.Articles(ctx, productID, cardmarket.ArticleQuery{
    MinCondition: cardmarket.ConditionGood,
    MinUserScore: cardmarket.UserScoreGood,
    Language:     cardmarket.LanguageEnglish,
    Signed:       cardmarket.None,
    Altered:      cardmarket.None,
}, 0, cardmarket.MaxEntities)

Each field is typed, so a misspelt filter or a language passed where a score belongs does not compile, and Articles refuses a value outside the package's constants (Language(99)) rather than send it. The flags take the same Filter as a storefront link: Only sends true, None sends false.

Cardmarket's filters fail open. A parameter that does not apply to a game, or a value it does not recognise, is ignored and the unfiltered listing is answered — not an error. Check each Article's own flags rather than trusting the request.

Pages start at zero and the API requires both bounds — asking for a page size without a start is refused. MaxEntities (100) is the largest page it serves.

total is the listing's full count, read from the response's Content-Range, so a caller can stop once it has paged through everything; it is 0 when the header is missing. capped means Cardmarket stopped counting at 1000 and the real count is unknown.

Two shapes of one Product

The marketplace answers the same entity two ways, and which request built a Product decides which half of it is filled:

filled by fields
ExpansionSingles ExpansionName, ExpansionIcon
Product Expansion (nested), PriceGuide

A product read one way carries nothing of the other's.

Identifiers are typed

Game, Language, Country, Condition, UserType and UserScore are each their own type. The API spends small integers freely — a game, a language, a country and a seller score are all 3 to a compiler told nothing else, and they ride on the same request — so naming them makes the wrong one a compile error instead of a plausible page.

Each has a …Name and a …FromName, read from one table so an entry cannot be added to one direction and not the other — all but UserType, whose constants are already the words the marketplace uses:

cardmarket.GameFromName("gundam")                    // GameGundam
cardmarket.GameName(cardmarket.GameGundam)           // "Gundam"
cardmarket.LanguageName(cardmarket.LanguageJapanese) // "Japanese"

GameName covers every game the marketplace sells, not only the ones any particular caller prices — including the abbreviations the storefront actually uses (FoW, WoW, Vanguard, Spoils), which are not interchangeable with the full names. An unknown name answers 0, and so builds no link at all.

The language and country numbers are upstream's. Three countries — Singapore (29), Canada (33), Japan (36) — are on the marketplace's own seller-country filter but missing from its documentation; they are included here, because filtering by the documented list alone silently drops every seller in them.

Condition and UserScore are declared best to worst, which is the order minCondition and minUserScore mean by "or better". ConditionFromName takes either the code or the word, so a caller holding an Article's own condition does not have to know which it has.

BuildURL(game, productID, opt) and SearchURL(game, name, opt) take the same URLOption, so a filter added to one cannot quietly skip the other:

url := cardmarket.BuildURL(game, id, cardmarket.URLOption{
    Foil:      cardmarket.Only,
    Signed:    cardmarket.None,
    Altered:   cardmarket.None,
    Language:  cardmarket.LanguageEnglish,
    Affiliate: "mtgban",
})

Every flag is three-valued, because the marketplace's are: Any says nothing, Only asks for the listings that carry it, None for the ones that do not. Any is the zero value, so URLOption{} narrows nothing.

None earns its place on the finishes too — a non-foil price that links to a page showing foils is quoting from a shelf the reader cannot see.

Every flag is a bare parameter (isFoil=N, isSigned=N, isAltered=N).

SellerTypes and SellerCountries narrow by who is selling. Each is a list, as the storefront's own controls are, and is written as one parameter with its values joined by commas in ascending order, whatever order they were named in:

cardmarket.URLOption{
    SellerTypes:     []cardmarket.UserType{cardmarket.UserTypePowerseller},
    SellerCountries: []cardmarket.Country{cardmarket.CountryNetherlands, cardmarket.CountryGermany},
}

The storefront numbers the seller types where the API names them, so the UserType words are translated on the way out: private is 0, professional 1 and powerseller 2, as an Article's IsCommercial numbers them.

Language asks the page to prefer one language, and sends nothing at zero. Affiliate tags the link with utm_source, utm_medium and utm_campaign, and sends nothing when empty.

Both builders return "" for a number that names no game, rather than a link to a path the site does not serve.

Finish flags are per-game

A Magic Article carries IsFoil and neither of the others. A Pokémon Article carries IsFirstEd and IsReverseHolo and no IsFoil at all. A flag absent from a game's listings decodes false, so read the one its game uses.


Published catalog files

  • DownloadPriceGuide(ctx, game) ([]PriceGuide, error)
  • DownloadProductListSingles(ctx, game) ([]ProductList, error)
  • DownloadProductListSealed(ctx, game) ([]ProductList, error)

Each has a …File form — DownloadPriceGuideFile(ctx, game) (*PriceGuideFile, error), and likewise for the two product lists — that also returns the file's Version and the CreatedAt Cardmarket stamped it with, so a caller can refuse a file that has stopped updating. How old is too old is yours to say: the price guide and the product lists are rebuilt on different schedules. An unreadable CreatedAt is an error in the …File forms; the plain ones never read it.

PriceGuide.SecondPrinting(game) reads the printing sold beside the default one under whichever heading the game's guide publishes it — most games publish a foil, Pokémon publishes a reverse holo.


The catalog file

Catalog is the id map a price run reads instead of walking the API. mkmcatalog writes it for every game, Magic included, from the API's own answers:

field
expansionId, name every product
number, rarity where the marketplace gives one
version where the product carries one; see ProductVersion
expansion code the marketplace's own abbreviation

It names products and nothing else: which printing of a datastore a product stands for is the reader's to work out.

catalog, err := cardmarket.LoadCatalog(reader)
product := catalog.Data.Products[571798]

meta.date is carried through and not enforced. A file naming no products is refused — that is the shape a truncated download takes.


Commands

mkmcatalog

Walks one game's catalog and writes the file above.

go build ./cmd/mkmcatalog

MKM_APP_TOKEN=... MKM_APP_SECRET=... \
  ./mkmcatalog -game pokemon -output cardmarket_catalog.json

Flags:

  • -game — any game by name (magic, lorcana, riftbound, gundam, pokemon, …); see GameFromName
  • -previous — a catalog to carry an unanswerable expansion's products over from, read only when the walk leaves one. The API intermittently answers 5xx for a particular expansion however many times it is asked; rather than throwing away the whole walk over one shelf, its products are taken from this file and the expansion is named in meta.unwalked. A shelf added since the last good walk has nothing to carry over and is recorded with no products rather than failing the run — it would have been missing either way, and failing would leave every other shelf a day stale too.
  • -output — a path, or a b2://bucket/object one; an .xz suffix compresses it

A b2:// output reads B2_APPLICATION_KEY_ID and B2_APPLICATION_KEY — the same names the b2 command line uses, so one pair of credentials works for both.

A few of Magic's shelves are left out of the catalog altogether: the vendor tokens and alters, accessories, the box toppers and 9x12 oversized promos, player cards and the marketplace's own series, matched by name in skippedShelves or by a TOK<n> code. A set's own <Set>: Tokens shelf and the Judge Program Tokens are kept.

An expansion the API will not answer for gets one more try at the end of the walk before it is carried over. The run fails, rather than publish a catalog that silently unprices whatever it dropped, when:

  • the walk does not finish within its three-hour deadline
  • more than a tenth of the expansions go unanswered
  • an expansion needs carrying over and -previous is missing or unreadable
mkmpriceguide

Downloads a published file to stdout. Needs no credentials.

go build ./cmd/mkmpriceguide
./mkmpriceguide -game pokemon -mode prices > pokemon-prices.json

Flags:

  • -game — the game by name (see the Game* constants)
  • -mode — prices (default), singles, sealed

License

MIT

Documentation

Overview

Package cardmarket reads Cardmarket's marketplace: the signed API that answers for products, expansions and listings, and the catalog files the site publishes for anyone to download. The entities are named as https://apiv2.cardmarket.com/ws/documentation names them, so a field can be looked up in one place rather than translated first.

Index

Constants

View Source
const (

	// MaxEntities is how many results one request may ask for
	MaxEntities = 100
)

Variables

This section is empty.

Functions

func BuildURL

func BuildURL(game Game, idProduct int, opt URLOption) string

BuildURL builds the storefront link for a product, narrowed and tagged by whatever the option carries.

func ConditionName added in v0.3.0

func ConditionName(condition Condition) string

ConditionName returns the condition spelled out, or "" for a code the marketplace does not use.

func CountryName added in v0.3.0

func CountryName(country Country) string

CountryName returns the country's name in English, or "" for a number the marketplace carries no country for.

func GameName

func GameName(game Game) string

GameName returns the game as Cardmarket spells it in an address, or "" for a number it carries no game for.

Every game the marketplace sells is named here, not only the ones a particular caller prices. Which games are worth reading is the caller's business; which games exist is Cardmarket's.

func LanguageName added in v0.3.0

func LanguageName(language Language) string

LanguageName returns the language's name in English, as the API's own languageName field spells it, or "" for a number it carries no language for.

func ParseContentRange added in v0.2.0

func ParseContentRange(header string) (total int, capped bool, ok bool)

ParseContentRange reads the total listing count out of a Content-Range header ("0-29/33", or "0-99/1000+" once Cardmarket stops counting past its own cap). Capped reports the latter case: total is 1000, the highest figure the API ever reports, not the real count, which is unknowable past it. An empty or unrecognized header reports ok=false rather than a misleading zero, and so does one no listing could have sent: a signed or out of range number, or a range that ends before it starts.

func ProductVersion

func ProductVersion(product *Product) int

ProductVersion reads that index off a product, or zero where it carries none. The marketplace publishes it in the name for some games and only in the address for others - of the catalogs we walk, 31,523 of Yu-Gi-Oh's 86,628 products name it and not one of Pokemon's 72,752 does, though its shelves are full of it - so both are read and the name wins.

func SearchURL

func SearchURL(game Game, name string, opt URLOption) string

SearchURL returns the catalog search for a product name, the fallback for a card whose Cardmarket product id is not known. It takes the same option as BuildURL and is empty for the same games, so adding a filter to one cannot quietly leave the other behind.

func UserScoreName added in v0.3.0

func UserScoreName(score UserScore) string

UserScoreName returns the score spelled out, or "" for a number the marketplace does not use.

Types

type APIError

type APIError struct {
	Description       string `json:"mkm_error_description"`
	InternalCode      any    `json:"internal_error_code"`
	PostDataField     any    `json:"post_data_field"`
	StatusCode        int    `json:"http_status_code"`
	StatusDescription string `json:"http_status_code_description"`
}

APIError is the body Cardmarket answers a rejected request with. The description is the only part meant for a person; the rest says which of the request it objected to.

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface.

type Article

type Article struct {
	IDArticle int `json:"idArticle"`
	IDProduct int `json:"idProduct"`
	Language  struct {
		IDLanguage   Language `json:"idLanguage"`
		LanguageName string   `json:"languageName"`
	} `json:"language"`
	Comments       string         `json:"comments"`
	Price          float64        `json:"price"`
	IDCurrency     int            `json:"idCurrency"`
	CurrencyCode   string         `json:"currencyCode"`
	Prices         []ArticlePrice `json:"prices,omitempty"`
	Count          int            `json:"count"`
	InShoppingCart bool           `json:"inShoppingCart"`
	Condition      Condition      `json:"condition"`
	Product        struct {
		Name        string `json:"enName"`
		LocName     string `json:"locName"`
		Expansion   string `json:"expansion"`
		IDExpansion int    `json:"idExpansion"`
		SetCode     string `json:"abbreviation"`
		Number      string `json:"nr"`
		Rarity      string `json:"rarity"`
		IDGame      Game   `json:"idGame"`
		ExpIcon     int    `json:"expIcon"`
	} `json:"product"`
	Seller ArticleSeller `json:"seller"`

	IsFoil        bool `json:"isFoil"`
	IsFirstEd     bool `json:"isFirstEd"`
	IsReverseHolo bool `json:"isReverseHolo"`
	IsSigned      bool `json:"isSigned"`
	IsAltered     bool `json:"isAltered"`

	Links []Link `json:"links,omitempty"`
}

Article is one seller's listing on a product.

The finish flags are the game's own and not one vocabulary: a Magic listing carries IsFoil and neither of the others, a Pokemon listing carries IsFirstEd and IsReverseHolo and no IsFoil at all. A flag absent from a game's listings decodes false, so read the one its game uses.

type ArticlePrice

type ArticlePrice struct {
	Price        float64 `json:"price"`
	IDCurrency   int     `json:"idCurrency"`
	CurrencyCode string  `json:"currencyCode"`
}

ArticlePrice is one listing's price in one currency. A listing is quoted in the seller's currency and in every currency the marketplace converts it to.

type ArticleQuery added in v0.4.0

type ArticleQuery struct {
	// UserType narrows to one kind of seller; UserTypeCommercial includes
	// powersellers.
	UserType UserType
	// MinUserScore and MinCondition each mean that or better.
	MinUserScore UserScore
	MinCondition Condition
	// Language is the listing's language.
	Language Language
	// SellerCountry is the seller's country of origin. The documentation
	// takes one.
	SellerCountry Country

	// Foil, FirstEd and ReverseHolo narrow to a printing, and Signed and
	// Altered by what was done to the card, as the Article's flags of the
	// same names. FirstEd and ReverseHolo are not in the documentation.
	Foil        Filter
	FirstEd     Filter
	ReverseHolo Filter
	Signed      Filter
	Altered     Filter

	// MinAvailable is the fewest copies a seller holds across all their
	// listings matching the rest of the query, not in any one listing.
	MinAvailable int
}

ArticleQuery is what an Articles request narrows the listings by. The zero value asks for every listing, and each field set adds the parameter the documentation names. Articles refuses a value outside this package's own constants rather than send one the API would ignore.

The API's filters fail open: one that does not apply to a game is ignored and the unfiltered listing answered, not an error. Check each Article's own fields rather than trusting the request.

type ArticleSeller

type ArticleSeller struct {
	IDUser       int    `json:"idUser"`
	Username     string `json:"username"`
	IsCommercial int    `json:"isCommercial"`
	IsSeller     bool   `json:"isSeller"`
	Reputation   int    `json:"reputation"`
	SellCount    int    `json:"sellCount"`
	SoldItems    int    `json:"soldItems"`
	OnVacation   bool   `json:"onVacation"`
	// AvgShippingTime is in days, and RiskGroup and LossPercentage are the
	// marketplace's own read on whether an order will arrive.
	AvgShippingTime int    `json:"avgShippingTime"`
	RiskGroup       int    `json:"riskGroup"`
	LossPercentage  string `json:"lossPercentage"`
	Address         struct {
		Country string `json:"country"`
	} `json:"address"`
}

ArticleSeller is who is selling, minus the fields naming the person. The marketplace answers a seller's first name, email, phone, VAT number and legal information beside their trading record; none of that is needed to judge a price, so none of it is decoded and none of it can leak into a log of an article.

type Catalog

type Catalog struct {
	Meta CatalogMeta `json:"meta"`
	Data CatalogData `json:"data"`
}

Catalog is a published Cardmarket catalog: every singles product mapped to what the marketplace says of it, which is what a price run reads instead of walking the API.

cmd/mkmcatalog writes it for every game, Magic included, from the API's own answers. It names products and expansions and nothing else: which printing of a datastore a product stands for is the reader's to work out.

func LoadCatalog

func LoadCatalog(reader io.Reader) (*Catalog, error)

LoadCatalog reads a published catalog. A file naming no products is refused: it is the shape a truncated download and an unrelated JSON file both take, and pricing from it would quietly unprice a whole game.

type CatalogData

type CatalogData struct {
	Expansions map[int]CatalogExpansion `json:"expansions"`
	Products   map[int]CatalogProduct   `json:"products"`
}

CatalogData is the file's two tables. JSON has only string keys, so the ids are written as strings and read back as the numbers they are, which encoding/json does on its own for an integer-keyed map.

type CatalogExpansion

type CatalogExpansion struct {
	Name string `json:"name"`
	Code string `json:"code,omitempty"`
}

CatalogExpansion names one expansion of the catalog. Code is the marketplace's own abbreviation, which is where the foreign catalogs say what they are.

type CatalogMeta

type CatalogMeta struct {
	Date string `json:"date"`
	// Unwalked names the expansions whose products did not come from this
	// walk, because the API would not answer for them: they are the
	// previous catalog's, or absent altogether where it had none for the
	// expansion - a shelf added since the last good walk. A reader that
	// cares how fresh a shelf is has to be told, since neither case is
	// otherwise distinguishable from one walked today. Empty in the
	// ordinary case.
	Unwalked []int `json:"unwalked,omitempty"`
}

CatalogMeta says when the file was built and how much of it was.

type CatalogProduct

type CatalogProduct struct {
	ExpansionID int    `json:"expansionId"`
	Name        string `json:"name"`
	Number      string `json:"number,omitempty"`
	Rarity      string `json:"rarity,omitempty"`
	// Version is the index the marketplace counts a card's printings with
	// where one shelf sells several - the second Budew of a stamp
	// programme, the master-ball pattern beside the poke-ball one. It is
	// the only thing telling two products of one name and number apart, and
	// zero where the product carries none. See ProductVersion for where it
	// is read from, which is not the same field in every game.
	Version int `json:"version,omitempty"`
}

CatalogProduct is one product of the catalog: what the marketplace calls it and where it files it.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client reads Cardmarket's API, which signs every request with an app token and secret.

func NewClient

func NewClient(appToken, appSecret string) *Client

NewClient returns a client signing with the given app credentials.

The API is very sensitive to concurrent requests and answers a burst with 429s - measured as a concurrency limiter rather than a rate limiter: one request in flight at a time clears with no 429s at all. More in flight is faster and pays for it in retries: on 10-listing pages of Articles, 8 in flight ran about four times as fast as one, with a fifth to a half of their requests refused and retried, each counted by RequestNo. Every 429 observed carries a `Retry-After` of one second, so the backoff honours it (falling back to jittered exponential backoff, up to 2-10 seconds times the attempt number, for a response with none) rather than waiting a fixed multi-second interval regardless of what the server actually asked for. A 429 is retried up to rateLimitRetries times and a 5xx only serverErrorRetries, for the reasons given there; give long walks a context deadline, since neither is a bound anyone should rely on.

func (*Client) Articles

func (mkm *Client) Articles(ctx context.Context, id int, query ArticleQuery, page, maxResults int) (articles []Article, total int, capped bool, err error)

Articles returns the listings on a product, one page at a time, together with the listing's true total count (see ParseContentRange) so a caller can stop paginating once it's covered the total, or confirm a filter actually narrowed the result (see ArticleQuery). total is 0 when the header is missing or unparseable; capped reports Cardmarket's own 1000-result ceiling, past which the real count is unknowable. Pages start at zero, and the API requires both bounds: asking for a page size without a start is refused.

func (*Client) ExpansionSingles

func (mkm *Client) ExpansionSingles(ctx context.Context, id int) ([]Product, error)

ExpansionSingles returns every single card in one expansion.

func (*Client) Expansions

func (mkm *Client) Expansions(ctx context.Context, game Game) ([]Expansion, error)

Expansions returns every expansion of one game.

func (*Client) Product

func (mkm *Client) Product(ctx context.Context, id int) (*Product, error)

Product returns one catalog entry by id.

func (*Client) RequestNo

func (mkm *Client) RequestNo() int

RequestNo returns how many requests the client has made, which matters against Cardmarket's daily allowance.

type Condition added in v0.3.0

type Condition string

Condition is a card's condition, which the API spells as a code rather than a number. The constants are declared best to worst, which is the order minCondition means by "or better".

const (
	ConditionMint        Condition = "MT"
	ConditionNearMint    Condition = "NM"
	ConditionExcellent   Condition = "EX"
	ConditionGood        Condition = "GD"
	ConditionLightPlayed Condition = "LP"
	ConditionPlayed      Condition = "PL"
	ConditionPoor        Condition = "PO"
)

The conditions, best to worst.

func ConditionFromName added in v0.3.0

func ConditionFromName(name string) Condition

ConditionFromName is the inverse, matching case-insensitively, and also accepting the code itself so a caller holding an Article's own condition string does not have to know which of the two it has.

type Country added in v0.3.0

type Country int

Country is a seller's country of origin, as sellerCountry numbers them.

const (
	CountryAustria Country = iota + 1
	CountryBelgium
	CountryBulgaria
	CountrySwitzerland
	CountryCyprus
	CountryCzechRepublic
	CountryGermany
	CountryDenmark
	CountryEstonia
	CountrySpain
	CountryFinland
	CountryFrance
	CountryUnitedKingdom
	CountryGreece
	CountryHungary
	CountryIreland
	CountryItaly
	CountryLiechtenstein
	CountryLithuania
	CountryLuxembourg
	CountryLatvia
	CountryMalta
	CountryNetherlands
	CountryNorway
	CountryPoland
	CountryPortugal
	CountryRomania
	CountrySweden
	CountrySingapore
	CountrySlovenia
	CountrySlovakia

	CountryCanada

	CountryCroatia
	CountryJapan
	CountryIceland
)

The countries, in the order the marketplace numbers them. The blanks are numbers it answers for no country - the run is otherwise unbroken, and counting it keeps a country from being given a neighbour's number by a typo no reviewer would catch.

func CountryFromName added in v0.3.0

func CountryFromName(name string) Country

CountryFromName is the inverse, matching case-insensitively.

type Expansion

type Expansion struct {
	IDExpansion  int            `json:"idExpansion"`
	Name         string         `json:"enName"`
	Localization []Localization `json:"localization,omitempty"`
	// SetCode is the marketplace's own abbreviation, which is the only
	// field telling a foreign catalog from the English one it shadows.
	SetCode     string `json:"abbreviation"`
	Icon        int    `json:"icon"`
	ReleaseDate string `json:"releaseDate"`
	IsReleased  bool   `json:"isReleased"`
	IDGame      Game   `json:"idGame"`
	Links       []Link `json:"links,omitempty"`
}

Expansion is a set as Cardmarket files it.

type Filter added in v0.3.0

type Filter int

Filter is how a listing property narrows a storefront link or an Articles request: not at all, to the listings that carry it, or to the listings that do not. The marketplace's own controls are three-valued in exactly this way, and its zero is "any", so the zero value here narrows nothing.

const (
	Any Filter = iota
	Only
	None
)

The three states. A storefront link spells them 0, Y and N; the API leaves the parameter out, or sends true or false.

type Game added in v0.3.0

type Game int

Game is a game Cardmarket carries, as its API numbers them.

The documentation does not publish this table - /games answers it at runtime - so these are read off the published catalog files, whose paths are keyed by exactly this number: products_singles_24.json shelves "Gundam Single". TestGameIDs pins them, since a game inserted in the wrong place would renumber every game after it and price one game from another's file.

const (
	GameMagic Game = iota + 1
	GameWorldOfWarcraft
	GameYuGiOh

	GameTheSpoils
	GamePokemon
	GameForceOfWill
	GameCardfightVanguard
	GameFinalFantasy
	GameWeissSchwarz
	GameDragoborne
	GameMyLittlePony
	GameDragonBallSuper

	GameStarWarsDestiny
	GameFleshAndBlood
	GameDigimon
	GameOnePiece
	GameLorcana
	GameBattleSpiritsSaga
	GameStarWarsUnlimited
	GameRiftbound
	GameCyberpunk
	GameGundam
)

The games, in the order the API numbers them. The blanks are numbers it no longer answers for - a game delisted since leaves its id spent.

func GameFromName

func GameFromName(name string) Game

GameFromName is the inverse, matching case-insensitively so a caller can hand over the name it already knows a game by ("lorcana") instead of translating to an id first. An unknown name answers 0, which the URL builders reject: a game Cardmarket does not carry yields no link at all rather than one pointing at a path it does not serve.

type Language added in v0.3.0

type Language int

Language is a language the marketplace trades in, as idLanguage numbers them.

const (
	LanguageEnglish Language = iota + 1
	LanguageFrench
	LanguageGerman
	LanguageSpanish
	LanguageItalian
	LanguageSimplifiedChinese
	LanguageJapanese
	LanguagePortuguese
	LanguageRussian
	LanguageKorean
	LanguageTraditionalChinese
)

The languages, in the order the documentation lists them.

func LanguageFromName added in v0.3.0

func LanguageFromName(name string) Language

LanguageFromName is the inverse, matching case-insensitively.

type Link struct {
	Rel    string `json:"rel"`
	Href   string `json:"href"`
	Method string `json:"method"`
}

Link is one HATEOAS entry, which every entity carries: what the link is for, where it points, and the method that follows it.

type Localization

type Localization struct {
	Name         string   `json:"name"`
	IDLanguage   Language `json:"idLanguage"`
	LanguageName string   `json:"languageName"`
}

Localization is a name as one language spells it. Every named entity carries the five languages the marketplace trades in.

type PriceGuide

type PriceGuide struct {
	IDProduct        int     `json:"idProduct"`
	AvgSellPrice     float64 `json:"avg"`
	LowPrice         float64 `json:"low"`
	TrendPrice       float64 `json:"trend"`
	FoilAvgSellPrice float64 `json:"avg-foil"`
	FoilLowPrice     float64 `json:"low-foil"`
	FoilTrendPrice   float64 `json:"trend-foil"`
	// Pokemon's guide names the second printing's prices "holo" rather
	// than "foil", and publishes them for the cards sold in a reverse
	// holo and for no others; see SecondPrinting.
	HoloAvgSellPrice float64 `json:"avg-holo"`
	HoloLowPrice     float64 `json:"low-holo"`
	HoloTrendPrice   float64 `json:"trend-holo"`
	AvgDay1          float64 `json:"avg1"`
	AvgDay7          float64 `json:"avg7"`
	AvgDay30         float64 `json:"avg30"`
	FoilAvgDay1      float64 `json:"avg1-foil"`
	FoilAvgDay7      float64 `json:"avg7-foil"`
	FoilAvgDay30     float64 `json:"avg30-foil"`
}

PriceGuide is one product's published prices: the low, the trend, and the averages Cardmarket derives rather than any single listing.

func DownloadPriceGuide

func DownloadPriceGuide(ctx context.Context, game Game) ([]PriceGuide, error)

DownloadPriceGuide downloads the published price guide for one game.

func (PriceGuide) SecondPrinting

func (pg PriceGuide) SecondPrinting(game Game) (low, trend float64)

SecondPrinting names the prices of the printing sold beside the product's default one, under whichever heading the game's guide publishes them. Most games sell a foil beside a plain card; Pokemon sells a reverse holo, and its guide says so.

type PriceGuideFile added in v0.4.0

type PriceGuideFile struct {
	Version     int
	CreatedAt   time.Time
	PriceGuides []PriceGuide
}

PriceGuideFile is a published price guide: when Cardmarket built it, and its rows.

func DownloadPriceGuideFile added in v0.4.0

func DownloadPriceGuideFile(ctx context.Context, game Game) (*PriceGuideFile, error)

DownloadPriceGuideFile downloads one game's price guide with the time Cardmarket built it, so a caller can refuse a file that has stopped updating. How old is too old is the caller's to say: each file is rebuilt on its own schedule.

type Product

type Product struct {
	IDProduct     int `json:"idProduct"`
	IDMetaproduct int `json:"idMetaproduct"`
	// CountReprints is how many products the metaproduct bundles, which is
	// how many times the card has been printed across every expansion.
	CountReprints int            `json:"countReprints"`
	Name          string         `json:"enName"`
	LocName       string         `json:"locName"`
	Localization  []Localization `json:"localization,omitempty"`
	Website       string         `json:"website"`
	// Image is a pre-signed address that expires an hour after the request
	// that returned it. It is worth reading and never worth storing.
	Image        string `json:"image"`
	GameName     string `json:"gameName"`
	CategoryName string `json:"categoryName"`
	IDGame       Game   `json:"idGame"`
	Number       string `json:"number"`
	// Rarity is what the marketplace calls the printing's rarity. It is the
	// game's own vocabulary where the game has one - Common through Secret
	// Rare for Pokemon - and "Promo" for a whole promotional shelf.
	Rarity string `json:"rarity"`
	// ExpansionName and ExpansionIcon are the ExpansionSingles form.
	ExpansionName string `json:"expansionName"`
	ExpansionIcon int    `json:"expansionIcon"`
	// ExpansionCode is the marketplace's own abbreviation of the
	// expansion, read off the catalog rather than the product
	ExpansionCode string `json:"-"`
	// Expansion and PriceGuide are the single-product form.
	Expansion struct {
		IDExpansion   int    `json:"idExpansion"`
		Name          string `json:"enName"`
		ExpansionIcon int    `json:"expansionIcon"`
	} `json:"expansion"`
	// PriceGuide is keyed by the marketplace's own headings - SELL, LOW,
	// LOWEX, LOWFOIL, AVG, TREND, TRENDFOIL. The documentation spells the
	// third LOWEX+; the API sends LOWEX.
	PriceGuide    map[string]float64 `json:"priceGuide"`
	CountArticles int                `json:"countArticles"`
	CountFoils    int                `json:"countFoils"`
	Links         []Link             `json:"links,omitempty"`
}

Product is one catalog entry, a card or a sealed item.

Two of its fields depend on which request built it, the marketplace answering the same entity two ways: ExpansionName and ExpansionIcon are filled by ExpansionSingles, and Expansion and PriceGuide by Product. A product read one way carries nothing of the other's.

type ProductList

type ProductList struct {
	IDProduct    int    `json:"idProduct"`
	Name         string `json:"name"`
	CategoryID   int    `json:"idCategory"`
	CategoryName string `json:"categoryName"`
	ExpansionID  int    `json:"idExpansion"`
	MetacardID   int    `json:"idMetacard"`
	DateAdded    string `json:"dateAdded"`
}

ProductList is one entry of the catalog dump, which names products without pricing them.

func DownloadProductListSealed

func DownloadProductListSealed(ctx context.Context, game Game) ([]ProductList, error)

DownloadProductListSealed downloads the catalog of one game's sealed product.

func DownloadProductListSingles

func DownloadProductListSingles(ctx context.Context, game Game) ([]ProductList, error)

DownloadProductListSingles downloads the catalog of one game's singles.

type ProductListFile added in v0.4.0

type ProductListFile struct {
	Version   int
	CreatedAt time.Time
	Products  []ProductList
}

ProductListFile is a published product list: when Cardmarket built it, and its rows.

func DownloadProductListSealedFile added in v0.4.0

func DownloadProductListSealedFile(ctx context.Context, game Game) (*ProductListFile, error)

DownloadProductListSealedFile is DownloadProductListSealed with the time Cardmarket built the file; see DownloadPriceGuideFile.

func DownloadProductListSinglesFile added in v0.4.0

func DownloadProductListSinglesFile(ctx context.Context, game Game) (*ProductListFile, error)

DownloadProductListSinglesFile is DownloadProductListSingles with the time Cardmarket built the file; see DownloadPriceGuideFile.

type URLOption added in v0.3.0

type URLOption struct {
	// Foil, FirstEd and ReverseHolo narrow to a printing. None is worth
	// as much as Only here: a non-foil price that links to a page
	// showing foils is quoting from a shelf the reader cannot see.
	Foil        Filter
	FirstEd     Filter
	ReverseHolo Filter

	// Signed and Altered narrow by what a signature or an alteration has
	// done to a listing, which is to make it incomparable with the rest
	// of the shelf. They are named as the Article carries them.
	Signed  Filter
	Altered Filter

	// SellerTypes and SellerCountries narrow to the listings of the kinds
	// of seller named, based in the countries named: UserTypePowerseller
	// with CountryGermany and CountryNetherlands is the German and Dutch
	// powersellers alone. Each is a list because the storefront's own
	// controls are, and an empty one asks for every seller. The order
	// given does not matter; the link is the same either way. A seller
	// type the storefront has no number for is left out.
	SellerTypes     []UserType
	SellerCountries []Country

	// Language asks the page to prefer one language, which it honours
	// where the card has a printing in it and quietly ignores where it
	// does not. Zero asks for nothing; LanguageEnglish is the usual choice.
	Language Language

	// Affiliate is the tag the link is attributed to, empty for none. It
	// is a named field so it cannot be transposed with SearchURL's name;
	// the cost is that leaving it out loses the tag without a word.
	Affiliate string
}

URLOption is everything that shapes a storefront link but the game and the thing being linked to: which listings the caller means, which language the page should prefer, and whose affiliate tag it carries.

The zero value asks for nothing at all - no narrowing, no language, no tag - and each field set adds a parameter the marketplace understands. What is worth filtering out is the caller's judgement, so this package spells the filters and decides none of them.

A warning, from the marketplace rather than from here: its filters fail open. A parameter that does not apply to a game, or a value it does not recognise, is ignored and the unfiltered listing is answered - not an error. A link is a suggestion to a browser, so that is survivable; a price read through a filter is not, and should be checked against each article's own flags rather than trusted to the request.

The finish flags are the game's own and not one vocabulary: a Magic listing carries Foil and neither of the others, a Pokemon listing carries FirstEd and ReverseHolo and no Foil at all. They are independent per Article (a Pokemon listing can carry FirstEd and ReverseHolo together, confirmed live), so a caller holding a real Article can pass its flags straight through with no per-game mapping of its own.

type UserScore added in v0.3.0

type UserScore int

UserScore is a seller's rating, as minUserScore numbers them. Like Condition, the constants run best to worst, which is the order minUserScore means by "or better".

const (
	UserScoreOutstanding UserScore = iota + 1
	UserScoreVeryGood
	UserScoreGood
	UserScoreAverage
	UserScoreBad
)

The seller scores, best to worst.

func UserScoreFromName added in v0.3.0

func UserScoreFromName(name string) UserScore

UserScoreFromName is the inverse, matching case-insensitively.

type UserType added in v0.3.0

type UserType string

UserType is the kind of seller an article may be filtered to. Commercial includes powersellers; Powerseller is only those.

Alone among these, it carries no Name and no FromName: its values are already the words the marketplace uses, so a table would map each one to itself. Note that an Article decodes the same idea as a number instead - ArticleSeller.IsCommercial, 0, 1 and 2 - and the storefront's sellerType filter numbers it too (see URLOption.SellerTypes), which is the marketplace's inconsistency, not one worth hiding behind a shared type.

const (
	UserTypePrivate     UserType = "private"
	UserTypeCommercial  UserType = "commercial"
	UserTypePowerseller UserType = "powerseller"
)

The seller types an article may be filtered to.

Directories

Path Synopsis
cmd
mkmcatalog command
Command mkmcatalog walks one game's Cardmarket catalog - every expansion and the products it shelves - and writes it in the shape a price run reads, so the twice-daily runs stop crawling the API.
Command mkmcatalog walks one game's Cardmarket catalog - every expansion and the products it shelves - and writes it in the shape a price run reads, so the twice-daily runs stop crawling the API.
mkmpriceguide command
Command mkmpriceguide downloads Cardmarket's published catalog files for a game: the price guide, or the product list for singles or sealed product.
Command mkmpriceguide downloads Cardmarket's published catalog files for a game: the price guide, or the product list for singles or sealed product.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL