#pragma once #include "ccm/domain/YuGiOhSetCatalog.hpp" #include "ccm/ports/ICardPreviewSource.hpp" #include "ccm/ports/IHttpClient.hpp" #include "ccm/services/YuGiOhSetCatalogService.hpp" #include #include #include #include namespace ccm { // YuGiOhCardPreviewSource - resolves preview images for Yu-Gi-Oh! cards. // // The image-preview path is backed by Yugipedia's MediaWiki API // (https://yugipedia.com/api.php). Yugipedia hosts actual per-printing card // scans, with deterministic file names of the shape // `----.` (e.g. // `BlueEyesWhiteDragon-LOB-EN-UR-UE.png` vs `BlueEyesWhiteDragon-SDK-NA-UR-UE.png`), // which lets us return the right artwork for printings that share a passcode // but have visibly different art - a case YGOPRODeck cannot disambiguate (its // card_images array is keyed by art-treatment passcode, not by physical // printing). // // The auto-detect-first-print path keeps using YGOPRODeck (`cardinfo.php`): // that endpoint returns a richer set listing (with rarities and release // dates) than Yugipedia, and we don't need image data for it. // // Reverse lookup (set + setNo → name) uses the offline set-completion catalog // written by Sets → Update Yu-Gi-Oh! (`YuGiOhSetCatalogService`). // // Region policy: always English (EN/NA/EU/AU) regardless of the card's // stored Language. Localized scans are intentionally not queried so the user // sees a consistent, well-stocked gallery (EN scans are the most complete). class YuGiOhCardPreviewSource final : public ICardPreviewSource { public: explicit YuGiOhCardPreviewSource(IHttpClient& http); // Optional offline catalog for set+setNo → name reverse lookup. When null // or empty, detectVariantsBySetNo returns a clear "Update Sets" error. void setCatalogService(YuGiOhSetCatalogService* catalogStore) noexcept { catalogStore_ = catalogStore; } [[nodiscard]] bool supportsAutoDetectPrint() const noexcept override { return true; } Result fetchImageUrl(std::string_view name, std::string_view setId, std::string_view setNo) override; Result detectFirstPrint(std::string_view name, std::string_view setId) override; Result> detectPrintVariants(std::string_view name, std::string_view setId) override; Result detectBySetNo(std::string_view setId, std::string_view setNo) override; Result> detectVariantsBySetNo( std::string_view setId, std::string_view setNo) override; // ---- Yugipedia helpers (image preview path) ---------------------------- // Build the list of candidate Yugipedia file names to try, in priority // order (most likely first). Always uses English regions; the caller may // pass an empty rarityCode when the rarity is unknown, in which case the // returned list will skip rarity in the filename. static std::vector buildCandidateFilenames( std::string_view name, std::string_view setCode, std::string_view rarityCode, bool firstEdition); // Build a single MediaWiki batch query URL that asks for imageinfo.url // for every filename. MediaWiki's `titles=` parameter joins page titles // with `|`, so we issue exactly one HTTP call per preview lookup. static std::string buildYugipediaQueryUrl( const std::vector& filenames); // Parse a MediaWiki `query.pages` response and return the resolved URL of // the first filename in `filenameOrder` that exists. Missing pages have // the `missing` marker (no `imageinfo`); existing pages carry an // `imageinfo[0].url` we forward verbatim. Errors are classified: // - JSON parse failure or schema deviation => Transient. // - Every candidate came back missing => NotFound. static Result parseYugipediaResponse( const std::string& body, const std::vector& filenameOrder); // Strip a card name down to Yugipedia's image-slug shape: alphanumerics // (and parentheses) only, no whitespace, no policy-banned punctuation. static std::string normalizeName(std::string_view name); // Map a CCM3 rarity name (e.g. "Ultra Rare") to the Yugipedia rarity // code used in image filenames (e.g. "UR"). Returns an empty string when // the rarity is unknown; the caller treats that as "skip rarity". static std::string rarityCodeFor(std::string_view rarityName); // Pull the set abbreviation out of a CCM3 setNo such as "LOB-005" or // "LOB-DE005" - in both cases we want "LOB". Returns the trimmed input // unchanged if no dash is present. static std::string extractSetCode(std::string_view setNo); // ---- YGOPRODeck helpers (auto-detect path + fallback) ------------------ // Build a fuzzy-name `cardinfo.php` URL. `setName` may be empty for an // unfiltered fuzzy lookup. Used by detectFirstPrint and by the // standard-art fallback when Yugipedia has no scan for this printing. static std::string buildSearchUrl(std::string_view name, std::string_view setName); // Pick the standard artwork (card_images[0]) from a YGOPRODeck response, // preferring the exact-name match. Used only as a last-resort fallback // when Yugipedia returns nothing for any of our candidate filenames. // Errors are classified: // - JSON parse failure or schema deviation => Transient. // - Empty `data` array, or matched cards without a usable image // variant => NotFound. static Result parseFallbackImageUrl(const std::string& body, std::string_view name); // Pick the first printing for `preferredSetName` from a YGOPRODeck // response. Drives the "Auto detect" button in the YGO edit dialog. static Result parseFirstPrint(const std::string& body, std::string_view preferredSetName); // Every `(set_code, set_rarity)` pair for cards whose name matches // `wantedCardName` (case-insensitive). When `wantedCardName` is empty, // scans every row in `data[]` like `parseFirstPrint` did historically. static Result> parsePrintVariants(const std::string& body, std::string_view preferredSetName, std::string_view wantedCardName); // Offline reverse lookup against a set-completion catalog. `setId` is the // pack's set code (e.g. "LOB"); `setNo` may be digits ("005") or a full // collector code ("LOB-005" / "LOB-EN005"). static Result> detectVariantsBySetNoFromCatalog(const YuGiOhSetCatalog& catalog, std::string_view setId, std::string_view setNo); // YGOPRODeck cardset= dump filtered by collector digits (HTTP fallback when // the offline catalog is missing or has no match). static Result> detectVariantsBySetNoFromCardset(const std::string& body, std::string_view preferredSetName, std::string_view setNo); static std::string buildCardsetOnlyUrl(std::string_view setName); private: IHttpClient& http_; YuGiOhSetCatalogService* catalogStore_{nullptr}; // Cached offline catalog so reverse auto-detect does not re-parse a // multi-MB JSON file on every button click. mutable std::optional catalogCache_; }; } // namespace ccm