mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-27 00:53:55 -07:00
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:
parent
be1403c920
commit
1ef07309d6
605 changed files with 3812 additions and 3408 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue