Minor: Additional functions (#25)

* several functions. fixes #1 and #2

* multi selection functionality
This commit is contained in:
Sebastian Dine
2026-08-03 08:35:38 +02:00
committed by GitHub
parent 2eb7c59f78
commit d3b4762b76
60 changed files with 1859 additions and 172 deletions
+2
View File
@@ -11,11 +11,13 @@ const YuGiOhSetCatalogPack* YuGiOhSetCatalog::findPack(std::string_view setId) c
void to_json(nlohmann::json& j, const YuGiOhCatalogCard& c) {
j = nlohmann::json{{"setNo", c.setNo}, {"name", c.name}};
if (!c.rarity.empty()) j["rarity"] = c.rarity;
}
void from_json(const nlohmann::json& j, YuGiOhCatalogCard& c) {
j.at("setNo").get_to(c.setNo);
j.at("name").get_to(c.name);
c.rarity = j.value("rarity", "");
}
void to_json(nlohmann::json& j, const YuGiOhSetCatalogPack& p) {
@@ -35,6 +35,44 @@ bool cardInPack(const nlohmann::json& card, std::string_view packName) {
return false;
}
// Numeric collector suffix: "ST-01" → "01", "01" → "01", "BO-115" → "115".
std::string numericSuffix(std::string_view setNo) {
const std::string n = DigiBattle99CardPreviewSource::normalizeCardNumber(setNo);
const auto dash = n.find('-');
const std::string_view tail =
dash == std::string::npos ? std::string_view{n} : std::string_view{n}.substr(dash + 1);
std::string out;
out.reserve(tail.size());
for (unsigned char c : tail) {
if (std::isdigit(c) != 0) out.push_back(static_cast<char>(c));
}
return out;
}
std::string stripLeadingZeros(std::string digits) {
std::size_t i = 0;
while (i + 1 < digits.size() && digits[i] == '0') ++i;
if (i > 0) digits.erase(0, i);
return digits;
}
bool hasAlphabeticPrefix(std::string_view setNo) {
const std::string n = DigiBattle99CardPreviewSource::normalizeCardNumber(setNo);
return !n.empty() && std::isalpha(static_cast<unsigned char>(n.front())) != 0;
}
// Exact id match, or digits-only input matched to the numeric suffix with
// leading zeros ignored ("1" ↔ "ST-01", but not "ST-11").
bool cardNumbersMatch(std::string_view wanted, std::string_view actual) {
const std::string a = DigiBattle99CardPreviewSource::normalizeCardNumber(wanted);
const std::string b = DigiBattle99CardPreviewSource::normalizeCardNumber(actual);
if (a.empty() || b.empty()) return false;
if (a == b) return true;
// Full id typed (ST-01): require exact normalized equality only.
if (hasAlphabeticPrefix(a)) return false;
return stripLeadingZeros(numericSuffix(a)) == stripLeadingZeros(numericSuffix(b));
}
} // namespace
DigiBattle99CardPreviewSource::DigiBattle99CardPreviewSource(IHttpClient& http)
@@ -144,7 +182,8 @@ DigiBattle99CardPreviewSource::fetchImageUrl(std::string_view name,
Result<std::vector<AutoDetectedPrint>> DigiBattle99CardPreviewSource::parsePrintVariants(
const std::string& body,
std::string_view setName,
std::string_view wantedCardName) {
std::string_view wantedCardName,
std::string_view wantedSetNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
try {
const auto j = nlohmann::json::parse(body);
@@ -157,6 +196,7 @@ Result<std::vector<AutoDetectedPrint>> DigiBattle99CardPreviewSource::parsePrint
const std::string wantedPack = trim(std::string(setName));
const std::string wantedNameLower = toLower(trim(std::string(wantedCardName)));
const std::string wantedNo = normalizeCardNumber(wantedSetNo);
std::vector<AutoDetectedPrint> collected;
for (const auto& card : j) {
@@ -166,13 +206,20 @@ Result<std::vector<AutoDetectedPrint>> DigiBattle99CardPreviewSource::parsePrint
}
if (!cardInPack(card, wantedPack)) continue;
AutoDetectedPrint out;
out.name = trim(card.value("name", ""));
out.setNo = normalizeCardNumber(card.value("id", ""));
out.rarity = ""; // Digi-Battle UI is Pokémon-like; rarity not persisted.
if (out.setNo.empty()) continue;
// digimoncard.io `card=` is fuzzy (card=1 can return ST-01 and ST-11).
// When the user typed a number, keep only exact / zero-padded matches.
if (!wantedNo.empty() && !cardNumbersMatch(wantedNo, out.setNo)) continue;
collected.push_back(std::move(out));
}
if (collected.empty()) {
if (!wantedNo.empty()) {
return R::err("Could not auto-detect Digi-Battle card name from set number.");
}
if (!wantedNameLower.empty() && !wantedPack.empty()) {
return R::err("Could not auto-detect Digi-Battle set print metadata.");
}
@@ -219,4 +266,40 @@ Result<std::vector<AutoDetectedPrint>> DigiBattle99CardPreviewSource::detectPrin
return parsePrintVariants(fallback.value(), setName, name);
}
Result<AutoDetectedPrint> DigiBattle99CardPreviewSource::detectBySetNo(
std::string_view setName,
std::string_view setNo) {
auto list = detectVariantsBySetNo(setName, setNo);
if (!list) return Result<AutoDetectedPrint>::err(list.error());
if (list.value().empty()) {
return Result<AutoDetectedPrint>::err(
"Could not auto-detect Digi-Battle card name from set number.");
}
return Result<AutoDetectedPrint>::ok(list.value().front());
}
Result<std::vector<AutoDetectedPrint>> DigiBattle99CardPreviewSource::detectVariantsBySetNo(
std::string_view setName,
std::string_view setNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
if (trim(std::string(setName)).empty()) return R::err("Select a set first.");
const std::string num = normalizeCardNumber(setNo);
if (num.empty()) return R::err("Card number is empty.");
const std::string url = buildSearchUrl("", setName, num);
auto resp = http_.get(url);
if (resp) {
auto parsed = parsePrintVariants(resp.value(), setName, "", num);
if (parsed && !parsed.value().empty()) return parsed;
}
// Retry number-only; still filter by pack + exact/padded number.
const std::string fallbackUrl = buildSearchUrl("", "", num);
auto fallback = http_.get(fallbackUrl);
if (!fallback) {
if (resp) return R::err("Could not auto-detect Digi-Battle card name from set number.");
return R::err(fallback.error());
}
return parsePrintVariants(fallback.value(), setName, "", num);
}
} // namespace ccm
@@ -27,6 +27,22 @@ std::string toLower(std::string s) {
return s;
}
std::string stripLeadingZeros(std::string_view s) {
std::size_t i = 0;
while (i + 1 < s.size() && s[i] == '0') ++i;
return std::string(s.substr(i));
}
// Exact localId match after slash-normalization, or leading-zero-insensitive
// equality ("4" ↔ "04", not "4" ↔ "14").
bool localIdsMatch(std::string_view a, std::string_view b) {
const std::string na = PokemonCardPreviewSource::normalizeCollectorNumber(a);
const std::string nb = PokemonCardPreviewSource::normalizeCollectorNumber(b);
if (na.empty() || nb.empty()) return false;
if (na == nb) return true;
return stripLeadingZeros(na) == stripLeadingZeros(nb);
}
} // namespace
PokemonCardPreviewSource::PokemonCardPreviewSource(IHttpClient& http) : http_(http) {}
@@ -299,4 +315,101 @@ Result<std::vector<AutoDetectedPrint>> PokemonCardPreviewSource::detectPrintVari
}
}
Result<AutoDetectedPrint> PokemonCardPreviewSource::parsePrintFromCardById(
const std::string& body) {
using R = Result<AutoDetectedPrint>;
try {
const auto j = nlohmann::json::parse(body);
if (!j.is_object()) {
return R::err("TCGdex EN card response is not a JSON object.");
}
AutoDetectedPrint print;
print.name = trim(j.value("name", ""));
print.setNo = normalizeCollectorNumber(j.value("localId", ""));
print.rarity = trim(j.value("rarity", ""));
if (print.name.empty()) {
return R::err("TCGdex EN card has no name.");
}
if (print.setNo.empty() && j.contains("id") && j.at("id").is_string()) {
const std::string id = j.at("id").get<std::string>();
const auto dash = id.rfind('-');
if (dash != std::string::npos) {
print.setNo = normalizeCollectorNumber(id.substr(dash + 1));
}
}
return R::ok(std::move(print));
} catch (const std::exception& e) {
return R::err(std::string("TCGdex EN card JSON parse error: ") + e.what());
}
}
Result<AutoDetectedPrint> PokemonCardPreviewSource::detectBySetNo(std::string_view setId,
std::string_view setNo) {
auto list = detectVariantsBySetNo(setId, setNo);
if (!list) return Result<AutoDetectedPrint>::err(list.error());
if (list.value().empty()) {
return Result<AutoDetectedPrint>::err("Could not auto-detect card name from set number.");
}
return Result<AutoDetectedPrint>::ok(list.value().front());
}
Result<std::vector<AutoDetectedPrint>> PokemonCardPreviewSource::detectVariantsBySetNo(
std::string_view setId,
std::string_view setNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
const std::string idCanon = canonicalizeWestSetId(setId);
const std::string num = normalizeCollectorNumber(setNo);
if (idCanon.empty()) return R::err("Select a set first.");
if (num.empty()) return R::err("Card number is empty.");
auto byId = http_.get(buildCardByIdUrl(idCanon, num));
if (byId) {
auto parsed = parsePrintFromCardById(byId.value());
if (parsed && localIdsMatch(parsed.value().setNo, num)) {
std::vector<AutoDetectedPrint> out;
out.push_back(std::move(parsed).value());
return R::ok(std::move(out));
}
}
// Fallback: filtered search by set.id + localId.
const std::string url = buildSearchUrl("", idCanon, num);
auto resp = http_.get(url);
if (!resp) return R::err(resp.error());
try {
const auto j = nlohmann::json::parse(resp.value());
if (!j.is_array() || j.empty()) {
return R::err("Could not auto-detect card name from set number.");
}
std::vector<AutoDetectedPrint> out;
std::unordered_set<std::string> seen;
for (const auto& card : j) {
AutoDetectedPrint print;
print.name = trim(card.value("name", ""));
print.setNo = normalizeCollectorNumber(card.value("localId", ""));
print.rarity = trim(card.value("rarity", ""));
if (print.name.empty()) continue;
if (print.setNo.empty() && card.contains("id") && card.at("id").is_string()) {
const std::string id = card.at("id").get<std::string>();
const auto dash = id.rfind('-');
if (dash != std::string::npos) {
print.setNo = normalizeCollectorNumber(id.substr(dash + 1));
}
}
// Defense-in-depth: TCGdex search can be fuzzy; never accept a
// different localId (e.g. "14" when the user asked for "4").
if (!localIdsMatch(print.setNo, num)) continue;
const std::string key = print.name + '\0' + print.setNo + '\0' + print.rarity;
if (!seen.insert(key).second) continue;
out.push_back(std::move(print));
}
if (out.empty()) {
return R::err("Could not auto-detect card name from set number.");
}
return R::ok(std::move(out));
} catch (const std::exception& e) {
return R::err(std::string("TCGdex EN cards search JSON parse error: ") + e.what());
}
}
} // namespace ccm
@@ -439,4 +439,113 @@ JapanesePokemonCardPreviewSource::detectPrintVariants(std::string_view name,
return parsed;
}
Result<AutoDetectedPrint> JapanesePokemonCardPreviewSource::parsePrintFromCardResponse(
const std::string& body) {
using R = Result<AutoDetectedPrint>;
try {
const auto j = nlohmann::json::parse(body);
if (!j.is_object()) {
return R::err("TCGdex JA card response is not a JSON object.");
}
AutoDetectedPrint print;
print.name = trim(j.value("name", ""));
print.setNo = normalizeLocalId(j.value("localId", ""));
print.rarity = trim(j.value("rarity", ""));
if (print.name.empty()) {
return R::err("TCGdex JA card has no name.");
}
if (print.setNo.empty() && j.contains("id") && j.at("id").is_string()) {
const std::string id = j.at("id").get<std::string>();
const auto dash = id.rfind('-');
if (dash != std::string::npos) {
print.setNo = normalizeLocalId(id.substr(dash + 1));
}
}
return R::ok(std::move(print));
} catch (const std::exception& e) {
return R::err(std::string("TCGdex JA card JSON parse error: ") + e.what());
}
}
Result<std::vector<AutoDetectedPrint>>
JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog(
std::string_view setId,
std::string_view localId,
const JapanesePokemonEnCatalog& catalog) {
using R = Result<std::vector<AutoDetectedPrint>>;
const std::string id = normalizeLocalId(localId);
if (setId.empty()) return R::err("Select a set first.");
if (id.empty()) return R::err("Card number is empty.");
// Prefer exact key, then leading-zero-insensitive scan ("1" ↔ "001").
auto found = catalog.findPrint(setId, id);
if (!found) {
for (const auto& print : catalog.printsForSet(setId)) {
if (localIdsMatch(print.localId, id)) {
found = print;
break;
}
}
}
if (!found) {
return R::err("Could not auto-detect card name from set number.");
}
AutoDetectedPrint print;
print.name = !found->nameEn.empty() ? found->nameEn : found->nameJa;
print.setNo = found->localId.empty() ? id : found->localId;
if (print.name.empty()) {
return R::err("Could not auto-detect card name from set number.");
}
std::vector<AutoDetectedPrint> out;
out.push_back(std::move(print));
return R::ok(std::move(out));
}
Result<AutoDetectedPrint> JapanesePokemonCardPreviewSource::detectBySetNo(
std::string_view setId,
std::string_view setNo) {
auto list = detectVariantsBySetNo(setId, setNo);
if (!list) return Result<AutoDetectedPrint>::err(list.error());
if (list.value().empty()) {
return Result<AutoDetectedPrint>::err(
"Could not auto-detect card name from set number.");
}
return Result<AutoDetectedPrint>::ok(list.value().front());
}
Result<std::vector<AutoDetectedPrint>>
JapanesePokemonCardPreviewSource::detectVariantsBySetNo(std::string_view setId,
std::string_view setNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
if (setId.empty()) return R::err("Select a set first.");
const std::string id = normalizeLocalId(setNo);
if (id.empty()) return R::err("Card number is empty.");
auto cardResp = http_.get(buildCardUrl(setId, id));
if (cardResp) {
auto parsed = parsePrintFromCardResponse(cardResp.value());
if (parsed && localIdsMatch(parsed.value().setNo, id)) {
// Prefer EN catalog name when available (exact or zero-insensitive).
if (auto cat = catalog_.findPrint(setId, id); cat && !cat->nameEn.empty()) {
parsed.value().name = cat->nameEn;
} else {
for (const auto& p : catalog_.printsForSet(setId)) {
if (localIdsMatch(p.localId, id) && !p.nameEn.empty()) {
parsed.value().name = p.nameEn;
break;
}
}
}
std::vector<AutoDetectedPrint> out;
out.push_back(std::move(parsed).value());
return R::ok(std::move(out));
}
}
if (catalog_.hasPrintsForSet(setId)) {
return detectVariantsBySetNoFromCatalog(setId, id, catalog_);
}
return R::err("Could not auto-detect card name from set number.");
}
} // namespace ccm
@@ -555,4 +555,176 @@ Result<std::vector<AutoDetectedPrint>> YuGiOhCardPreviewSource::detectPrintVaria
return parsePrintVariants(fallback.value(), canonicalSetName, name);
}
Result<std::vector<AutoDetectedPrint>>
YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(
const YuGiOhSetCatalog& catalog,
std::string_view setId,
std::string_view setNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
const std::string packId = std::string(trimAsciiSpaces(setId));
if (packId.empty()) return R::err("Select a set first.");
const std::string rawNo = std::string(trimAsciiSpaces(setNo));
if (rawNo.empty()) return R::err("Card number is empty.");
const std::string wantDigits =
ygoDigitsStripLeadingZeros(ygoCollectorDigitsFromInput(rawNo));
if (wantDigits.empty()) return R::err("Card number is empty.");
const YuGiOhSetCatalogPack* pack = catalog.findPack(packId);
if (pack == nullptr) {
// Allow callers to pass the display set name (HTTP fallback path).
for (const auto& candidate : catalog.packs) {
if (candidate.setName == packId) {
pack = &candidate;
break;
}
}
}
if (pack == nullptr) {
return R::err("Set not found in offline catalog. Run Sets → Update Yu-Gi-Oh! first.");
}
std::vector<AutoDetectedPrint> out;
std::unordered_set<std::string> seenNames;
for (const auto& card : pack->cards) {
if (!ygoCollectorDigitsEqual(card.setNo, rawNo)) continue;
if (card.name.empty()) continue;
if (!seenNames.insert(card.name).second) continue;
AutoDetectedPrint print;
print.name = card.name;
print.setNo = card.setNo;
print.rarity = card.rarity;
out.push_back(std::move(print));
}
if (out.empty()) {
return R::err("Could not auto-detect card name from set number.");
}
return R::ok(std::move(out));
}
std::string YuGiOhCardPreviewSource::buildCardsetOnlyUrl(std::string_view setName) {
return std::string("https://db.ygoprodeck.com/api/v7/cardinfo.php?cardset=") +
rfc3986PercentEncode(setName);
}
Result<std::vector<AutoDetectedPrint>>
YuGiOhCardPreviewSource::detectVariantsBySetNoFromCardset(
const std::string& body,
std::string_view preferredSetName,
std::string_view setNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
const std::string wantDigits =
ygoDigitsStripLeadingZeros(ygoCollectorDigitsFromInput(setNo));
if (wantDigits.empty()) return R::err("Card number is empty.");
try {
const auto j = nlohmann::json::parse(body);
if (!j.contains("data") || !j.at("data").is_array()) {
return R::err("YGOPRODeck response missing 'data' array.");
}
const std::string preferredLower = toLower(trim(std::string(preferredSetName)));
std::vector<AutoDetectedPrint> out;
std::unordered_set<std::string> seen;
for (const auto& card : j.at("data")) {
const std::string cardName = trim(card.value("name", ""));
if (cardName.empty()) continue;
if (!card.contains("card_sets") || !card.at("card_sets").is_array()) continue;
for (const auto& printing : card.at("card_sets")) {
const std::string setName = trim(printing.value("set_name", ""));
const std::string setCode = trim(printing.value("set_code", ""));
if (setCode.empty()) continue;
if (ygoLikelyEuropeanRegionalSetCode(setCode)) continue;
if (!preferredLower.empty() && toLower(setName) != preferredLower) continue;
if (!ygoCollectorDigitsEqual(setCode, setNo)) continue;
AutoDetectedPrint print;
print.name = cardName;
print.setNo = setCode;
print.rarity = trim(printing.value("set_rarity", ""));
const std::string key = print.name + '\0' + print.setNo + '\0' + print.rarity;
if (!seen.insert(key).second) continue;
out.push_back(std::move(print));
}
}
if (out.empty()) {
return R::err("Could not auto-detect card name from set number.");
}
return R::ok(std::move(out));
} catch (const std::exception& e) {
return R::err(std::string("YGOPRODeck JSON parse error: ") + e.what());
}
}
Result<AutoDetectedPrint> YuGiOhCardPreviewSource::detectBySetNo(std::string_view setId,
std::string_view setNo) {
auto list = detectVariantsBySetNo(setId, setNo);
if (!list) return Result<AutoDetectedPrint>::err(list.error());
if (list.value().empty()) {
return Result<AutoDetectedPrint>::err(
"Could not auto-detect card name from set number.");
}
return Result<AutoDetectedPrint>::ok(list.value().front());
}
Result<std::vector<AutoDetectedPrint>> YuGiOhCardPreviewSource::detectVariantsBySetNo(
std::string_view setId,
std::string_view setNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
const std::string setKey = std::string(trimAsciiSpaces(setId));
if (setKey.empty()) return R::err("Select a set first.");
if (ygoCollectorDigitsFromInput(setNo).empty()) {
return R::err("Card number is empty.");
}
// 1) Offline catalog (preferred — fast once cached).
if (catalogStore_ != nullptr) {
if (!catalogCache_) {
auto loaded = catalogStore_->load();
if (loaded) catalogCache_ = std::move(loaded).value();
}
if (catalogCache_ && !catalogCache_->empty()) {
auto fromCatalog =
detectVariantsBySetNoFromCatalog(*catalogCache_, setKey, setNo);
// Prefer YGOPRODeck when reachable so rarity (and multi-rarity
// variants) come through — the offline catalog may predate the
// rarity field or only keep one rarity per printing slot.
const YuGiOhSetCatalogPack* pack = catalogCache_->findPack(setKey);
std::string setName = setKey;
if (pack != nullptr) {
setName = pack->setName;
} else {
for (const auto& candidate : catalogCache_->packs) {
if (candidate.setName == setKey) {
setName = candidate.setName;
break;
}
}
}
if (!setName.empty()) {
auto resp = http_.get(buildCardsetOnlyUrl(setName));
if (resp) {
auto fromHttp =
detectVariantsBySetNoFromCardset(resp.value(), setName, setNo);
if (fromHttp) return fromHttp;
}
}
if (fromCatalog) return fromCatalog;
// Prefer catalog miss text when HTTP also missed / was unreachable.
return fromCatalog;
}
}
// 2) No catalog: treat setKey as display set name and query YGOPRODeck.
auto resp = http_.get(buildCardsetOnlyUrl(setKey));
if (!resp) {
return R::err(
"Set catalog missing and YGOPRODeck lookup failed. "
"Run Sets → Update Yu-Gi-Oh! or check your network.");
}
return detectVariantsBySetNoFromCardset(resp.value(), setKey, setNo);
}
} // namespace ccm
+9 -1
View File
@@ -166,7 +166,9 @@ Result<YuGiOhSetCatalog> YuGiOhSetSource::parseCatalog(const std::string& b
const auto existing = build.slotIndex.find(slot);
if (existing == build.slotIndex.end()) {
build.slotIndex.emplace(slot, build.cards.size());
build.cards.push_back(YuGiOhCatalogCard{setCode, cardName});
const std::string setRarity(
trimAsciiSpaces(printing.value("set_rarity", "")));
build.cards.push_back(YuGiOhCatalogCard{setCode, cardName, setRarity});
continue;
}
@@ -175,6 +177,12 @@ Result<YuGiOhSetCatalog> YuGiOhSetSource::parseCatalog(const std::string& b
if (!ygoHasEnRegionInfix(prev.setNo) && ygoHasEnRegionInfix(setCode)) {
prev.setNo = setCode;
if (!cardName.empty()) prev.name = cardName;
const std::string setRarity(
trimAsciiSpaces(printing.value("set_rarity", "")));
if (!setRarity.empty()) prev.rarity = setRarity;
} else if (prev.rarity.empty()) {
prev.rarity = std::string(
trimAsciiSpaces(printing.value("set_rarity", "")));
}
}
}
@@ -238,7 +238,8 @@ YuGiOhBandaiCardPreviewSource::parsePageImagesResponse(const std::string& body)
Result<std::vector<AutoDetectedPrint>>
YuGiOhBandaiCardPreviewSource::parseAskResponse(const std::string& body,
std::string_view preferredSetId) {
std::string_view preferredSetId,
std::string_view wantedSetNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
try {
const auto j = nlohmann::json::parse(body);
@@ -250,6 +251,8 @@ YuGiOhBandaiCardPreviewSource::parseAskResponse(const std::string& body,
return R::ok({});
}
const std::string wantNo = YuGiOhBandaiSetSource::normalizeCardNumber(wantedSetNo);
std::vector<std::pair<int, AutoDetectedPrint>> ranked;
for (auto it = results.begin(); it != results.end(); ++it) {
const std::string pageTitle = it.key();
@@ -268,6 +271,12 @@ YuGiOhBandaiCardPreviewSource::parseAskResponse(const std::string& body,
YuGiOhBandaiSetSource::normalizeCardNumber(num.get<std::string>());
}
}
// Defense-in-depth: SMW ask should be exact, but never accept a
// different Bandai number (e.g. #11 when the user asked for #1).
if (!wantNo.empty() &&
YuGiOhBandaiSetSource::normalizeCardNumber(print.setNo) != wantNo) {
continue;
}
if (printouts.contains("Rarity") && printouts.at("Rarity").is_array() &&
!printouts.at("Rarity").empty()) {
const auto& rar = printouts.at("Rarity").at(0);
@@ -328,6 +337,7 @@ Result<std::vector<AutoDetectedPrint>> YuGiOhBandaiCardPreviewSource::askByName(
}
Result<std::vector<AutoDetectedPrint>> YuGiOhBandaiCardPreviewSource::askByNumber(
std::string_view setId,
std::string_view setNo) {
using R = Result<std::vector<AutoDetectedPrint>>;
const std::string n = YuGiOhBandaiSetSource::normalizeCardNumber(setNo);
@@ -336,19 +346,52 @@ Result<std::vector<AutoDetectedPrint>> YuGiOhBandaiCardPreviewSource::askByNumbe
// Promo codes (J1, TA2, …) are not valid values for SMW's numeric
// `Bandai number` property — ask returns a type error. Resolve them from
// the promotional set gallery instead.
if (isAlphanumericPromoNumber(n)) {
static constexpr const char* kPromoGallery =
"Set Card Galleries:Promotional Cards (Bandai)";
const std::string url = YuGiOhBandaiSetSource::buildGalleryParseUrl(kPromoGallery);
R list = [&]() -> R {
if (isAlphanumericPromoNumber(n)) {
static constexpr const char* kPromoGallery =
"Set Card Galleries:Promotional Cards (Bandai)";
const std::string url = YuGiOhBandaiSetSource::buildGalleryParseUrl(kPromoGallery);
auto resp = http_.get(url);
if (!resp) return R::err(resp.error());
return parsePromoGalleryResponse(resp.value(), n);
}
const std::string url = buildAskByNumberUrl(n);
auto resp = http_.get(url);
if (!resp) return R::err(resp.error());
return parsePromoGalleryResponse(resp.value(), n);
}
return parseAskResponse(resp.value(), setId, n);
}();
if (!list) return list;
const std::string url = buildAskByNumberUrl(n);
auto resp = http_.get(url);
if (!resp) return R::err(resp.error());
return parseAskResponse(resp.value(), {});
const std::string wantSet = trimCopy(setId);
if (wantSet.empty()) return list;
std::vector<AutoDetectedPrint> filtered;
filtered.reserve(list.value().size());
for (auto& print : list.value()) {
if (print.setId == wantSet) filtered.push_back(std::move(print));
}
if (filtered.empty()) {
return R::err("No Bandai card matched that number in the selected set.");
}
return R::ok(std::move(filtered));
}
Result<AutoDetectedPrint> YuGiOhBandaiCardPreviewSource::detectBySetNo(
std::string_view setId,
std::string_view setNo) {
auto list = detectVariantsBySetNo(setId, setNo);
if (!list) return Result<AutoDetectedPrint>::err(list.error());
if (list.value().empty()) {
return Result<AutoDetectedPrint>::err(
"Could not auto-detect Bandai card from number.");
}
return Result<AutoDetectedPrint>::ok(list.value().front());
}
Result<std::vector<AutoDetectedPrint>>
YuGiOhBandaiCardPreviewSource::detectVariantsBySetNo(std::string_view setId,
std::string_view setNo) {
return askByNumber(setId, setNo);
}
Result<std::string, PreviewLookupError>
@@ -407,20 +450,4 @@ YuGiOhBandaiCardPreviewSource::detectPrintVariants(std::string_view name,
return askByName(name, setId);
}
Result<AutoDetectedPrint> YuGiOhBandaiCardPreviewSource::detectBySetNo(
std::string_view setNo) {
auto list = detectVariantsBySetNo(setNo);
if (!list) return Result<AutoDetectedPrint>::err(list.error());
if (list.value().empty()) {
return Result<AutoDetectedPrint>::err(
"Could not auto-detect Bandai card from number.");
}
return Result<AutoDetectedPrint>::ok(list.value().front());
}
Result<std::vector<AutoDetectedPrint>>
YuGiOhBandaiCardPreviewSource::detectVariantsBySetNo(std::string_view setNo) {
return askByNumber(setNo);
}
} // namespace ccm
+4 -2
View File
@@ -263,6 +263,7 @@ Result<std::vector<AutoDetectedPrint>> CardPreviewService::detectPrintVariants(
}
Result<AutoDetectedPrint> CardPreviewService::detectBySetNo(Game game,
std::string_view setId,
std::string_view setNo) {
auto it = sources_.find(game);
if (it == sources_.end() || it->second == nullptr) {
@@ -271,11 +272,12 @@ Result<AutoDetectedPrint> CardPreviewService::detectBySetNo(Game game,
if (!it->second->supportsAutoDetectPrint()) {
return Result<AutoDetectedPrint>::err("Auto-detect not enabled for this game.");
}
return it->second->detectBySetNo(setNo);
return it->second->detectBySetNo(setId, setNo);
}
Result<std::vector<AutoDetectedPrint>> CardPreviewService::detectVariantsBySetNo(
Game game,
std::string_view setId,
std::string_view setNo) {
auto it = sources_.find(game);
if (it == sources_.end() || it->second == nullptr) {
@@ -286,7 +288,7 @@ Result<std::vector<AutoDetectedPrint>> CardPreviewService::detectVariantsBySetNo
return Result<std::vector<AutoDetectedPrint>>::err(
"Auto-detect not enabled for this game.");
}
return it->second->detectVariantsBySetNo(setNo);
return it->second->detectVariantsBySetNo(setId, setNo);
}
Result<std::string> CardPreviewService::fetchImageBytesByUrl(std::string_view url) {
+3 -1
View File
@@ -18,7 +18,7 @@ struct Replacement {
std::string_view to;
};
constexpr std::array<Replacement, 14> kReplacements{{
constexpr std::array<Replacement, 16> kReplacements{{
{"'", ""},
{"`", ""},
{",", ""},
@@ -34,6 +34,8 @@ constexpr std::array<Replacement, 14> kReplacements{{
{"\xC3\xBB", "u"}, // u-circumflex
// Remaining accented vowels appear in modern Scryfall data but were not
// listed in the Rust source. Keeping behavior 1:1 deliberately.
{"\xE2\x99\x82", "male"}, // ♂ male sign
{"\xE2\x99\x80", "female"}, // ♀ female sign
}};
void replaceAllInPlace(std::string& s, std::string_view from, std::string_view to) {