mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-29 01:08:49 +00:00
minor: yugioh support added
This commit is contained in:
@@ -1,8 +1,46 @@
|
||||
#include "ccm/services/CardPreviewService.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
CardPreviewService::CardPreviewService(IHttpClient& http) : http_(http) {}
|
||||
namespace {
|
||||
|
||||
// Compose a stable cache key from the four lookup coordinates. Using NUL as
|
||||
// a separator keeps the key unambiguous even if a card's name happens to
|
||||
// contain `|` or other punctuation.
|
||||
std::string makePreviewKey(Game game,
|
||||
std::string_view name,
|
||||
std::string_view setId,
|
||||
std::string_view setNo) {
|
||||
std::string k;
|
||||
k.reserve(2 + name.size() + setId.size() + setNo.size() + 3);
|
||||
k.push_back('p');
|
||||
k.push_back(static_cast<char>(static_cast<int>(game)));
|
||||
k.push_back('\0');
|
||||
k.append(name);
|
||||
k.push_back('\0');
|
||||
k.append(setId);
|
||||
k.push_back('\0');
|
||||
k.append(setNo);
|
||||
return k;
|
||||
}
|
||||
|
||||
std::string makeUrlKey(std::string_view url) {
|
||||
std::string k;
|
||||
k.reserve(url.size() + 1);
|
||||
k.push_back('u');
|
||||
k.append(url);
|
||||
return k;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CardPreviewService::CardPreviewService(IHttpClient& http,
|
||||
IPreviewByteCache* persistentCache)
|
||||
: http_(http), persistentCache_(persistentCache) {}
|
||||
|
||||
void CardPreviewService::registerModule(IGameModule& module) {
|
||||
if (auto* src = module.cardPreviewSource(); src != nullptr) {
|
||||
@@ -10,6 +48,80 @@ void CardPreviewService::registerModule(IGameModule& module) {
|
||||
}
|
||||
}
|
||||
|
||||
CardPreviewService::CacheLookupKind CardPreviewService::cacheLookup(
|
||||
const std::string& key, std::string& outPayload) {
|
||||
std::lock_guard<std::mutex> lock(cacheMutex_);
|
||||
auto it = cacheIndex_.find(key);
|
||||
if (it == cacheIndex_.end()) {
|
||||
outPayload.clear();
|
||||
return CacheLookupKind::Miss;
|
||||
}
|
||||
// Move-to-front to mark as most-recently-used.
|
||||
cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
|
||||
if (it->second->negative) {
|
||||
outPayload.clear();
|
||||
return CacheLookupKind::NegativeHit;
|
||||
}
|
||||
outPayload = it->second->payload;
|
||||
return CacheLookupKind::Hit;
|
||||
}
|
||||
|
||||
void CardPreviewService::cacheStore(const std::string& key, std::string payload) {
|
||||
if (payload.empty()) return;
|
||||
std::lock_guard<std::mutex> lock(cacheMutex_);
|
||||
auto it = cacheIndex_.find(key);
|
||||
if (it != cacheIndex_.end()) {
|
||||
// Overwrite existing entry (positive or negative) and bump it to
|
||||
// the front. Replacing a negative entry is the "we got a real
|
||||
// image after a previous NotFound" path - rare but valid.
|
||||
it->second->payload = std::move(payload);
|
||||
it->second->negative = false;
|
||||
cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
|
||||
return;
|
||||
}
|
||||
cacheList_.push_front({key, std::move(payload), /*negative=*/false});
|
||||
cacheIndex_.emplace(key, cacheList_.begin());
|
||||
while (cacheList_.size() > kCacheCapacity) {
|
||||
cacheIndex_.erase(cacheList_.back().key);
|
||||
cacheList_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void CardPreviewService::cacheStoreNegative(const std::string& key) {
|
||||
std::lock_guard<std::mutex> lock(cacheMutex_);
|
||||
auto it = cacheIndex_.find(key);
|
||||
if (it != cacheIndex_.end()) {
|
||||
it->second->payload.clear();
|
||||
it->second->negative = true;
|
||||
cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
|
||||
return;
|
||||
}
|
||||
cacheList_.push_front({key, std::string{}, /*negative=*/true});
|
||||
cacheIndex_.emplace(key, cacheList_.begin());
|
||||
while (cacheList_.size() > kCacheCapacity) {
|
||||
cacheIndex_.erase(cacheList_.back().key);
|
||||
cacheList_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
Result<std::string> CardPreviewService::fetchAndCache(const std::string& cacheKey,
|
||||
std::string_view url) {
|
||||
auto bytes = http_.get(url);
|
||||
if (!bytes) return Result<std::string>::err(bytes.error());
|
||||
std::string payload = std::move(bytes).value();
|
||||
if (payload.empty()) {
|
||||
return Result<std::string>::err("Empty response body from " + std::string(url));
|
||||
}
|
||||
cacheStore(cacheKey, payload);
|
||||
// Best-effort persist to disk so the next app launch starts warm.
|
||||
// The persistent tier is fire-and-forget: any I/O error is swallowed
|
||||
// by the adapter, the in-memory tier still holds the bytes.
|
||||
if (persistentCache_ != nullptr) {
|
||||
persistentCache_->store(cacheKey, payload);
|
||||
}
|
||||
return Result<std::string>::ok(std::move(payload));
|
||||
}
|
||||
|
||||
Result<std::string> CardPreviewService::fetchPreviewBytes(Game game,
|
||||
std::string_view name,
|
||||
std::string_view setId,
|
||||
@@ -18,17 +130,114 @@ Result<std::string> CardPreviewService::fetchPreviewBytes(Game game,
|
||||
if (it == sources_.end() || it->second == nullptr) {
|
||||
return Result<std::string>::err("No preview source registered for this game.");
|
||||
}
|
||||
|
||||
// Cache check before any HTTP call. The (game, name, setId, setNo) tuple
|
||||
// uniquely identifies a printing for our purposes - the resolved image
|
||||
// URL is always a deterministic function of those four inputs, and any
|
||||
// edit to a lookup-relevant field changes the key automatically.
|
||||
const std::string key = makePreviewKey(game, name, setId, setNo);
|
||||
std::string cached;
|
||||
switch (cacheLookup(key, cached)) {
|
||||
case CacheLookupKind::Hit:
|
||||
return Result<std::string>::ok(std::move(cached));
|
||||
case CacheLookupKind::NegativeHit:
|
||||
return Result<std::string>::err("No preview available for this card.");
|
||||
case CacheLookupKind::Miss:
|
||||
break;
|
||||
}
|
||||
// Disk-backed second tier: previews persisted by an earlier app run
|
||||
// get promoted into the in-memory LRU on first access this session, so
|
||||
// subsequent re-selections stay fast without re-touching the network.
|
||||
// Negative entries on disk are likewise promoted - the user already
|
||||
// knows from a previous session that this record has no upstream image.
|
||||
if (persistentCache_ != nullptr) {
|
||||
const auto disk = persistentCache_->load(key);
|
||||
switch (disk.kind) {
|
||||
case IPreviewByteCache::HitKind::Hit:
|
||||
cacheStore(key, disk.payload);
|
||||
return Result<std::string>::ok(disk.payload);
|
||||
case IPreviewByteCache::HitKind::NegativeHit:
|
||||
cacheStoreNegative(key);
|
||||
return Result<std::string>::err("No preview available for this card.");
|
||||
case IPreviewByteCache::HitKind::Miss:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto url = it->second->fetchImageUrl(name, setId, setNo);
|
||||
if (!url) return Result<std::string>::err(url.error());
|
||||
auto bytes = http_.get(url.value());
|
||||
if (!bytes) return Result<std::string>::err(bytes.error());
|
||||
return Result<std::string>::ok(std::move(bytes).value());
|
||||
if (!url) {
|
||||
// The two error kinds split here:
|
||||
// - NotFound: upstream answered cleanly that this record has no
|
||||
// image. Persist the verdict so we don't keep retrying.
|
||||
// - Transient: network/HTTP/parse failure. Surface the error
|
||||
// unchanged and DO NOT cache anything; the next selection
|
||||
// retries from scratch.
|
||||
const auto err = std::move(url).error();
|
||||
if (err.kind == PreviewLookupError::Kind::NotFound) {
|
||||
cacheStoreNegative(key);
|
||||
if (persistentCache_ != nullptr) persistentCache_->storeNegative(key);
|
||||
}
|
||||
return Result<std::string>::err(err.message);
|
||||
}
|
||||
return fetchAndCache(key, url.value());
|
||||
}
|
||||
|
||||
Result<AutoDetectedPrint> CardPreviewService::detectFirstPrint(Game game,
|
||||
std::string_view name,
|
||||
std::string_view setId) {
|
||||
auto it = sources_.find(game);
|
||||
if (it == sources_.end() || it->second == nullptr) {
|
||||
return Result<AutoDetectedPrint>::err("No preview source registered for this game.");
|
||||
}
|
||||
if (!it->second->supportsAutoDetectPrint()) {
|
||||
return Result<AutoDetectedPrint>::err("Auto-detect not enabled for this game.");
|
||||
}
|
||||
return it->second->detectFirstPrint(name, setId);
|
||||
}
|
||||
|
||||
Result<std::vector<AutoDetectedPrint>> CardPreviewService::detectPrintVariants(
|
||||
Game game,
|
||||
std::string_view name,
|
||||
std::string_view setId) {
|
||||
auto it = sources_.find(game);
|
||||
if (it == sources_.end() || it->second == nullptr) {
|
||||
return Result<std::vector<AutoDetectedPrint>>::err(
|
||||
"No preview source registered for this game.");
|
||||
}
|
||||
if (!it->second->supportsAutoDetectPrint()) {
|
||||
return Result<std::vector<AutoDetectedPrint>>::err(
|
||||
"Auto-detect not enabled for this game.");
|
||||
}
|
||||
return it->second->detectPrintVariants(name, setId);
|
||||
}
|
||||
|
||||
Result<std::string> CardPreviewService::fetchImageBytesByUrl(std::string_view url) {
|
||||
auto bytes = http_.get(url);
|
||||
if (!bytes) return Result<std::string>::err(bytes.error());
|
||||
return Result<std::string>::ok(std::move(bytes).value());
|
||||
// The by-URL path is used for fixed per-game card-back fallback images.
|
||||
// A failure there is always transient (the URL itself is constant), so
|
||||
// there is no negative-cache analogue to worry about; we just look up
|
||||
// and, if needed, fetch+store.
|
||||
const std::string key = makeUrlKey(url);
|
||||
std::string cached;
|
||||
switch (cacheLookup(key, cached)) {
|
||||
case CacheLookupKind::Hit:
|
||||
return Result<std::string>::ok(std::move(cached));
|
||||
case CacheLookupKind::NegativeHit:
|
||||
// Defensive: nothing in this code path ever stores a negative
|
||||
// entry under a URL key, but if one ever ends up here (cache
|
||||
// file tampering, future code paths) treat it as a miss so the
|
||||
// fallback fetch can still run.
|
||||
break;
|
||||
case CacheLookupKind::Miss:
|
||||
break;
|
||||
}
|
||||
if (persistentCache_ != nullptr) {
|
||||
const auto disk = persistentCache_->load(key);
|
||||
if (disk.kind == IPreviewByteCache::HitKind::Hit) {
|
||||
cacheStore(key, disk.payload);
|
||||
return Result<std::string>::ok(disk.payload);
|
||||
}
|
||||
}
|
||||
return fetchAndCache(key, url);
|
||||
}
|
||||
|
||||
} // namespace ccm
|
||||
|
||||
Reference in New Issue
Block a user