Turn Card, Deck_List, Protocol, RNG, Network (Client, Server), Settings and Utility into libraries and remove cockatrice_common. (#6212)

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: ebbit1q <ebbit1q@gmail.com>
This commit is contained in:
BruebachL 2025-10-09 07:36:12 +02:00 committed by GitHub
parent be1403c920
commit 1ef07309d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
605 changed files with 3812 additions and 3408 deletions

View file

@ -0,0 +1,242 @@
#include "card_picture_loader.h"
#include <QApplication>
#include <QBuffer>
#include <QDebug>
#include <QDirIterator>
#include <QFileInfo>
#include <QMainWindow>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkRequest>
#include <QPainter>
#include <QPixmapCache>
#include <QScreen>
#include <QStatusBar>
#include <QThread>
#include <algorithm>
#include <libcockatrice/settings/cache_settings.h>
#include <utility>
// never cache more than 300 cards at once for a single deck
#define CACHED_CARD_PER_DECK_MAX 300
CardPictureLoader::CardPictureLoader() : QObject(nullptr)
{
worker = new CardPictureLoaderWorker;
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged);
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
statusBar = new CardPictureLoaderStatusBar(nullptr);
QMainWindow *mainWindow = qobject_cast<QMainWindow *>(QApplication::activeWindow());
if (mainWindow) {
mainWindow->statusBar()->addPermanentWidget(statusBar);
}
connect(worker, &CardPictureLoaderWorker::imageRequestQueued, statusBar,
&CardPictureLoaderStatusBar::addQueuedImageLoad);
connect(worker, &CardPictureLoaderWorker::imageRequestSucceeded, statusBar,
&CardPictureLoaderStatusBar::addSuccessfulImageLoad);
}
CardPictureLoader::~CardPictureLoader()
{
worker->deleteLater();
}
void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::gray); // Fallback to a gray pixmap
} else {
qCDebug(CardPictureLoaderLog) << "Successfully loaded 'theme:cardback'.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey =
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::blue); // Fallback with blue color
} else {
qCDebug(CardPictureLoaderCardBackCacheFailLog)
<< "Successfully loaded 'theme:cardback' for in-progress state.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey =
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::red); // Fallback with red color
} else {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "Successfully loaded 'theme:cardback' for failed state.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize size)
{
if (!card) {
qCWarning(CardPictureLoaderLog) << "getPixmap called with null card!";
return;
}
QString key = card.getPixmapCacheKey();
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + "x" + QString::number(size.height());
if (QPixmapCache::find(sizeKey, &pixmap)) {
return; // Use cached version
}
// load the image and create a copy of the correct size
QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) {
qCDebug(CardPictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!";
return;
}
QScreen *screen = qApp->primaryScreen();
qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
qCDebug(CardPictureLoaderLog) << "Scaling cached image for" << card.getName();
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pixmap.setDevicePixelRatio(dpr);
QPixmapCache::insert(sizeKey, pixmap);
return;
}
// add the card to the load queue
qCDebug(CardPictureLoaderLog) << "Enqueuing " << card.getName() << " for " << card.getPixmapCacheKey();
getInstance().worker->enqueueImageLoad(card);
}
void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
{
if (image.isNull()) {
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap());
} else {
if (card.getInfo().getUpsideDownArt()) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical);
#else
QImage mirrorImage = image.mirrored(true, true);
#endif
QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
} else {
QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap::fromImage(image));
}
}
// imageLoaded should only be reached if the exactCard isn't already in cache.
// (plus there's a deduplication mechanism in CardPictureLoaderWorker)
// It should be safe to connect the CardInfo here without worrying about redundant connections.
connect(card.getCardPtr().data(), &QObject::destroyed, this,
[cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); });
card.emitPixmapUpdated();
}
void CardPictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
}
void CardPictureLoader::clearNetworkCache()
{
getInstance().worker->clearNetworkCache();
}
void CardPictureLoader::cacheCardPixmaps(const QList<ExactCard> &cards)
{
QPixmap tmp;
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
for (int i = 0; i < max; ++i) {
const ExactCard &card = cards.at(i);
if (!card) {
continue;
}
QString key = card.getPixmapCacheKey();
if (QPixmapCache::find(key, &tmp)) {
continue;
}
getInstance().worker->enqueueImageLoad(card);
}
}
void CardPictureLoader::picDownloadChanged()
{
QPixmapCache::clear();
}
void CardPictureLoader::picsPathChanged()
{
QPixmapCache::clear();
}
bool CardPictureLoader::hasCustomArt()
{
auto picsPath = SettingsCache::instance().getPicsPath();
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
// Check if there is at least one non-directory file in the pics path, other
// than in the "downloadedPics" subdirectory.
while (it.hasNext()) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
QFileInfo dir(it.nextFileInfo());
#else
// nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
// the QFileInfo from a QString which requires more system calls.
QFileInfo dir(it.next());
#endif
if (it.fileName() == "downloadedPics")
continue;
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
if (subIt.hasNext()) {
return true;
}
}
return false;
}

View file

@ -0,0 +1,58 @@
/**
* @file card_picture_loader.h
* @ingroup PictureLoader
* @brief TODO: Document this.
*/
#ifndef CARD_PICTURE_LOADER_H
#define CARD_PICTURE_LOADER_H
#include "card_picture_loader_status_bar.h"
#include "card_picture_loader_worker.h"
#include <QLoggingCategory>
#include <libcockatrice/card/card_info.h>
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
inline Q_LOGGING_CATEGORY(CardPictureLoaderCardBackCacheFailLog, "card_picture_loader.card_back_cache_fail");
class CardPictureLoader : public QObject
{
Q_OBJECT
public:
static CardPictureLoader &getInstance()
{
static CardPictureLoader instance;
return instance;
}
private:
explicit CardPictureLoader();
~CardPictureLoader() override;
// Singleton - Don't implement copy constructor and assign operator
CardPictureLoader(CardPictureLoader const &);
void operator=(CardPictureLoader const &);
CardPictureLoaderWorker *worker;
CardPictureLoaderStatusBar *statusBar;
public:
static void getPixmap(QPixmap &pixmap, const ExactCard &card, QSize size);
static void getCardBackPixmap(QPixmap &pixmap, QSize size);
static void getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size);
static void getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size);
static void clearPixmapCache();
static void cacheCardPixmaps(const QList<ExactCard> &cards);
static bool hasCustomArt();
public slots:
static void clearNetworkCache();
private slots:
void picDownloadChanged();
void picsPathChanged();
public slots:
void imageLoaded(const ExactCard &card, const QImage &image);
};
#endif

View file

@ -0,0 +1,155 @@
#include "card_picture_loader_local.h"
#include "card_picture_to_load.h"
#include <QDirIterator>
#include <QMovie>
#include <libcockatrice/card/card_database/card_database_manager.h>
#include <libcockatrice/settings/cache_settings.h>
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
: QObject(parent), picsPath(SettingsCache::instance().getPicsPath()),
customPicsPath(SettingsCache::instance().getCustomPicsPath())
{
// Hook up signals to settings
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this,
&CardPictureLoaderLocal::picsPathChanged);
refreshIndex();
refreshTimer = new QTimer(this);
connect(refreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex);
refreshTimer->start(REFRESH_INTERVAL_MS);
}
void CardPictureLoaderLocal::refreshIndex()
{
customFolderIndex.clear();
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (thisFileInfo.isFile()) {
// We don't know which name is the correctedName because there might be '.'s in the cardName.
// Just add all possibilities to be sure.
customFolderIndex.insert(thisFileInfo.baseName(), thisFileInfo.absoluteFilePath());
customFolderIndex.insert(thisFileInfo.completeBaseName(), thisFileInfo.absoluteFilePath());
}
}
qCDebug(CardPictureLoaderLocalLog) << "Finished indexing local image folder CUSTOM; map now has"
<< customFolderIndex.size() << "entries.";
}
/**
* Tries to load the card image from the local images.
*
* @param toLoad The card to load
* @return The loaded image, or an empty QImage if loading failed.
*/
QImage CardPictureLoaderLocal::tryLoad(const ExactCard &toLoad) const
{
PrintingInfo setInstance = toLoad.getPrinting();
QString cardName = toLoad.getName();
QString correctedCardName = toLoad.getInfo().getCorrectedName();
QString setName, collectorNumber, providerId;
if (setInstance.getSet()) {
setName = setInstance.getSet()->getCorrectedShortName();
collectorNumber = setInstance.getProperty("num");
providerId = setInstance.getUuid();
}
qCDebug(CardPictureLoaderLocalLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: Attempting to load picture from local";
return tryLoadCardImageFromDisk(setName, correctedCardName, collectorNumber, providerId);
}
QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
const QString &correctedCardName,
const QString &collectorNumber,
const QString &providerId) const
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
// Most-to-least specific, these will fall through in order.
QStringList nameVariants;
// cardName_providerId
if (!providerId.isEmpty()) {
nameVariants << QString("%1-%2").arg(correctedCardName, providerId)
<< QString("%1_%2").arg(correctedCardName, providerId);
}
// cardName_setName_collectorNumber & setName-collectorNumber-cardName
if (!setName.isEmpty() && !collectorNumber.isEmpty()) {
nameVariants << QString("%1_%2_%3").arg(correctedCardName, setName, collectorNumber)
<< QString("%1-%2-%3").arg(setName, collectorNumber, correctedCardName);
}
// cardName_setName
if (!setName.isEmpty()) {
nameVariants << QString("%1_%2").arg(correctedCardName, setName)
<< QString("%1-%2").arg(setName, correctedCardName);
}
// cardName
nameVariants << correctedCardName;
for (const QString &nameVariant : nameVariants) {
if (nameVariant.isEmpty()) {
continue;
}
QStringList candidatePaths = customFolderIndex.values(nameVariant);
if (!setName.isEmpty()) {
candidatePaths << picsPath + "/" + setName + "/" + nameVariant;
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
}
for (const QString &path : candidatePaths) {
QFileInfo fileInfo(path);
QDir dir = fileInfo.dir();
QString baseName = fileInfo.fileName();
if (!dir.exists()) {
continue;
}
QStringList files = dir.entryList(QDir::Files);
for (const QString &file : files) {
if (!file.startsWith(baseName)) {
continue;
}
QString fullPath = dir.filePath(file);
imgReader.setFileName(fullPath);
if (imgReader.read(&image)) {
qCDebug(CardPictureLoaderLocalLog).nospace()
<< "[card: " << correctedCardName << " set: " << setName << "] Found picture at: " << fullPath;
return image;
}
}
}
}
qCDebug(CardPictureLoaderLocalLog).nospace()
<< "[card: " << correctedCardName << " set: " << setName << "]: Picture NOT found on disk.";
return QImage();
}
void CardPictureLoaderLocal::picsPathChanged()
{
picsPath = SettingsCache::instance().getPicsPath();
customPicsPath = SettingsCache::instance().getCustomPicsPath();
}

View file

@ -0,0 +1,47 @@
/**
* @file card_picture_loader_local.h
* @ingroup PictureLoader
* @brief TODO: Document this.
*/
#ifndef PICTURE_LOADER_LOCAL_H
#define PICTURE_LOADER_LOCAL_H
#include <QLoggingCategory>
#include <QObject>
#include <QTimer>
#include <libcockatrice/card/card_printing/exact_card.h>
inline Q_LOGGING_CATEGORY(CardPictureLoaderLocalLog, "card_picture_loader.local");
/**
* Handles searching for and loading card images from the local pics and custom image folders.
* This class maintains an index of the CUSTOM folder, to avoid repeatedly searching the entire directory.
*/
class CardPictureLoaderLocal : public QObject
{
Q_OBJECT
public:
explicit CardPictureLoaderLocal(QObject *parent);
QImage tryLoad(const ExactCard &toLoad) const;
private:
QString picsPath, customPicsPath;
QMultiHash<QString, QString> customFolderIndex; // multimap of cardName to picPaths
QTimer *refreshTimer;
void refreshIndex();
QImage tryLoadCardImageFromDisk(const QString &setName,
const QString &correctedCardName,
const QString &collectorNumber,
const QString &providerId) const;
private slots:
void picsPathChanged();
};
#endif // PICTURE_LOADER_LOCAL_H

View file

@ -0,0 +1,31 @@
#include "card_picture_loader_request_status_display_widget.h"
CardPictureLoaderRequestStatusDisplayWidget::CardPictureLoaderRequestStatusDisplayWidget(QWidget *parent,
const QUrl &_url,
const ExactCard &card,
const QString &setName)
: QWidget(parent)
{
layout = new QHBoxLayout(this);
name = new QLabel(this);
name->setText(card.getName());
setShortname = new QLabel(this);
setShortname->setText(setName);
providerId = new QLabel(this);
providerId->setText(card.getPrinting().getUuid());
layout->addWidget(name);
layout->addWidget(setShortname);
layout->addWidget(providerId);
startTime = new QLabel(QDateTime::currentDateTime().toString(), this);
elapsedTime = new QLabel("0", this);
finished = new QLabel("False", this);
url = new QLabel(_url.toString(), this);
layout->addWidget(startTime);
layout->addWidget(elapsedTime);
layout->addWidget(finished);
layout->addWidget(url);
}

View file

@ -0,0 +1,74 @@
/**
* @file card_picture_loader_request_status_display_widget.h
* @ingroup PictureLoader
* @brief TODO: Document this.
*/
#ifndef PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
#define PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
#include "card_picture_loader_worker_work.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class CardPictureLoaderRequestStatusDisplayWidget : public QWidget
{
Q_OBJECT
public:
CardPictureLoaderRequestStatusDisplayWidget(QWidget *parent,
const QUrl &url,
const ExactCard &card,
const QString &setName);
void setFinished()
{
finished->setText("True");
update();
repaint();
}
bool getFinished() const
{
return finished->text() == "True";
}
void setElapsedTime(const QString &_elapsedTime) const
{
elapsedTime->setText(_elapsedTime);
}
int queryElapsedSeconds()
{
if (!finished) {
int elapsedSeconds = QDateTime::fromString(startTime->text()).secsTo(QDateTime::currentDateTime());
elapsedTime->setText(QString::number(elapsedSeconds));
update();
repaint();
return elapsedSeconds;
}
return elapsedTime->text().toInt();
}
QString getStartTime() const
{
return startTime->text();
}
QString getUrl() const
{
return url->text();
}
private:
QHBoxLayout *layout;
QLabel *name;
QLabel *setShortname;
QLabel *providerId;
QLabel *startTime;
QLabel *elapsedTime;
QLabel *finished;
QLabel *url;
};
#endif // PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H

View file

@ -0,0 +1,57 @@
#include "card_picture_loader_status_bar.h"
#include "card_picture_loader_request_status_display_widget.h"
CardPictureLoaderStatusBar::CardPictureLoaderStatusBar(QWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
progressBar = new QProgressBar(this);
progressBar->setMaximum(0);
progressBar->setFormat("%v/%m");
layout->addWidget(progressBar);
loadLog = new SettingsButtonWidget(this);
layout->addWidget(loadLog);
cleaner = new QTimer(this);
cleaner->setInterval(1000);
connect(cleaner, &QTimer::timeout, this, &CardPictureLoaderStatusBar::cleanOldEntries);
cleaner->start();
setLayout(layout);
}
void CardPictureLoaderStatusBar::cleanOldEntries()
{
if (!loadLog || !loadLog->popup) {
return;
}
for (CardPictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
loadLog->popup->findChildren<CardPictureLoaderRequestStatusDisplayWidget *>()) {
statusDisplayWidget->queryElapsedSeconds();
if (statusDisplayWidget->getFinished() &&
QDateTime::fromString(statusDisplayWidget->getStartTime()).secsTo(QDateTime::currentDateTime()) > 10) {
loadLog->removeSettingsWidget(statusDisplayWidget);
progressBar->setMaximum(progressBar->maximum() - 1);
progressBar->setValue(progressBar->value() - 1);
}
}
}
void CardPictureLoaderStatusBar::addQueuedImageLoad(const QUrl &url, const ExactCard &card, const QString &setName)
{
loadLog->addSettingsWidget(new CardPictureLoaderRequestStatusDisplayWidget(loadLog, url, card, setName));
progressBar->setMaximum(progressBar->maximum() + 1);
}
void CardPictureLoaderStatusBar::addSuccessfulImageLoad(const QUrl &url)
{
progressBar->setValue(progressBar->value() + 1);
for (CardPictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
loadLog->popup->findChildren<CardPictureLoaderRequestStatusDisplayWidget *>()) {
if (statusDisplayWidget->getUrl() == url.toString()) {
statusDisplayWidget->queryElapsedSeconds();
statusDisplayWidget->setFinished();
}
}
}

View file

@ -0,0 +1,35 @@
/**
* @file card_picture_loader_status_bar.h
* @ingroup PictureLoader
* @brief TODO: Document this.
*/
#ifndef PICTURE_LOADER_STATUS_BAR_H
#define PICTURE_LOADER_STATUS_BAR_H
#include "../../interface/widgets/quick_settings/settings_button_widget.h"
#include "card_picture_loader_worker_work.h"
#include <QHBoxLayout>
#include <QProgressBar>
#include <QWidget>
class CardPictureLoaderStatusBar : public QWidget
{
Q_OBJECT
public:
explicit CardPictureLoaderStatusBar(QWidget *parent);
public slots:
void addQueuedImageLoad(const QUrl &url, const ExactCard &card, const QString &setName);
void addSuccessfulImageLoad(const QUrl &url);
void cleanOldEntries();
private:
QHBoxLayout *layout;
QProgressBar *progressBar;
SettingsButtonWidget *loadLog;
QTimer *cleaner;
};
#endif // PICTURE_LOADER_STATUS_BAR_H

View file

@ -0,0 +1,238 @@
#include "card_picture_loader_worker.h"
#include "card_picture_loader_local.h"
#include "card_picture_loader_worker_work.h"
#include <QDirIterator>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QThread>
#include <libcockatrice/card/card_database/card_database_manager.h>
#include <libcockatrice/settings/cache_settings.h>
#include <utility>
static constexpr int MAX_REQUESTS_PER_SEC = 10;
CardPictureLoaderWorker::CardPictureLoaderWorker()
: QObject(nullptr), picDownload(SettingsCache::instance().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC)
{
networkManager = new QNetworkAccessManager(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
// Use Qt's default timeout (30s, as of 2023-02-22)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout();
#endif
cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L *
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, this,
[this](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
networkManager->setCache(cache);
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
loadRedirectCache();
cleanStaleEntries();
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
&CardPictureLoaderWorker::saveRedirectCache);
localLoader = new CardPictureLoaderLocal(this);
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
connect(this, &CardPictureLoaderWorker::imageLoadEnqueued, this, &CardPictureLoaderWorker::handleImageLoadEnqueued);
connect(&requestTimer, &QTimer::timeout, this, &CardPictureLoaderWorker::resetRequestQuota);
requestTimer.setInterval(1000);
requestTimer.start();
}
CardPictureLoaderWorker::~CardPictureLoaderWorker()
{
saveRedirectCache();
pictureLoaderThread->deleteLater();
}
void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker)
{
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
queueRequest(cachedRedirect, worker);
} else if (cache->metaData(url).isValid()) {
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the rate-limit
makeRequest(url, worker);
} else {
requestLoadQueue.append(qMakePair(url, worker));
emit imageRequestQueued(url, worker->cardToDownload.getCard(), worker->cardToDownload.getSetName());
processQueuedRequests();
}
}
QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker)
{
// Check for cached redirects
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
emit imageRequestSucceeded(url);
return makeRequest(cachedRedirect, worker);
}
QNetworkRequest req(url);
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}
QNetworkReply *reply = networkManager->get(req);
// Connect reply handling
connect(reply, &QNetworkReply::finished, worker, [reply, worker] { worker->handleNetworkReply(reply); });
return reply;
}
void CardPictureLoaderWorker::resetRequestQuota()
{
requestQuota = MAX_REQUESTS_PER_SEC;
processQueuedRequests();
}
/**
* Keeps processing requests from the queue until it is empty or until the quota runs out.
*/
void CardPictureLoaderWorker::processQueuedRequests()
{
while (requestQuota > 0 && processSingleRequest()) {
--requestQuota;
}
}
/**
* Immediately processes a single queued request. No-ops if the load queue is empty
* @return If a request was processed
*/
bool CardPictureLoaderWorker::processSingleRequest()
{
if (!requestLoadQueue.isEmpty()) {
auto request = requestLoadQueue.takeFirst();
makeRequest(request.first, request.second);
return true;
}
return false;
}
void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
{
// Send call through a connection to ensure the handling is run on the pictureLoader thread
emit imageLoadEnqueued(card);
}
void CardPictureLoaderWorker::handleImageLoadEnqueued(const ExactCard &card)
{
// deduplicate loads for the same card
if (currentlyLoading.contains(card.getPixmapCacheKey())) {
qCDebug(CardPictureLoaderWorkerLog())
<< "Skipping enqueued" << card.getName() << "because it's already being loaded";
return;
}
currentlyLoading.insert(card.getPixmapCacheKey());
// try to load image from local first
QImage image = localLoader->tryLoad(card);
if (!image.isNull()) {
handleImageLoaded(card, image);
} else {
// queue up to load image from remote only after local loading failed
new CardPictureLoaderWorkerWork(this, card);
}
}
/**
* Called when image loading is done. Failures are indicated by an empty QImage.
*/
void CardPictureLoaderWorker::handleImageLoaded(const ExactCard &card, const QImage &image)
{
currentlyLoading.remove(card.getPixmapCacheKey());
emit imageLoaded(card, image);
}
void CardPictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
{
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
// saveRedirectCache();
}
void CardPictureLoaderWorker::removedCachedUrl(const QUrl &url)
{
networkManager->cache()->remove(url);
}
QUrl CardPictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
{
if (redirectCache.contains(originalUrl)) {
return redirectCache[originalUrl].first;
}
return {};
}
void CardPictureLoaderWorker::loadRedirectCache()
{
QSettings settings(cacheFilePath, QSettings::IniFormat);
redirectCache.clear();
int size = settings.beginReadArray(REDIRECT_HEADER_NAME);
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
QUrl originalUrl = settings.value(REDIRECT_ORIGINAL_URL).toUrl();
QUrl redirectUrl = settings.value(REDIRECT_URL).toUrl();
QDateTime timestamp = settings.value(REDIRECT_TIMESTAMP).toDateTime();
if (originalUrl.isValid() && redirectUrl.isValid()) {
redirectCache[originalUrl] = qMakePair(redirectUrl, timestamp);
}
}
settings.endArray();
}
void CardPictureLoaderWorker::saveRedirectCache() const
{
QSettings settings(cacheFilePath, QSettings::IniFormat);
settings.beginWriteArray(REDIRECT_HEADER_NAME, static_cast<int>(redirectCache.size()));
int index = 0;
for (auto it = redirectCache.cbegin(); it != redirectCache.cend(); ++it) {
settings.setArrayIndex(index++);
settings.setValue(REDIRECT_ORIGINAL_URL, it.key());
settings.setValue(REDIRECT_URL, it.value().first);
settings.setValue(REDIRECT_TIMESTAMP, it.value().second);
}
settings.endArray();
}
void CardPictureLoaderWorker::cleanStaleEntries()
{
QDateTime now = QDateTime::currentDateTimeUtc();
auto it = redirectCache.begin();
while (it != redirectCache.end()) {
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
it = redirectCache.erase(it); // Remove stale entry
} else {
++it;
}
}
}
void CardPictureLoaderWorker::clearNetworkCache()
{
networkManager->cache()->clear();
redirectCache.clear();
}

View file

@ -0,0 +1,84 @@
/**
* @file card_picture_loader_worker.h
* @ingroup PictureLoader
* @brief TODO: Document this.
*/
#ifndef PICTURE_LOADER_WORKER_H
#define PICTURE_LOADER_WORKER_H
#include "card_picture_loader_local.h"
#include "card_picture_loader_worker_work.h"
#include "card_picture_to_load.h"
#include <QLoggingCategory>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QObject>
#include <QQueue>
#include <QTimer>
#include <libcockatrice/card/card_database/card_database.h>
#include <libcockatrice/card/card_info.h>
#define REDIRECT_HEADER_NAME "redirects"
#define REDIRECT_ORIGINAL_URL "original"
#define REDIRECT_URL "redirect"
#define REDIRECT_TIMESTAMP "timestamp"
#define REDIRECT_CACHE_FILENAME "cache.ini"
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerLog, "card_picture_loader.worker");
class CardPictureLoaderWorkerWork;
class CardPictureLoaderWorker : public QObject
{
Q_OBJECT
public:
explicit CardPictureLoaderWorker();
~CardPictureLoaderWorker() override;
void enqueueImageLoad(const ExactCard &card); // Starts a thread for the image to be loaded
void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker); // Queues network requests for load threads
void clearNetworkCache();
public slots:
QNetworkReply *makeRequest(const QUrl &url, CardPictureLoaderWorkerWork *workThread);
void processQueuedRequests();
bool processSingleRequest();
void handleImageLoaded(const ExactCard &card, const QImage &image);
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
void removedCachedUrl(const QUrl &url);
private:
QThread *pictureLoaderThread;
QNetworkAccessManager *networkManager;
QNetworkDiskCache *cache;
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
QString cacheFilePath; // Path to persistent storage
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
bool picDownload;
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue;
int requestQuota;
QTimer requestTimer; // Timer for refreshing request quota
CardPictureLoaderLocal *localLoader;
QSet<QString> currentlyLoading; // for deduplication purposes. Contains pixmapCacheKey
QUrl getCachedRedirect(const QUrl &originalUrl) const;
void loadRedirectCache();
void saveRedirectCache() const;
void cleanStaleEntries();
private slots:
void resetRequestQuota();
void handleImageLoadEnqueued(const ExactCard &card);
signals:
void imageLoadEnqueued(const ExactCard &card);
void imageLoaded(const ExactCard &card, const QImage &image);
void imageRequestQueued(const QUrl &url, const ExactCard &card, const QString &setName);
void imageRequestSucceeded(const QUrl &url);
};
#endif // PICTURE_LOADER_WORKER_H

View file

@ -0,0 +1,223 @@
#include "card_picture_loader_worker_work.h"
#include "card_picture_loader_worker.h"
#include <QBuffer>
#include <QDirIterator>
#include <QLoggingCategory>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QThread>
#include <QThreadPool>
#include <libcockatrice/card/card_database/card_database_manager.h>
#include <libcockatrice/settings/cache_settings.h>
// Card back returned by gatherer when card is not found
static const QStringList MD5_BLACKLIST = {"db0c48db407a907c16ade38de048a441"};
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
picDownload(SettingsCache::instance().getPicDownload())
{
// Hook up signals to the orchestrator
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest);
connect(this, &CardPictureLoaderWorkerWork::urlRedirected, worker, &CardPictureLoaderWorker::cacheRedirect);
connect(this, &CardPictureLoaderWorkerWork::cachedUrlInvalidated, worker,
&CardPictureLoaderWorker::removedCachedUrl);
connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
&CardPictureLoaderWorker::imageRequestSucceeded);
// Hook up signals to settings
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
startNextPicDownload();
}
void CardPictureLoaderWorkerWork::startNextPicDownload()
{
QString picUrl = cardToDownload.getCurrentUrl();
if (picUrl.isEmpty()) {
picDownloadFailed();
} else {
QUrl url(picUrl);
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Trying to fetch picture from url "
<< url.toDisplayString();
emit requestImageDownload(url, this);
}
}
/**
* Starts another pic download using the next possible url combination for the card.
* If all possibilities are exhausted, then concludes the image loading with an empty QImage.
*/
void CardPictureLoaderWorkerWork::picDownloadFailed()
{
/* Take advantage of short-circuiting here to call the nextUrl until one
is not available. Only once nextUrl evaluates to false will this move
on to nextSet. If the Urls for a particular card are empty, this will
effectively go through the sets for that card. */
if (cardToDownload.nextUrl() || cardToDownload.nextSet()) {
startNextPicDownload();
} else {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
concludeImageLoad(QImage());
}
}
/**
*
* @param reply The reply. Takes ownership of the object
*/
void CardPictureLoaderWorkerWork::handleNetworkReply(QNetworkReply *reply)
{
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (redirectTarget.isValid()) {
QUrl url = reply->request().url();
QUrl redirectUrl = redirectTarget.toUrl();
if (redirectUrl.isRelative()) {
redirectUrl = url.resolved(redirectUrl);
}
emit urlRedirected(url, redirectUrl);
}
if (reply->error()) {
handleFailedReply(reply);
} else {
handleSuccessfulReply(reply);
emit requestSucceeded(reply->url());
}
reply->deleteLater();
}
static bool imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return MD5_BLACKLIST.contains(md5sum);
}
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
{
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests.";
} else {
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (isFromCache) {
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
<< " set: " << cardToDownload.getSetName() << "]: Removing corrupted cache file for url "
<< reply->url().toDisplayString() << " and retrying (" << reply->errorString() << ")";
emit cachedUrlInvalidated(reply->url());
emit requestImageDownload(reply->url(), this);
} else {
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
<< " set: " << cardToDownload.getSetName() << "]: " << (picDownload ? "Download" : "Cache search")
<< " failed for url " << reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
}
}
}
void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
statusCode == 308) {
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName() << " set: " << cardToDownload.getSetName()
<< "]: following " << (isFromCache ? "cached redirect" : "redirect") << " to "
<< redirectUrl.toDisplayString();
emit requestImageDownload(redirectUrl, this);
return;
}
// peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size());
if (imageIsBlackListed(picData)) {
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName() << " set: " << cardToDownload.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
return;
}
QImage image = tryLoadImageFromReply(reply);
if (image.isNull()) {
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName() << " set: " << cardToDownload.getSetName()
<< "]: Possible " << (isFromCache ? "cached" : "downloaded") << " picture at "
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
picDownloadFailed();
} else {
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName() << " set: " << cardToDownload.getSetName()
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
<< reply->url().toDisplayString();
concludeImageLoad(image);
}
}
/**
* @param reply The reply to load the image from
* @return The loaded image, or an empty QImage if loading failed
*/
QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
{
static constexpr int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
auto replyHeader = reply->peek(riffHeaderSize);
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
auto imgBuf = QBuffer(this);
imgBuf.setData(reply->readAll());
auto movie = QMovie(&imgBuf);
movie.start();
movie.stop();
return movie.currentImage();
}
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
return imgReader.read();
}
/**
* Call this method when the image has finished being loaded.
* @param image The image that was loaded. Empty QImage indicates failure.
*/
void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
{
emit imageLoaded(cardToDownload.getCard(), image);
deleteLater();
}
void CardPictureLoaderWorkerWork::picDownloadChanged()
{
picDownload = SettingsCache::instance().getPicDownload();
}

View file

@ -0,0 +1,72 @@
/**
* @file picture_loader_worker_work.h
* @ingroup PictureLoader
* @brief TODO: Document this.
*/
#ifndef PICTURE_LOADER_WORKER_WORK_H
#define PICTURE_LOADER_WORKER_WORK_H
#include "card_picture_loader_worker.h"
#include "card_picture_to_load.h"
#include <QLoggingCategory>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QObject>
#include <QThread>
#include <libcockatrice/card/card_database/card_database.h>
#define REDIRECT_HEADER_NAME "redirects"
#define REDIRECT_ORIGINAL_URL "original"
#define REDIRECT_URL "redirect"
#define REDIRECT_TIMESTAMP "timestamp"
#define REDIRECT_CACHE_FILENAME "cache.ini"
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
class CardPictureLoaderWorker;
class CardPictureLoaderWorkerWork : public QObject
{
Q_OBJECT
public:
explicit CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad);
CardPictureToLoad cardToDownload;
public slots:
void handleNetworkReply(QNetworkReply *reply);
private:
bool picDownload;
void startNextPicDownload();
void picDownloadFailed();
void handleFailedReply(const QNetworkReply *reply);
void handleSuccessfulReply(QNetworkReply *reply);
QImage tryLoadImageFromReply(QNetworkReply *reply);
void concludeImageLoad(const QImage &image);
private slots:
void picDownloadChanged();
signals:
/**
* Emitted when this worker has successfully loaded the image or has exhausted all attempts at loading the image.
* Failures are represented by an empty QImage.
* Note that this object will delete itself as this signal is emitted.
*/
void imageLoaded(const ExactCard &card, const QImage &image);
/**
* Emitted when a request did not return a 400 or 500 response
*/
void requestSucceeded(const QUrl &url);
void requestImageDownload(const QUrl &url, CardPictureLoaderWorkerWork *instance);
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl);
void cachedUrlInvalidated(const QUrl &url);
};
#endif // PICTURE_LOADER_WORKER_WORK_H

View file

@ -0,0 +1,290 @@
#include "card_picture_to_load.h"
#include <QCoreApplication>
#include <QDate>
#include <QRegularExpression>
#include <QUrl>
#include <algorithm>
#include <libcockatrice/card/card_set/card_set_comparator.h>
#include <libcockatrice/settings/cache_settings.h>
CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
{
if (card) {
sortedSets = extractSetsSorted(card);
// The first time called, nextSet will also populate the Urls for the first set.
nextSet();
}
}
/**
* Extracts a list of all the sets from the card, sorted in priority order.
* If the card does not contain any sets, then a dummy set will be inserted into the list.
*
* @return A list of sets. Will not be empty.
*/
QList<CardSetPtr> CardPictureToLoad::extractSetsSorted(const ExactCard &card)
{
QList<CardSetPtr> sortedSets;
for (const auto &printings : card.getInfo().getSets()) {
for (const auto &printing : printings) {
sortedSets << printing.getSet();
}
}
if (sortedSets.empty()) {
sortedSets << CardSet::newInstance("", "", "", QDate());
}
std::sort(sortedSets.begin(), sortedSets.end(), SetPriorityComparator());
// If the user hasn't disabled arts other than their personal preference...
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) {
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
qsizetype setIndex = sortedSets.indexOf(card.getPrinting().getSet());
if (setIndex > 0) { // we don't need to move the set if it's already first
CardSetPtr setForCardProviderID = sortedSets.takeAt(setIndex);
sortedSets.prepend(setForCardProviderID);
}
}
return sortedSets;
}
/**
* Finds the PrintingInfo corresponding to the exactCards's card name that belongs to a given set and has the
* exactCards's providerId.
* If the set name is in the CardInfo, but no printings in that set match the card's providerId, then the first
* PrintingInfo for the set is returned.
*
* This method only exists to maintain existing behavior.
* TODO: check if going through all sets is still necessary after the ExactCard refactor.
*
* @param card The card to look in
* @param setName The set's short name
* @return A PrintingInfo, or a default-constructed PrintingInfo if the set name is not in the CardInfo.
*/
static PrintingInfo findPrintingForSet(const ExactCard &card, const QString &setName)
{
SetToPrintingsMap setsToPrintings = card.getInfo().getSets();
if (!setsToPrintings.contains(setName))
return PrintingInfo();
for (const auto &printing : setsToPrintings[setName]) {
if (printing.getUuid() == card.getPrinting().getUuid()) {
return printing;
}
}
return setsToPrintings[setName][0];
}
void CardPictureToLoad::populateSetUrls()
{
/* currentSetUrls is a list, populated each time a new set is requested for a particular card
and Urls are removed from it as a download is attempted from each one. Custom Urls for
a set are given higher priority, so should be placed first in the list. */
currentSetUrls.clear();
if (card && currentSet) {
QString setCustomURL = findPrintingForSet(card, currentSet->getShortName()).getProperty("picurl");
if (!setCustomURL.isEmpty()) {
currentSetUrls.append(setCustomURL);
}
}
for (const QString &urlTemplate : urlTemplates) {
QString transformedUrl = transformUrl(urlTemplate);
if (!transformedUrl.isEmpty()) {
currentSetUrls.append(transformedUrl);
}
}
/* Call nextUrl to make sure currentUrl is up-to-date
but we don't need the result here. */
(void)nextUrl();
}
/**
* Advances the currentSet to the next set in the list. Then repopulates the url list with the urls from that set.
* If we are already at the end of the list, then currentSet is set to empty.
* @return If we are already at the end of the list
*/
bool CardPictureToLoad::nextSet()
{
if (!sortedSets.isEmpty()) {
currentSet = sortedSets.takeFirst();
populateSetUrls();
return true;
}
currentSet = {};
return false;
}
/**
* Advances the currentUrl to the next url in the list.
* If we are already at the end of the list, then currentUrl is set to empty.
* @return If we are already at the end of the list
*/
bool CardPictureToLoad::nextUrl()
{
if (!currentSetUrls.isEmpty()) {
currentUrl = currentSetUrls.takeFirst();
return true;
}
currentUrl = QString();
return false;
}
QString CardPictureToLoad::getSetName() const
{
if (currentSet) {
return currentSet->getCorrectedShortName();
} else {
return QString();
}
}
static int parse(const QString &urlTemplate,
const QString &propType,
const QString &cardName,
const QString &setName,
std::function<QString(const QString &)> getProperty,
QMap<QString, QString> &transformMap)
{
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
auto matches = rxCardProp.globalMatch(urlTemplate);
while (matches.hasNext()) {
auto match = matches.next();
QString templatePropertyName = match.captured(1);
auto fillMatch = rxFillWith.match(templatePropertyName);
QString cardPropertyName;
QString fillWith;
int subStrPos = 0;
int subStrLen = -1;
if (fillMatch.hasMatch()) {
cardPropertyName = fillMatch.captured(1);
fillWith = fillMatch.captured(2);
} else {
fillWith = QString();
auto subStrMatch = rxSubStr.match(templatePropertyName);
if (subStrMatch.hasMatch()) {
cardPropertyName = subStrMatch.captured(1);
subStrPos = subStrMatch.captured(2).toInt();
subStrLen = subStrMatch.captured(3).toInt();
} else {
cardPropertyName = templatePropertyName;
}
}
QString propertyValue = getProperty(cardPropertyName);
if (propertyValue.isEmpty()) {
qCDebug(CardPictureToLoadLog).nospace()
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
<< "property (" << cardPropertyName << ") for Url template (" << urlTemplate << ") is not available";
return 1;
} else {
int propLength = propertyValue.length();
if (subStrLen > 0) {
if (subStrPos + subStrLen > propLength) {
qCDebug(CardPictureToLoadLog).nospace()
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
<< " property (" << cardPropertyName << ") for Url template (" << urlTemplate
<< ") is smaller than substr specification (" << subStrPos << " + " << subStrLen << " > "
<< propLength << ")";
return 1;
} else {
propertyValue = propertyValue.mid(subStrPos, subStrLen);
propLength = subStrLen;
}
}
if (!fillWith.isEmpty()) {
int fillLength = fillWith.length();
if (fillLength < propLength) {
qCDebug(CardPictureToLoadLog).nospace()
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
<< " property (" << cardPropertyName << ") for Url template (" << urlTemplate
<< ") is longer than fill specification (" << fillWith << ")";
return 1;
} else {
propertyValue = fillWith.left(fillLength - propLength) + propertyValue;
}
}
transformMap["!" + propType + ":" + templatePropertyName + "!"] = propertyValue;
}
}
return 0;
}
QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
{
/* This function takes Url templates and substitutes actual card details
into the url. This is used for making Urls with follow a predictable format
for downloading images. If information is requested by the template that is
not populated for this specific card/set combination, an empty string is returned.*/
CardSetPtr set = getCurrentSet();
QMap<QString, QString> transformMap = QMap<QString, QString>();
QString setName = getSetName();
// name
QString cardName = card.getName();
transformMap["!name!"] = cardName;
transformMap["!name_lower!"] = card.getName().toLower();
transformMap["!corrected_name!"] = card.getInfo().getCorrectedName();
transformMap["!corrected_name_lower!"] = card.getInfo().getCorrectedName().toLower();
// card properties
if (parse(
urlTemplate, "prop", cardName, setName,
[&](const QString &name) { return card.getInfo().getProperty(name); }, transformMap)) {
return QString();
}
if (set) {
transformMap["!setcode!"] = set->getShortName();
transformMap["!setcode_lower!"] = set->getShortName().toLower();
transformMap["!setname!"] = set->getLongName();
transformMap["!setname_lower!"] = set->getLongName().toLower();
if (parse(
urlTemplate, "set", cardName, setName,
[&](const QString &name) { return findPrintingForSet(card, set->getShortName()).getProperty(name); },
transformMap)) {
return QString();
}
}
// language setting
transformMap["!sflang!"] = QString(QCoreApplication::translate(
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) {
if (transformedUrl.contains(prop)) {
if (!transformMap[prop].isEmpty()) {
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
} else {
/* This means the template is requesting information that is not
* populated in this card, so it should return an empty string,
* indicating an invalid Url.
*/
qCDebug(CardPictureToLoadLog).nospace()
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested information ("
<< prop << ") for Url template (" << urlTemplate << ") is not available";
return QString();
}
}
}
return transformedUrl;
}

View file

@ -0,0 +1,49 @@
/**
* @file card_picture_to_load.h
* @ingroup PictureLoader
* @brief TODO: Document this.
*/
#ifndef PICTURE_TO_LOAD_H
#define PICTURE_TO_LOAD_H
#include <QLoggingCategory>
#include <libcockatrice/card/card_printing/exact_card.h>
inline Q_LOGGING_CATEGORY(CardPictureToLoadLog, "card_picture_loader.picture_to_load");
class CardPictureToLoad
{
private:
ExactCard card;
QList<CardSetPtr> sortedSets;
QList<QString> urlTemplates;
QList<QString> currentSetUrls;
QString currentUrl;
CardSetPtr currentSet;
public:
explicit CardPictureToLoad(const ExactCard &_card);
const ExactCard &getCard() const
{
return card;
}
QString getCurrentUrl() const
{
return currentUrl;
}
CardSetPtr getCurrentSet() const
{
return currentSet;
}
QString getSetName() const;
QString transformUrl(const QString &urlTemplate) const;
bool nextSet();
bool nextUrl();
void populateSetUrls();
static QList<CardSetPtr> extractSetsSorted(const ExactCard &card);
};
#endif // PICTURE_TO_LOAD_H