Files
Card-Collection-Manager-3-s…/docs/assets-and-info-apis.md
T
2026-07-30 14:51:53 +02:00

42 KiB
Raw Blame History

#documentation #apis #integrations #ccm3

Asset And Info APIs

This document explains which external APIs Card Collection Manager 3 uses, and what each API is responsible for in the app. Use this page with adding-a-new-game.md when you are wiring a new game module or debugging API behavior.

API Roles

The code separates remote APIs into two roles: info APIs and asset APIs. Info APIs provide set metadata used to populate local set lists (ID, name, release date). Asset APIs resolve a card lookup into an image URL, then CardPreviewService downloads the raw preview image bytes for the UI.

Magic: The Gathering APIs

Info API: https://api.scryfall.com/sets
Used by MagicSetSource to fetch all sets. The parser drops digital-only sets, maps Scryfall fields to the internal Set type, rewrites released_at from YYYY-MM-DD to YYYY/MM/DD, and sorts ascending by release date.

Asset API: https://api.scryfall.com/cards/search?q=...
Used by MagicCardPreviewSource to find a card printing from name + setId, then extract data[0].image_uris.normal as the preview URL. The search query is percent-encoded and card names apply & -> and normalization before lookup.

Pokemon APIs

Unified Pokemon Game menu entry. Per-card region (West / Asia) selects the backend below. Collection: pokemon/collection.json. West sets: pokemon/sets-west.json. Asia sets: pokemon/sets-asia.json. Language choices: West → English/German/French/Spanish/Italian/Russian; Asia → Japanese/S-Chinese/T-Chinese/Korean.

West (Game::Pokemon, TCGdex EN)

Upstream: TCGdex REST API locale en. No API key. Canonical West set ids are TCGdex EN ids (e.g. base1, sv01, swsh12.5tg). Legacy pokemontcg.io ids (sv1, pgo, swsh12tg, …) are rewritten via canonicalizeWestSetId on West collection load, preview/auto-detect lookups, and set-completion matching so existing collections keep working; the next save persists TCGdex ids.

Info API: https://api.tcgdex.net/v2/en/sets
Used by PokemonSetSource to fetch the slim set list (id, name). Release dates are not on the list endpoint — each sets GET /v2/en/sets/{id} supplies releaseDate as YYYY-MM-DD, rewritten to YYYY/MM/DD, then the list is sorted ascending by release date.

Asset API: https://api.tcgdex.net/v2/en/cards/{setId}-{localId} (by id), https://api.tcgdex.net/v2/en/cards?… (filtered search), and set-detail cards[] for auto-detect. Image CDN bases live on assets.tcgdex.net; the preview source appends /high.png (wxImage decodes PNG, not webp).

Used by PokemonCardPreviewSource in two ways:

  1. Preview lookup (fetchImageUrl). When both set id and collector number are present, prefers GET /v2/en/cards/{setId}-{localId} (card object with image base). On HTTP failure or missing image, falls back to a filtered search set.id=eq:…&localId=eq:… (collector numbers are unique within a set). When Set # or set id is missing, uses name=eq:… with optional set.id / localId. Legacy set ids are canonicalized before URL build.

  2. Auto-detect print (detectFirstPrint / detectPrintVariants, Pokémon edit dialog). Prefers GET /v2/en/sets/{setId} and filters cards[] by exact case-insensitive card name. Maps localIdAutoDetectedPrint::setNo and rarityAutoDetectedPrint::rarity (the edit dialog does not auto-sync holo flags from rarity). If set detail fails, falls back to a filtered cards search and still restricts rows to the chosen set id when present. Distinct (setNo, rarity) pairs are deduped. The edit dialog offers Auto detect, Next, silent prefetch on Edit open, and clears cached variants when Name or Set changes. The Set # field and persisted PokemonCard::setNo keep only the printed-number portion; values such as 4/104 are trimmed to 4 on load and save.

The preview path normalizes collector numbers before request build. For example, 4/102 is reduced to 4 because the remote localId path expects only the printed-number component.

Set-completion catalog (West)

Sets → Update Pokemon uses PokemonSetSource::fetchAllWithCatalog() so the West path writes:

  1. The set list (pokemon/sets-west.json) from /v2/en/sets + per-set detail dates
  2. A pack checklist at <dataStorage>/pokemon/set-catalog-west.json from each sets detail cards[] (localIdsetNo, name → name)

Each catalog pack stores id (TCGdex EN set id), name (display), and cards[] of { setNo, name } keyed by localId (normalized by stripping anything after /). Duplicate collector numbers within a pack collapse to one checklist row. The Pokemon Set Completion tab reads this file offline; ownership for a West pack requires PokemonRegion::West, a canonicalized card.set.id match, and a normalized collector number match. Amount / holo / 1st Edition are ignored for completion counts.

After a successful Update, PokemonGameView also runs syncPokemonCollectionSets against the refreshed set lists: West cards get legacy set-id migration plus set.name / releaseDate refresh when the id is present; Asia cards refresh name/date the same way. Changed cards are persisted via CollectionService::saveAll.

If set-catalog-west.json is missing (and the active region filter is West or All with no Asia catalog either), the Set Completion tab prompts the user to run Update Pokemon.

Yu-Gi-Oh! APIs (Yugipedia + YGOPRODeck)

Yu-Gi-Oh! splits its remote calls across two upstreams. Yugipedia is the primary preview source because it hosts actual per-printing card scans; YGOPRODeck continues to drive set listings and the auto-detect-first-print helper, plus a last-resort image fallback.

Upstream documentation:

Info API: YGOPRODeck cardsets.php

https://db.ygoprodeck.com/api/v7/cardsets.php
Used by YuGiOhSetSource. The response is a top-level JSON array. Each object maps set_code → internal Set.id, set_nameSet.name, and tcg_dateSet.releaseDate with - rewritten to / for consistency with other games date strings. Results are sorted ascending by releaseDate.

CCM3 also applies a deterministic local patch step in YuGiOhSetSource::appendMissingSetAliases(...) after parsing: if upstream omits known 25th Anniversary TCG reprints, the app injects missing aliases for LOB-25TH, MRD-25TH, SRL-25TH, PSV-25TH, DCR-25TH, and IOC-25TH (with fixed release dates) so users can still select those products in the set picker.

UI note (set code entry, no extra HTTP): The Yu-Gi-Oh! Add/Edit dialog can resolve a typed product code against the already cached set vector (same data as the set dropdown). Matching is implemented in core/include/ccm/util/YuGiOhSetLookup.hpp as lookupYuGiOhSetByShorthand(...): trim ASCII whitespace, ASCII case-fold, then require an exact match on Set.id (the YGOPRODeck set_code). Zero matches → user error; more than one row with the same normalized id → ambiguous error (defensive). On a unique hit the dialog returns to the dropdown and selects that set.

Asset API: Yugipedia api.php (primary)

https://yugipedia.com/api.php?action=query&prop=imageinfo&iiprop=url&titles=...
Used by YuGiOhCardPreviewSource::fetchImageUrl for the actual per-printing card scan. Yugipedia is the only public source we have found that distinguishes art between same-passcode reprints (LOB Blue-Eyes vs SDK Blue-Eyes, for example), and uses a deterministic file-name convention of the shape <Slug>-<SET>-<REGION>-<RARITY>-<EDITION>[-Misc].<png|jpg> per Yugipedias image policy.

The UI passes a positional tuple in setNo of the form set_code||rarity||edition (for example SDK-001||Ultra Rare||UE); the source splits on || before building filenames. Field meanings:

  • set_code — full code as printed (LOB-005, SDK-001, RA04-EN001). Everything before the first - becomes the Yugipedia <SET> slot (LOB, SDK, RA04).
  • rarity — full English rarity name from the edit dialog (Ultra RareUR, Quarter Century Secret RareQCScR, …). The canonical short-form mapping lives in ygoRarityShortCode(...) (core/include/ccm/util/YuGiOhPrintingSlot.hpp) and is reused by both the Yu-Gi-Oh overview-table rarity rendering and preview filename construction (rarityCodeFor(...)). Unknown values fall back to the rarity-less filename pattern.
  • edition1E when the user marked the card as 1st Edition, otherwise UE (Unlimited).

buildCandidateFilenames(...) then produces a priority-ordered list:

  1. Printed edition first (1E then UE, or UE then 1E for non-first), with LE last for promo-style prints.
  2. English regions only — EN, then NA, then EU, then AU. Yugipedia is queried with English regions regardless of the cards stored Language, so a German-language card still shows the English scan; this matches the user-visible policy in the edit dialog and avoids querying region-specific scans that are sparser on Yugipedia.
  3. Both .png and .jpg extensions per combo (older LOB-era uploads are .jpg, modern reprints are .png).
  4. A rarity-less fallback round so cards with unknown rarities still resolve in single-rarity sets.

buildYugipediaQueryUrl(...) joins all candidates into one MediaWiki batch query (titles=File:A|File:B|... URL-encoded), so the entire list resolves in a single HTTP call. parseYugipediaResponse(...) walks the candidate list in order and returns the URL of the first filename that came back with imageinfo[0].url; missing files come back with "missing": "" and are skipped.

Asset API: YGOPRODeck cardinfo.php (fallback + auto-detect)

https://db.ygoprodeck.com/api/v7/cardinfo.php?fname=...
Used in two situations:

  1. Last-resort preview fallback. If Yugipedia returns no candidate match (cards without an English scan yet, transient API errors), fetchImageUrl falls through to parseFallbackImageUrl(...), which prefers an exact-name match in YGOPRODecks data[], otherwise the first row, and returns the first entry from card_images[0]. This is intentionally not filtered by cardset=: when YGOPRODeck applies that filter, it reorders card_images so alt-art passcodes are promoted ahead of the standard art, which would re-introduce the “wrong artwork” bug we fixed by switching to Yugipedia.

  2. Auto-detect print (detectFirstPrint / detectPrintVariants, Yu-Gi-Oh! edit dialog). Uses fname= plus cardset= set to the display set name from the picker (must match card_sets[].set_name in the payload). If that request fails (for example unknown set label), it retries with fname= only and still filters prints by preferred set_name. YuGiOhCardPreviewSource::parsePrintVariants(...) walks every (set_code, set_rarity) pair for rows whose card name matches exactly (case-insensitive) so the dialog can offer ring-buffer Next controls: one cycles distinct set_code values for that name+set (and resets rarity to the first upstream rarity for the newly selected code); another cycles distinct set_rarity values for the current set_code without changing the collector number. Shared HTTP and parsing rules live beside parseFirstPrint. When the dialog passes both an exact card name and a display set_name, an upstream miss on that label returns an error instead of falling back to unfiltered card_sets[] rows — otherwise unrelated products (same card name, different set_name on each printing) could be blended into one bogus variant list. The Yu-Gi-Oh! edit dialog additionally drops European alternate set_code rows that use the -E### pattern (single E before digits, e.g. LOB-E003) when the card language is English, because YGOPRODeck keeps those alongside NA numbering (LOB-005) under the same English set_name; it also collapses LOB-005-style and LOB-EN005-style codes to one Next slot via digit-tail matching (ccm/util/YuGiOhPrintingSlot.hpp). No image data is needed for this path, so Yugipedia is not consulted.

YGOPRODeck publishes rate limits and asks clients to cache responses and avoid abusive hotlinking; treat failures after burst traffic as an upstream policy signal, not an app bug. Yugipedias MediaWiki API is similarly polite — one batched call per preview lookup keeps us well under any normal threshold.

Set-completion catalog (cardinfo.php all-cards dump)

Sets → Update Yu-Gi-Oh! uses YuGiOhSetSource::fetchAllWithCatalog() so two HTTP responses write:

  1. The set list (yugioh/sets.json) from cardsets.php (same as before, including local 25th Anniversary aliases)
  2. A pack checklist at <dataStorage>/yugioh/set-catalog.json from the unfiltered cardinfo.php dump

Each catalog pack stores id (YGOPRODeck product set_code / Set.id, e.g. LOB), name (display set_name), and cards[] of { setNo, name } drawn from each cards card_sets[]. European -E### alternate codes are dropped; LOB-005 / LOB-EN005-style equivalents collapse to one checklist row (preferring an EN-embedded code when present). The Yu-Gi-Oh! Set Completion tab reads this file offline; ownership for a pack requires matching card.set.id plus a printing-slot match (ygoPrintingSlotsMatch — same abbrev + digit run). Rarity and 1st Edition are ignored for completion counts.

If set-catalog.json is missing, the Set Completion tab prompts the user to run Update Yu-Gi-Oh!.

Yu-Gi-Oh! (Bandai) APIs (Yugipedia)

Bandai Carddass (pre-Konami) is wired as Game::YuGiOhBandai (dirName yugiohbandai, UI label Yu-Gi-Oh! (Bandai)). There is no dedicated Bandai REST API; everything goes through Yugipedia MediaWiki + Semantic MediaWiki.

Info API (sets + catalog)

YuGiOhBandaiSetSource keeps an app-owned set manifest (stable ids, no fragile category scrape):

id Name Numbers
ban1 1st Generation 142
ban2 2nd Generation 4388
ban3 3rd Generation 89118
banpromo-j Jump Promos J1J3
banpromo-ta Toei Promos TA1TA2
bansealdass Sealdass 142

fetchAll() returns that manifest (offline — no HTTP). fetchAllWithCatalog() additionally GETs each sets Yugipedia gallery page via action=parse&prop=wikitext and parses lines like … | {{pound}}014 ([[R]]) {{Gallery card names|Dark Magician (Bandai)|…}} into checklist entries {setNo, name, rarity} (rarity codes C/R/SR → Common/Rare/Super Rare). The shared promo gallery is split by setNo prefix (J* vs TA*). Persisted at yugiohbandai/set-catalog.json.

Set Completion ownership keys on (set.id, normalized setNo). Because fetchAll() is offline, Add/Edit can work before any catalog download; the catalog is filled on the first visit to the Set Completion tab (or via Sets → Update Yu-Gi-Oh! (Bandai)). Cards without a set number do not count toward progress.

English Blue-Eyes is not a separate set — it is ban3 card #118 with language English.

Asset API (preview + auto-detect)

  1. Preview: pageimages on preferred titles Name (Bandai) / Name (English Bandai) / Name (Bandai Sealdass), falling back to SMW ask by English name then pageimages on the best hit.
  2. Auto-detect by name: SMW ask [[Category:Bandai cards]][[English name::…]] → fills name, setId/setName, setNo, rarity, language.
  3. Auto-detect by number: SMW ask [[Bandai number::…]] → same fields.

Card-back fallback URL: https://ms.yugipedia.com//3/34/Back-BAN-JP-1999.png.

Digimon Digi-Battle (1999) APIs (digimoncard.io)

English Digi-Battle is wired as Game::DigiBattle99 (dirName digibattle99, UI label Digimon (Digi-Battle)). Upstream docs: digimoncard.io Public API. Always scope requests with series=Digimon Digi-Battle Card Game so modern Digimon Card Game rows are never mixed in. Rate limit: 15 requests / 10 seconds / IP (429 then temporary block on abuse).

Info API: derived set list from search.php

There is no dedicated sets endpoint. DigiBattle99SetSource calls:

https://digimoncard.io/api-public/search.php?series=Digimon%20Digi-Battle%20Card%20Game&limit=1000&sort=name&sortdirection=asc

and collects unique set_name[] pack strings. Each pack becomes a Set with:

  • Set.name — exact pack display name (used as pack= on search / auto-detect)
  • Set.id — stable slug (Series 1 Starter Setseries-1-starter-set); never rename after ship
  • Set.releaseDate — curated table in the set source (Series 1 Starter = 1999/06/01 verified; other packs use documented year/month anchors)

Unknown future packs get an empty release date and sort last.

Cached on disk as <dataStorage>/digibattle99/sets.json via SetService / JsonSetRepository.

Set-completion catalog (same search.php payload)

Sets → Update Digimon (Digi-Battle) uses DigiBattle99SetSource::fetchAllWithCatalog() so one HTTP response writes both:

  1. The set list (sets.json) as above
  2. A pack checklist at <dataStorage>/digibattle99/set-catalog.json

Each catalog pack stores id (slug), name (display), and cards[] of { setNo, name } (API id normalized like preview — alphabetic prefix uppercased). A card listed in multiple set_name[] packs appears under each pack. The Digimon Set Completion tab reads this file offline (no live HTTP while browsing); ownership for a pack requires matching card.set.id plus normalized setNo.

If set-catalog.json is missing, the Set Completion tab prompts the user to run Update Digimon (Digi-Battle).

Asset API: CDN images + search.php lookup

Card scans live at:

https://images.digimoncard.io/images/cards/{id}.jpg

where {id} is the API card number (ST-01, BO-115, MO-06). The CDN also serves .webp, but CCM3 uses .jpg because OnInit only registers wxPNGHandler / wxJPEGHandler (WebP bytes would surface as “image decode failed”).

DigiBattle99CardPreviewSource::fetchImageUrl:

  1. If setNo is non-empty → normalize alphabetic prefix to uppercase (no invented zero-padding) and return the CDN URL with no search round-trip.
  2. Otherwise search with n= + optional pack= (display set name) + series=, take the first exact name matchs id, then build the CDN URL.

Preview key: (name, set.name, setNo) — middle slot is the pack display name (same idea as Yu-Gi-Oh! passing set.name for YGOPRODeck cardset=), not the slug id.

Auto-detect (detectPrintVariants): same search; distinct id values become AutoDetectedPrint::setNo. Digi-Battle UI is Pokémon-like (no persisted rarity).

Empty search array / {"error":"..."}NotFound; bad JSON / HTTP → Transient.

Japanese Pokémon TCG APIs (TCGdex ja) — Asia region backend

Asia Pokémon is routed internally as Game::JapanesePokemon (dirName pokemon, same data directory as West). It is not a separate Game menu entry: the unified Pokemon UI stores both West and Asia cards in pokemon/collection.json with a per-card region (West / Asia). Set caches are split by filename under that directory (pokemon/sets-west.json vs pokemon/sets-asia.json). JsonSetRepository migrate-on-load promotes legacy pokemon/sets.jsonsets-west.json and pokemonjp/sets.jsonsets-asia.json when the new files are missing. Sets > Update Pokemon refreshes both lists. Upstream: TCGdex REST API. No API key. Japanese set IDs (e.g. PMCG1, SV1a) are never merged into Western TCGdex EN ids.

Info API: TCGdex GET /v2/ja/sets (+ per-set detail)

https://api.tcgdex.net/v2/ja/sets returns a slim array (id, name, cardCount). Release dates require GET /v2/ja/sets/{id} (releaseDate as YYYY-MM-DD, rewritten to YYYY/MM/DD). JapanesePokemonSetSource:

  • Excludes Chinese-region CS* junk rows mislabeled on the JA endpoint.
  • Applies field overrides (e.g. SV4a Japanese name → シャイニートレジャーex).
  • Prefers English display names and release dates from the bundled EN catalog when present; otherwise keeps the TCGdex Japanese name and fetches detail for the date.
  • After parsing the TCGdex list, injects Original-era / catalog-only products TCGdex omits (idempotent by set id — skipped if upstream later adds them). The same injection runs when loading a cached Asia set list (sets-asia.json) via ISetSource::augmentCachedSets, so these products appear without requiring Update Sets first. Stable ids and English names:
Id English name
UnnumberedPromo Unnumbered Promotional cards (Bulbapedia catch-all; synthetic 001… localIds; preview via catalog image_url preferring Japanese / Unnumbered Bulbagarden scans)
ExpSheet1 / ExpSheet2 / ExpSheet3 Expansion Sheet Series 13
NiviCG Nivi City Gym
HanadaCG Hanada City Gym
KuchibaCG Kuchiba City Gym
TamamushiCG Tamamushi City Gym
YamabukiCG Yamabuki City Gym
GurenTG Guren Town Gym
SouthernIslands Southern Islands

Seed data lives in tools/pokemon_jp/classic_missing_sets.json + classic_missing_prints.json (merged into the EN catalog via merge_classic_missing.py). LocalIds for these products are sequential 001… within each product (cards were unnumbered in print). Refresh UnnumberedPromo prints from Bulbapedia with python tools/pokemon_jp/harvest_unnumbered_promos.py, then fill preview images with python tools/pokemon_jp/enrich_unnumbered_promo_images.py (prefers Unnumbered / Japanese reprint-gallery scans over English Wizards |image= primaries; EN-only Bulbapedia pages leave image_url empty), then re-run merge_classic_missing.py. Numbered Japanese promo eras (SV-P, S-P, …) remain out of scope — TCGdex does not expose them, and they are not part of this curated set.

Set-completion catalog (Asia)

Sets → Update Pokemon uses JapanesePokemonSetSource::fetchAllWithCatalog() so the Asia path writes:

  1. The set list (pokemon/sets-asia.json) as above (EN names + classic product injection)
  2. A pack checklist at <dataStorage>/pokemon/set-catalog-asia.json

For each set, the source GETs /v2/ja/sets/{id} and builds checklist rows from cards[] (localIdsetNo, display name prefers EN catalog nameEn, else TCGdex Japanese name). Prints present in the bundled EN catalog but missing from TCGdex cards[] are gap-filled into the pack (covers UnnumberedPromo / City Gym / Expansion Sheets / Southern Islands and sparse classic sets). Catalog-only products with no TCGdex detail become packs entirely from JapanesePokemonEnCatalog::printsForSet.

The Pokemon Set Completion tab also loads this file offline; ownership for an Asia pack requires PokemonRegion::Asia, matching card.set.id, and normalizeLocalId on setNo. Region and language filters on the tab restrict which packs/cards count. West and Asia never cross-count.

If set-catalog-asia.json is missing (and the active region filter needs it), the Set Completion tab prompts the user to run Update Pokemon.

Sets without printed collector numbers (UnnumberedPromo)

Physically unnumbered Japanese promos (and the other classic catalog-only products above) have no printed set number. The app still stores a synthetic setNo / catalog local_id (001, 002, …) so preview and collection JSON stay keyed by (setId, localId) — but that value must not be treated as something the user can read off the card.

Edit dialog (PokemonCardEditDialog, Asia region) for set id UnnumberedPromo:

  • The Set # text field is hidden (row label becomes Print). Auto detect and Next remain.
  • Auto-detect / silent Edit prefetch lists catalog prints matching the typed name (exact EN/JA, plus qualified titles such as MewtwoMewtwo (CoroCoro promo)). Distinct synthetic localIds form the Next ring.
  • Next on the edit form shows a position counter (Next (2/5)), not the synthetic id. For ordinary numbered JP sets, Next still shows the current collector number (Next (42)).
  • A modeless Print preview popup (VariantImagePreviewDialog) opens ~20px to the right of the Add/Edit dialog. It loads the current print via CardPreviewService::fetchPreviewBytes and refreshes on each ring step. The popup has its own << Prev / Next >> controls that drive the same ring as the edit dialog (buttons disabled when fewer than two variants).
  • On save, the dialog writes the rings synthetic setNo into PokemonCard::setNo even though the text field was hidden.

Other classic unnumbered products (City Gyms, Expansion Sheets, Southern Islands) currently keep the normal Set # field; only UnnumberedPromo uses the print-preview UX above.

Asset API: TCGdex card / set-detail images

Preview is local-id based. JapanesePokemonCardPreviewSource:

  1. With setId + setNo (localId), tries GET /v2/ja/cards/{setId}-{localId} and reads image.
  2. Falls back to set-detail cards[] (which often already carries image on modern sets).
  3. Appends /high.png to the TCGdex image base URL (PNG — wxImage does not decode webp).
  4. If the set-specific card still has no scan (classic sets like PMCG1), looks up the bundled EN catalog print for that exact setId+localId and uses optional image_url or a TCGPlayer product image built from tcgplayer_id (https://product-images.tcgplayer.com/fit-in/437x437/{id}.jpg). Gap-fill sources differ by era:
    • PMCG and other data-asia sets with thirdParty.tcgplayer: printing-accurate tcgplayer_id harvested offline from tcgdex/cards-database data-asia (the live TCGdex API does not expose them).
    • neo1neo4: data-asia has no tcgplayer_id and TCGdex JA image is null; the catalog may carry an ETL-written image_url from a Japanese CardIndex set scan (enrich_neo_image_urls.py scrapes the JA neo set pages and matches by English card name). No English pokemontcg.io fallback — if CardIndex has no JP image, image_url is left empty and the UI shows the card-back. Use --overwrite to re-resolve / clear stale EN URLs. Runtime still resolves only by exact JA setId+localId — no C++ name search across printings. This is printing-accurate gap-fill — not a name search across other Charizard printings at runtime.
  5. For catalog-only products (Unnumbered Promotional cards, City Gym theme decks, Expansion Sheets, Southern Islands), when TCGdex set/card GETs fail, Auto-detect and preview fall back to the bundled catalog prints for that setId (EN/JA name → localId; optional tcgplayer_id / image_url for preview). UnnumberedPromo rows typically carry Bulbagarden Archives image_url values written by enrich_unnumbered_promo_images.py, which prefers Japanese / Unnumbered Promotional reprint scans and omits English-only Wizards Black Star primaries when no JP file is available. Without a catalog image field, preview returns NotFound and the UI shows the card-back. Auto-detect matches exact EN/JA names and also qualified English titles (MewtwoMewtwo (CoroCoro promo)).
  6. City Gym deck exclusives must stay printing-accurate. Do not reuse Leaders' Stadium / PMCG donor tcgplayer_ids for those prints; that shows the wrong set art. Instead, bundle local scans under assets/pokemon_jp_classic/<setId>/<localId>.jpg and point the catalog row at image_url: "asset:pokemon_jp_classic/<setId>/<localId>.jpg". CardPreviewService loads asset: URLs from disk next to the executable, bypassing HTTP entirely.

It does not substitute another printing of the same Pokémon when both TCGdex and the catalog lack an image. Then preview returns NotFound and the UI shows the Japanese TCG card-back.

Auto-detect / Next uses the same set-detail cards[], matching the typed name against catalog English names or TCGdex Japanese names. Catalog EN aliases are applied only when the catalog name_ja agrees with the TCGdex row (stale seed mappings like Charmander→001 are ignored).

Pokémon English aliases in the catalog come from National Dex → species table (dexId) for ordinary Pokémon. When name_ja carries a known owner / Rocket's / Dark / Light / Shining prefix, enrich_preview_images.py composes the full English product title (e.g. エリカのナゾノクサErika's Oddish, わるいリザードンDark Charizard, R団のサンダーRocket's Zapdos, neo garbled 輝くセレビShining Celebi). Those rows use name_en_source: "species-table-variant". Trainer/Energy English aliases come from the offline JA→EN map tools/pokemon_jp/non_pokemon_en_by_ja.json (e.g. Switch ← ポケモンいれかえ).

That trainer/energy map is maintained to cover at least the first 15 chronological main Japanese expansions present in TCGdex (PMCG1PMCG6, neo1neo4, VS1, web1, E1E3). The same JA→EN entry also applies to later reprints that reuse the Japanese name.

Variant Pokémon English titles

Auto-detect for English owner / Rocket's / Dark / Light / Shining Pokémon names requires the bundled catalog's full name_en for that print (same rule as City Gym manuals that already store Erika's Oddish). Typing the Japanese TCGdex name still works when name_ja is correct.

To extend variant coverage:

  1. Add new JA prefix → English title prefix pairs to VARIANT_JA_PREFIXES in tools/pokemon_jp/enrich_preview_images.py (longest prefixes first).
  2. Re-run:
python tools/pokemon_jp/enrich_preview_images.py
  1. For neo1neo4 Japanese preview images (CardIndex JP scans only; clears EN pokemontcg.io URLs on miss), run:
python tools/pokemon_jp/enrich_neo_image_urls.py
python tools/pokemon_jp/enrich_neo_image_urls.py --overwrite
  1. Rebuild so assets/pokemon_jp_en_catalog.json next to the exe is updated.

Rows with name_en_source: "manual" (City Gym theme decks in classic_missing_prints.json) are never overwritten. Prefer stable English TCG product names (Bulbapedia / Limitless English titles).

Extending Trainer/Energy English aliases

Auto-detect for English Trainer/Energy names only works when the bundled catalog has a name_en for that print. Pokémon get name_en automatically from dexId (bare species) or from variant prefix composition (full titles); Trainers and Energy do not. To add more sets or staples:

  1. Collect unique Japanese Trainer/Energy names for the sets you care about (from TCGdex set detail cards[].name, or from tools/pokemon_jp/_tcgdex_cards_database/data-asia/<serie>/<setId>/*.ts after running enrich once).
  2. Add each missing name_ja → English display name to tools/pokemon_jp/non_pokemon_en_by_ja.json. One entry covers every set that reprints that Japanese title.
  3. Re-run:
python tools/pokemon_jp/enrich_preview_images.py
  1. Confirm enrich_preview_images.py prints FIRST15 trainer/energy coverage OK (or extend FIRST15_SETS in that script if you raise the coverage baseline). Copy/rebuild so assets/pokemon_jp_en_catalog.json next to the exe is updated.
  2. Prefer stable English TCG product names (Bulbapedia / Limitless English titles). Do not invent per-set aliases that differ for the same name_ja.

Bundled English catalog

ui_wx/assets/pokemon_jp_en_catalog.json is copied next to the exe on build (assets/pokemon_jp_en_catalog.json). It supplies English set/card names TCGdex JA cannot provide, plus optional classic-image gap-fill fields (tcgplayer_id / image_url). Generated offline via tools/pokemon_jp/ (set EN merge + enrich_preview_images.py using species, variant, and trainer/energy tables + optional enrich_neo_image_urls.py for neo image_url). Missing catalog → Japanese-only labels still work; missing image fields → card-back for unscanned printings. Missing EN aliases for a Trainer still allow Auto-detect when the Japanese name is typed.

Card-back fallback uses the Japanese TCG Bulbagarden scan (TCG_Card_Back_Japanese.jpg), not the Western Cardback.jpg.

Runtime Flow In CCM3

The app uses the same flow for every game that registers a module:

  • SetService asks the game's ISetSource (info API) for the latest set list.
  • CardPreviewService asks the game's ICardPreviewSource (asset API) for a preview image URL.
  • CardPreviewService performs a second HTTP GET to that URL and returns raw bytes to the UI layer.
  • If preview lookup fails (or returns empty bytes), the UI loads a per-game card-back fallback in BaseSelectedCardPanel: Magic / Pokémon / Japanese Pokémon call CardPreviewService::fetchImageBytesByUrl(...) against fixed HTTPS URLs. Yu-Gi-Oh! tries two Yugipedia URLs (thumbnail then full Back-EN.png), then reads assets/ygo_card_back.png next to the executable if both downloads fail (bundled asset; see app/CMakeLists.txt).

Caching And Connection Reuse

See caching.md for a dedicated reference on preview cache tiers, internal keys, eviction, clearing, and HTTP session reuse.

Three mechanisms reduce preview latency for all games (Magic, Pokemon West/Asia backends, Yu-Gi-Oh!, DigiBattle99). In addition, the shared HTTP session speeds every IHttpClient::get call (including set-list fetches), not only previews:

  • In-memory preview LRU (CardPreviewService). Successful fetchPreviewBytes results are cached keyed by (game, name, setId, setNo); successful fetchImageBytesByUrl results are cached keyed by URL (used for the per-game card-back fallback). Re-selecting a previously viewed row is decode-only — no HTTP at all. The cache is bounded by CardPreviewService::kCacheCapacity (currently 128 entries) and uses a list+map LRU under a mutex (the preview pipeline is invoked from a worker thread in BaseSelectedCardPanel). Source errors are split by PreviewLookupError::Kind: NotFound (the upstream answered cleanly that the record has no image) is negative-cached in this tier so subsequent selections short-circuit without HTTP, while Transient (HTTP/network/parse failures) is never cached so a brief outage cannot permanently disable a card's preview.
  • Persistent disk byte cache (LocalPreviewByteCache, port IPreviewByteCache). Wraps the in-memory tier with an on-disk store under <exeDir>/.cache/preview-cache/ — pinned next to the executable, in the same scope as config.json, not under the user-configurable Configuration.dataStorage path. The cache stays put when the user reconfigures or relocates their collection data, and it is not part of the user's data directory backups; it is install-scoped, not collection-scoped. Both positive previews and NotFound verdicts survive an app restart. Each entry is a mutually-exclusive <hash>.bin (positive payload) or <hash>.neg (negative marker) plus a <hash>.idx sidecar containing the original key — load-time mismatch on the sidecar treats the entry as a miss, so a hash collision degrades to a one-time HTTP refetch instead of serving the wrong card's bytes (or the wrong card's "no image" verdict). Hashing is FNV-1a 64-bit (no crypto dependency). The cache is bounded by total .bin payload bytes (default kDefaultMaxBytes = 64 MiB) and evicts oldest entries by mtime when a new write would exceed the cap; reading an entry touches its mtime so frequently-viewed cards survive eviction. Negative .neg markers are tiny and not counted against the cap — their count is naturally bounded by the user's actively-viewed records. Filesystem mutations route through IFileSystem; size and mtime queries (which the port does not expose) use std::filesystem directly inside the adapter. The persistent tier is fire-and-forget on the way down — every adapter operation swallows I/O errors so a flaky or full disk never breaks the preview path.
  • Persistent HTTP session (CprHttpClient). The adapter owns one long-lived cpr::Session (libcurl easy handle) for the lifetime of the app. Per-request configuration is limited to SetUrl(...); headers, timeout, and redirect policy are configured once in the constructor. Default Accept: */* keeps JSON responses and raw image bodies working on the same session (avoid tying every GET to application/json). libcurl's connection pool keeps the TLS connection to each host warm, so repeat calls to api.scryfall.com, api.tcgdex.net, assets.tcgdex.net, db.ygoprodeck.com, yugipedia.com, ms.yugipedia.com, digimoncard.io, and images.digimoncard.io skip the TLS handshake. A std::mutex serializes callers — libcurl easy handles are not thread-safe, and the preview pipeline is single-flight per panel anyway.

CardPreviewService consults the tiers in order memory → disk → source/HTTP. On a disk hit (positive or negative) the entry is promoted into the in-memory LRU so the next click on the same row never re-touches the disk cache. On HTTP success the bytes are written through to both tiers in one shot. On a NotFound source error the negative marker is written through to both tiers; on Transient source errors nothing is written, so the next selection retries cleanly.

The combined effect on the preview path: first selection of a previously-unseen card pays one TLS handshake per new host this session (typically two hops for Yu-Gi-Oh!: yugipedia.com for the API, ms.yugipedia.com for the image; Digi-Battle often hits images.digimoncard.io only when setNo is already known), each subsequent fresh card on the same host skips the handshake, any re-selection of an already-viewed card is instant, after the first run with the disk cache populated even a fresh app launch is decode-only for previously-seen cards until eviction or a manual cache clear, and records the upstream cleanly has no image for stay "instant card-back" across restarts instead of re-paying the lookup every launch. Editing a lookup-relevant field of a record (name, set, setNo, or for Yu-Gi-Oh! the rarity / edition packed into setNo) changes the cache key automatically, so a fresh resolution attempt happens on the next click.

To clear the persistent cache (for example to recover from a bad upstream image), delete the <exeDir>/.cache/preview-cache/ subdirectory or the umbrella <exeDir>/.cache/ folder. Note: the in-app "Reset" / data-storage-relocation flow does not touch this directory — the cache is install-scoped, not collection-scoped, so it is preserved across data-dir moves and only cleared by deleting the directory above explicitly (or by reinstalling / relocating the executable).

For the full caching design and contributor rules see the dedicated caching.md.

Fallback card-back sources (BaseSelectedCardPanel; Magic/Pokémon URLs match CCM2):

  • Magic: https://gamepedia.cursecdn.com/mtgsalvation_gamepedia/f/f8/Magic_card_back.jpg
  • Pokémon: https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg
  • Japanese Pokémon: https://archives.bulbagarden.net/media/upload/2/2a/TCG_Card_Back_Japanese.jpg
  • Yu-Gi-Oh!: Yugipedia English TCG back — try https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/250px-Back-EN.png, then https://ms.yugipedia.com/e/e5/Back-EN.png; if both fail, load <exeDir>/assets/ygo_card_back.png (shipped from ui_wx/assets/ygo_card_back.png at link time). fallbackImageUrlForGame(Game::YuGiOh) returns the thumbnail URL for helpers that only consult a single string.
  • Yu-Gi-Oh! (Bandai): https://ms.yugipedia.com//3/34/Back-BAN-JP-1999.png.
  • Digimon (Digi-Battle): no stable public back URL; load <exeDir>/assets/digibattle99_card_back.png (shipped from ui_wx/assets/digibattle99_card_back.png at link time).

If a game module does not provide a preview source (cardPreviewSource() == nullptr), preview registration is skipped and the UI behaves as "no remote preview API available."

Error Surface And Debugging Intent

All source types return Result<T, std::string> errors so failures cross boundaries without exceptions. In practice, this keeps failures debuggable by separating:

  • info API failures (bad set payload, schema mismatch, endpoint/network failure), and
  • asset API failures (query mismatch, no matching card, missing image fields, image download failure).

When previews fail, verify request construction first (name sanitization, number normalization, percent encoding), then verify response shape assumptions: Scryfall (data, image_uris), Pokemon West (GET /v2/cards/{setId}-{number}data object, or search data[]; images.large/images.small; auto-detect also needs name, number, rarity, and set.id on each matching row), Yu-Gi-Oh! Yugipedia (query.pages.<id>.imageinfo[0].url per filename, missing files tagged "missing": ""), Yu-Gi-Oh! YGOPRODeck fallback (data, name, card_images), Digi-Battle digimoncard.io (top-level array with name/id/set_name; CDN images.digimoncard.io/images/cards/{id}.jpg), Japanese Pokémon TCGdex (image base + /high.png; set-detail cards[] with localId). If the UI fallback path succeeds (network card-back and/or bundled PNG), the panel shows the card-back image and the inline label (image preview unavailable); only if every fallback fails does the preview stay empty with status text.

For Yu-Gi-Oh! specifically, when a printing shows the wrong art compared with Yugipedias gallery, debug in this order: (1) verify the candidate list via YuGiOhCardPreviewSource::buildCandidateFilenames(...) against the actual file names on Yugipedias Card_Gallery:<Card> page; (2) confirm the dialog rarity name maps to the expected short code in ygoRarityShortCode(...) / rarityCodeFor(...) (extend the mapping when a new rarity surfaces); (3) confirm the firstEdition flag matches the printed edition stamp — the candidate ordering puts the printed edition first.