mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 23:01:09 +00:00
d3b4762b76
* several functions. fixes #1 and #2 * multi selection functionality
374 lines
44 KiB
Markdown
374 lines
44 KiB
Markdown
#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](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](https://tcgdex.dev/) 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 set’s `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 `localId` → `AutoDetectedPrint::setNo` and `rarity` → `AutoDetectedPrint::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.
|
||
|
||
3. **Reverse auto-detect (`detectVariantsBySetNo`, same Set # Auto detect button).** Requires a selected set. When **Name** is blank and **Set #** is filled, uses `GET /v2/en/cards/{setId}-{localId}` (then filtered search) to fill the card **name**. Returned `localId`s are post-filtered so a fuzzy hit cannot win on a shared digit prefix (`4` must not accept `14`). When Name is filled, behavior stays name → setNo as above. Set is always required for either direction.
|
||
|
||
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 set’s detail `cards[]` (`localId` → `setNo`, `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:
|
||
- [Yugipedia MediaWiki API help](https://yugipedia.com/api.php?action=help) (standard MediaWiki action API; we only need `prop=imageinfo`).
|
||
- [Yu-Gi-Oh! API Guide — YGOPRODeck](https://ygoprodeck.com/api-guide/). CCM3 uses **v7** endpoints only.
|
||
|
||
### 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_name` → `Set.name`, and `tcg_date` → `Set.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 [Yugipedia’s image policy](https://yugipedia.com/wiki/Yugipedia: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 Rare` → `UR`, `Quarter Century Secret Rare` → `QCScR`, …). 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.
|
||
- `edition` — `1E` 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 card’s 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 YGOPRODeck’s `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.
|
||
|
||
3. **Reverse auto-detect (`detectVariantsBySetNo`).** When **Name** is blank and **Set #** is filled, the Set # Auto detect button looks up the offline `yugioh/set-catalog.json` checklist (same file as Set Completion) by `Set.id` + collector digits / full code, and fills the card **name** (and **rarity** when present on the catalog row or when YGOPRODeck `cardset=` enrichment succeeds). Digit matching strips leading zeros but is not a prefix match (`5` ↔ `LOB-005`, `1` does not match `LOB-011`). Name→Set # Auto detect also applies the matched print’s rarity. When **both** Name and Set # are filled, the field last typed by the user is the lookup key (so editing Set # after a name detect and clicking Auto detect again resolves by set number, not by re-running the name path). Requires a prior **Sets → Update Yu-Gi-Oh!** so the catalog exists (re-run Update to refresh rarities on older catalogs). Set is always required for both directions.
|
||
|
||
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. Yugipedia’s 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, rarity? }` drawn from each card’s `card_sets[]` (`set_rarity` when present). 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 | 1–42 |
|
||
| `ban2` | 2nd Generation | 43–88 |
|
||
| `ban3` | 3rd Generation | 89–118 |
|
||
| `banpromo-j` | Jump Promos | J1–J3 |
|
||
| `banpromo-ta` | Toei Promos | TA1–TA2 |
|
||
| `bansealdass` | Sealdass | 1–42 |
|
||
|
||
`fetchAll()` returns that manifest (offline — no HTTP). `fetchAllWithCatalog()` additionally `GET`s each set’s 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`. Requires a selected set.
|
||
3. **Auto-detect by number:** SMW `ask` `[[Bandai number::…]]` (or promo gallery parse for `J*`/`TA*` codes) → same fields, then **filtered to the selected set**. Ask results are also dropped when the returned Bandai number does not match the requested one after normalization (`1` must not accept `11`). The Set # Auto detect button is bidirectional: blank name + number fills name; name filled fills number/rarity. Set is always required.
|
||
|
||
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](https://digimoncard.io/api-documentation). 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 Set` → `series-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 match’s `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).
|
||
|
||
**Reverse auto-detect** (`detectVariantsBySetNo`): when Name is blank and Set # is filled, search with `card=` + `pack=` fills `AutoDetectedPrint::name` (and normalizes `setNo`). Hits are post-filtered so digits-only input matches the numeric suffix with leading zeros ignored (`1` ↔ `ST-01`, not `ST-11`). Set (pack display name) is always required for either direction.
|
||
|
||
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.json` → `sets-west.json` and `pokemonjp/sets.json` → `sets-asia.json` when the new files are missing. **Sets > Update Pokemon** refreshes both lists. Upstream: [TCGdex REST API](https://tcgdex.dev/). 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 1–3 |
|
||
| `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 `GET`s `/v2/ja/sets/{id}` and builds checklist rows from `cards[]` (`localId` → `setNo`, 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 `Mewtwo` → `Mewtwo (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 ring’s 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](https://github.com/tcgdex/cards-database) `data-asia` (the live TCGdex API does not expose them).
|
||
- **neo1–neo4**: 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](https://www.cardindex.co/) 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 (`Mewtwo` → `Mewtwo (CoroCoro promo)`).
|
||
6. City Gym deck exclusives must stay **printing-accurate**. Do **not** reuse Leaders' Stadium / PMCG donor `tcgplayer_id`s 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).
|
||
|
||
Reverse auto-detect (`detectVariantsBySetNo`) uses `GET /v2/ja/cards/{setId}-{localId}` (preferring catalog `nameEn` when present) or a catalog scan with leading-zero-insensitive `localId` matching (`1` ↔ `001`, not `011`) for catalog-only sets, filling Name when Set # is known and Name is blank. Set is always required.
|
||
|
||
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 (PMCG1–PMCG6, neo1–neo4, VS1, web1, E1–E3). 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`](../tools/pokemon_jp/enrich_preview_images.py) (longest prefixes first).
|
||
2. Re-run:
|
||
|
||
```bash
|
||
python tools/pokemon_jp/enrich_preview_images.py
|
||
```
|
||
|
||
3. For neo1–neo4 Japanese preview images (CardIndex JP scans only; clears EN
|
||
pokemontcg.io URLs on miss), run:
|
||
|
||
```bash
|
||
python tools/pokemon_jp/enrich_neo_image_urls.py
|
||
python tools/pokemon_jp/enrich_neo_image_urls.py --overwrite
|
||
```
|
||
|
||
4. 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`](../tools/pokemon_jp/non_pokemon_en_by_ja.json). One entry covers **every set** that reprints that Japanese title.
|
||
3. Re-run:
|
||
|
||
```bash
|
||
python tools/pokemon_jp/enrich_preview_images.py
|
||
```
|
||
|
||
4. 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.
|
||
5. 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](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](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 Yugipedia’s gallery, debug in this order: (1) verify the candidate list via `YuGiOhCardPreviewSource::buildCandidateFilenames(...)` against the actual file names on Yugipedia’s `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.
|