Fix: replace api.pokemontcg.io with api.tcgdex.net (#20)

This commit is contained in:
Sebastian Dine
2026-07-23 17:58:00 +02:00
committed by GitHub
parent 9917e364c1
commit ab0e3c5ae2
27 changed files with 1067 additions and 784 deletions
@@ -1,11 +1,9 @@
#pragma once
// PokemonCardPreviewSource: ICardPreviewSource implementation for the Pokemon
// TCG. When set id + collector number are both known, prefers
// GET https://api.pokemontcg.io/v2/cards/{setId}-{number}
// then falls back to a name-less search `set.id:… number:…`. Name-based
// search is kept for lookups that lack a set number (or set id). Returns
// `images.large` (with `images.small` as a graceful fallback).
// PokemonCardPreviewSource: West Pokemon previews via TCGdex EN.
// Prefers GET /v2/en/cards/{setId}-{localId}, then filtered card search, then
// set-detail name match for auto-detect. Image URLs append /high.png (wxImage
// decodes PNG, not webp).
#include "ccm/ports/ICardPreviewSource.hpp"
#include "ccm/ports/IHttpClient.hpp"
@@ -31,40 +29,33 @@ public:
Result<std::vector<AutoDetectedPrint>> detectPrintVariants(std::string_view name,
std::string_view setId) override;
// Build the fully URL-encoded Pokemon TCG search URL for the given card.
// When both setId and setNo are non-empty, omits the name: clause so the
// Lucene query cannot miss on name∩number intersections.
// Exposed for unit testing and to keep encoding rules in one place.
// Strip everything after the first '/' (e.g. "4/102" -> "4").
static std::string normalizeCollectorNumber(std::string_view setNo);
static std::string buildCardByIdUrl(std::string_view setId, std::string_view setNo);
static std::string buildSetDetailUrl(std::string_view setId);
static std::string buildSearchUrl(std::string_view name,
std::string_view setId,
std::string_view setNo);
static std::string imageUrlFromBase(std::string_view imageBase);
// Direct card endpoint: /v2/cards/{setId}-{normalizedNumber}.
static std::string buildCardByIdUrl(std::string_view setId, std::string_view setNo);
struct SetCardRow {
std::string localId;
std::string name;
std::string imageBase;
std::string rarity;
};
// Strip everything after the first '/' (e.g. "4/102" -> "4"). Used by
// preview lookups, auto-detect, and set-completion ownership matching.
static std::string normalizeCollectorNumber(std::string_view setNo);
static Result<std::vector<SetCardRow>, PreviewLookupError>
parseSetCards(const std::string& body);
// Slimmer search URL for auto-detect: omits the number clause and asks the
// API for only the fields the print-variant parser needs.
static std::string buildDetectSearchUrl(std::string_view name,
std::string_view setId);
// Parse a Pokemon TCG /v2/cards *search* response body (`data` array) and
// pull out the image URL for the first matching card. Prefers
// `images.large`, falls back to `images.small`. Errors are classified:
// - JSON parse failure or missing/non-array `data` => Transient.
// - Empty `data` array or missing image variants => NotFound.
static Result<std::string, PreviewLookupError>
parseResponse(const std::string& body);
// Parse a Pokemon TCG /v2/cards/{id} response (`data` object).
static Result<std::string, PreviewLookupError>
parseCardByIdResponse(const std::string& body);
// Enumerate distinct collector numbers (and rarities) for an exact card
// name inside the chosen set. Exposed for unit testing without HTTP.
// Parse a slim TCGdex cards-array search response; prefer first hit with image.
static Result<std::string, PreviewLookupError>
parseSearchResponse(const std::string& body);
static Result<std::vector<AutoDetectedPrint>>
parsePrintVariants(const std::string& body,
std::string_view setId,
@@ -0,0 +1,21 @@
#pragma once
// Sync Pokemon collection cards against freshly fetched set lists:
// - West: canonicalize legacy pokemontcg set ids, then refresh name/date
// - Asia: refresh name/date when the set id is present in the Asia list
#include "ccm/domain/PokemonCard.hpp"
#include "ccm/domain/Set.hpp"
#include <cstddef>
#include <vector>
namespace ccm {
// Mutates cards in place. Returns how many cards changed at least one set field.
[[nodiscard]] std::size_t syncPokemonCollectionSets(
std::vector<PokemonCard>& cards,
const std::vector<Set>& westSets,
const std::vector<Set>& asiaSets);
} // namespace ccm
@@ -1,7 +1,7 @@
#pragma once
// PokemonGameModule: IGameModule for the Pokemon TCG. Owns its set source
// and card preview source, both backed by api.pokemontcg.io/v2.
// and card preview source, both backed by TCGdex EN (api.tcgdex.net/v2/en).
#include "ccm/games/IGameModule.hpp"
#include "ccm/games/pokemon/PokemonCardPreviewSource.hpp"
@@ -1,12 +1,8 @@
#pragma once
// PokemonSetSource: ISetSource implementation for the Pokemon TCG.
// Calls the Pokemon TCG API at https://api.pokemontcg.io/v2/sets, maps the
// response into our `Set` domain type, and sorts by release date ascending.
// The Pokemon TCG API already returns `releaseDate` in `YYYY/MM/DD` format,
// so no rewriting is needed (unlike Scryfall's `released_at`).
// Behavior matches `pokemon/set_services.rs::update_sets`.
// Set-completion catalog is built from a paginated /v2/cards dump.
// PokemonSetSource: ISetSource for West Pokemon via TCGdex EN
// (https://api.tcgdex.net/v2/en). List endpoint returns a slim array; release
// dates and set-completion checklists come from per-set detail GETs.
#include "ccm/domain/PokemonSetCatalog.hpp"
#include "ccm/domain/Set.hpp"
@@ -14,15 +10,14 @@
#include "ccm/ports/IHttpClient.hpp"
#include <string>
#include <string_view>
#include <vector>
namespace ccm {
class PokemonSetSource final : public ISetSource {
public:
static constexpr const char* kEndpoint = "https://api.pokemontcg.io/v2/sets";
static constexpr const char* kCardsEndpoint = "https://api.pokemontcg.io/v2/cards";
static constexpr int kCardsPageSize = 250;
static constexpr const char* kListEndpoint = "https://api.tcgdex.net/v2/en/sets";
struct FetchWithCatalog {
std::vector<Set> sets;
@@ -33,28 +28,18 @@ public:
Result<std::vector<Set>> fetchAll() override;
// Sets endpoint + paginated cards dump for the offline checklist.
// List + per-set detail (cards + release date) for the offline checklist.
Result<FetchWithCatalog> fetchAllWithCatalog();
// Pure parser exposed for unit testing without a network round-trip.
static Result<std::vector<Set>> parseResponse(const std::string& body);
// Pure parsers exposed for unit testing without a network round-trip.
static Result<std::vector<Set>> parseListResponse(const std::string& body);
static Result<std::string> parseReleaseDate(const std::string& detailBody);
static std::string rewriteReleaseDate(std::string_view isoDate);
static std::string buildSetDetailUrl(std::string_view setId);
// Build / merge checklist packs from one /v2/cards page body. Pass an
// accumulating catalog; returns page count metadata for pagination.
struct CardsPageMeta {
int page{1};
int pageSize{kCardsPageSize};
int count{0};
int totalCount{0};
};
static Result<CardsPageMeta> mergeCardsPage(const std::string& body,
PokemonSetCatalog& catalog,
const std::vector<Set>& sets);
static Result<PokemonSetCatalog> parseCatalog(const std::string& body,
const std::vector<Set>& sets);
static std::string buildCardsPageUrl(int page, int pageSize = kCardsPageSize);
static Result<PokemonSetCatalogPack> parseCatalogPackFromSetDetail(
const std::string& detailBody,
const Set& set);
private:
IHttpClient& http_;
@@ -0,0 +1,16 @@
#pragma once
// Canonicalize legacy pokemontcg.io West set ids to TCGdex EN ids.
// Identity when the id is already TCGdex (or unknown). Asia set ids must not
// be passed through this helper.
#include <string>
#include <string_view>
namespace ccm {
// Returns the TCGdex EN set id for a West Pokemon card.set.id. Unknown ids
// and ids that already match TCGdex are returned unchanged.
[[nodiscard]] std::string canonicalizeWestSetId(std::string_view setId);
} // namespace ccm
@@ -80,6 +80,15 @@ public:
return repo_.save(game, map);
}
// Replace the entire collection map in one save (e.g. after bulk set-id sync).
Result<void> saveAll(Game game, std::vector<TCard> cards) {
Map map;
for (auto& card : cards) {
map.insert_or_assign(card.id, std::move(card));
}
return repo_.save(game, map);
}
// Remove the card with the given id. Also deletes any associated images
// via the IImageStore (best-effort - image removal failures are logged in
// the error string but the card itself is still purged from the JSON).