Parallel picture loader (#5977)

* Parallelize picture loader.

* Queue requests instead.

* Include a status bar for the picture loader.

* Save redirect cache on destruction.

* Address comments.

* Let's not overwrite an ambigious variable name.

* Lint.

* Oracle needs the status bar too.

* We actually get a free request if we hit a cached image.

* Fix cmake list.

* toString() the url.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2025-06-13 01:44:38 +02:00 committed by GitHub
parent 9af3fbc35f
commit fe57efb1a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 596 additions and 350 deletions

View file

@ -67,7 +67,10 @@ set(cockatrice_SOURCES
src/client/ui/line_edit_completer.cpp
src/client/ui/phases_toolbar.cpp
src/client/ui/picture_loader/picture_loader.cpp
src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
src/client/ui/picture_loader/picture_loader_status_bar.cpp
src/client/ui/picture_loader/picture_loader_worker.cpp
src/client/ui/picture_loader/picture_loader_worker_work.cpp
src/client/ui/picture_loader/picture_to_load.cpp
src/client/ui/pixel_map_generator.cpp
src/client/ui/theme_manager.cpp

View file

@ -7,12 +7,14 @@
#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 <utility>
@ -27,6 +29,16 @@ PictureLoader::PictureLoader() : QObject(nullptr)
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded);
statusBar = new PictureLoaderStatusBar(nullptr);
QMainWindow *mainWindow = qobject_cast<QMainWindow *>(QApplication::activeWindow());
if (mainWindow) {
mainWindow->statusBar()->addPermanentWidget(statusBar);
}
connect(worker, &PictureLoaderWorker::imageLoadQueued, statusBar, &PictureLoaderStatusBar::addQueuedImageLoad);
connect(worker, &PictureLoaderWorker::imageLoadSuccessful, statusBar,
&PictureLoaderStatusBar::addSuccessfulImageLoad);
}
PictureLoader::~PictureLoader()

View file

@ -2,6 +2,7 @@
#define PICTURELOADER_H
#include "../../../game/cards/card_info.h"
#include "picture_loader_status_bar.h"
#include "picture_loader_worker.h"
#include <QLoggingCategory>
@ -27,6 +28,7 @@ private:
void operator=(PictureLoader const &);
PictureLoaderWorker *worker;
PictureLoaderStatusBar *statusBar;
public:
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);

View file

@ -0,0 +1,32 @@
#include "picture_loader_request_status_display_widget.h"
PictureLoaderRequestStatusDisplayWidget::PictureLoaderRequestStatusDisplayWidget(QWidget *parent,
const QUrl &_url,
PictureLoaderWorkerWork *worker)
: QWidget(parent)
{
layout = new QHBoxLayout(this);
if (worker->cardToDownload.getCard()) {
name = new QLabel(this);
name->setText(worker->cardToDownload.getCard()->getName());
setShortname = new QLabel(this);
setShortname->setText(worker->cardToDownload.getSetName());
providerId = new QLabel(this);
providerId->setText(worker->cardToDownload.getCard()->getProperty("uuid"));
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,66 @@
#ifndef PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
#define PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
#include "picture_loader_worker_work.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class PictureLoaderRequestStatusDisplayWidget : public QWidget
{
Q_OBJECT
public:
PictureLoaderRequestStatusDisplayWidget(QWidget *parent, const QUrl &url, PictureLoaderWorkerWork *worker);
PictureLoaderWorkerWork *worker;
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,58 @@
#include "picture_loader_status_bar.h"
#include "picture_loader_request_status_display_widget.h"
PictureLoaderStatusBar::PictureLoaderStatusBar(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, &PictureLoaderStatusBar::cleanOldEntries);
cleaner->start();
setLayout(layout);
}
void PictureLoaderStatusBar::cleanOldEntries()
{
if (!loadLog || !loadLog->popup) {
return;
}
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
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 PictureLoaderStatusBar::addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
{
loadLog->addSettingsWidget(new PictureLoaderRequestStatusDisplayWidget(loadLog, url, worker));
progressBar->setMaximum(progressBar->maximum() + 1);
}
void PictureLoaderStatusBar::addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
{
Q_UNUSED(worker)
progressBar->setValue(progressBar->value() + 1);
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
if (statusDisplayWidget->getUrl() == url.toString()) {
statusDisplayWidget->queryElapsedSeconds();
statusDisplayWidget->setFinished();
}
}
}

View file

@ -0,0 +1,29 @@
#ifndef PICTURE_LOADER_STATUS_BAR_H
#define PICTURE_LOADER_STATUS_BAR_H
#include "../widgets/quick_settings/settings_button_widget.h"
#include "picture_loader_worker_work.h"
#include <QHBoxLayout>
#include <QProgressBar>
#include <QWidget>
class PictureLoaderStatusBar : public QWidget
{
Q_OBJECT
public:
explicit PictureLoaderStatusBar(QWidget *parent);
public slots:
void addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
void addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
void cleanOldEntries();
private:
QHBoxLayout *layout;
QProgressBar *progressBar;
SettingsButtonWidget *loadLog;
QTimer *cleaner;
};
#endif // PICTURE_LOADER_STATUS_BAR_H

View file

@ -2,13 +2,15 @@
#include "../../../game/cards/card_database_manager.h"
#include "../../../settings/cache_settings.h"
#include "picture_loader_worker_work.h"
#include <QBuffer>
#include <QDirIterator>
#include <QJsonDocument>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QThread>
#include <utility>
// Card back returned by gatherer when card is not found
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
@ -19,11 +21,8 @@ PictureLoaderWorker::PictureLoaderWorker()
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
{
connect(this, &PictureLoaderWorker::startLoadQueue, this, &PictureLoaderWorker::processLoadQueue,
Qt::QueuedConnection);
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &PictureLoaderWorker::picsPathChanged);
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
&PictureLoaderWorker::picDownloadChanged);
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
@ -34,18 +33,17 @@ PictureLoaderWorker::PictureLoaderWorker()
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout();
#endif
auto cache = new QNetworkDiskCache(this);
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, cache,
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
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);
connect(networkManager, &QNetworkAccessManager::finished, this, &PictureLoaderWorker::picDownloadFinished);
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
loadRedirectCache();
@ -57,244 +55,104 @@ PictureLoaderWorker::PictureLoaderWorker()
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
connect(&requestTimer, &QTimer::timeout, this, &PictureLoaderWorker::processQueuedRequests);
requestTimer.setInterval(1000);
requestTimer.start();
}
PictureLoaderWorker::~PictureLoaderWorker()
{
saveRedirectCache();
pictureLoaderThread->deleteLater();
}
void PictureLoaderWorker::processLoadQueue()
void PictureLoaderWorker::queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
{
if (loadQueueRunning) {
return;
}
loadQueueRunning = true;
while (true) {
mutex.lock();
if (loadQueue.isEmpty()) {
mutex.unlock();
loadQueueRunning = false;
return;
}
cardBeingLoaded = loadQueue.takeFirst();
mutex.unlock();
QString setName = cardBeingLoaded.getSetName();
QString cardName = cardBeingLoaded.getCard()->getName();
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: Trying to load picture";
// FIXME: This is a hack so that to keep old Cockatrice behavior
// (ignoring provider ID) when the "override all card art with personal
// preference" is set.
//
// Figure out a proper way to integrate the two systems at some point.
//
// Note: need to go through a member for
// overrideAllCardArtWithPersonalPreference as reading from the
// SettingsCache instance from the PictureLoaderWorker thread could
// cause race conditions.
//
// XXX: Reading from the CardDatabaseManager instance from the
// PictureLoaderWorker thread might not be safe either
bool searchCustomPics = overrideAllCardArtWithPersonalPreference ||
CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey());
if (searchCustomPics && cardImageExistsOnDisk(setName, correctedCardName, searchCustomPics)) {
continue;
}
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: No custom picture, trying to download";
cardsToDownload.append(cardBeingLoaded);
cardBeingLoaded.clear();
if (!downloadRunning) {
startNextPicDownload();
}
}
}
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname, bool searchCustomPics)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
if (searchCustomPics) {
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() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
}
if (!setName.isEmpty()) {
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
// We no longer store downloaded images there, but don't just ignore
// stuff that old versions have put there.
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
}
// Iterates through the list of paths, searching for images with the desired
// name with any QImageReader-supported
// extension
for (const auto &_picsPath : picsPaths) {
imgReader.setFileName(_picsPath);
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".full");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.full found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".xlhq");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.xlhq found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
}
return false;
}
void PictureLoaderWorker::startNextPicDownload()
{
if (cardsToDownload.isEmpty()) {
cardBeingDownloaded.clear();
downloadRunning = false;
return;
}
downloadRunning = true;
cardBeingDownloaded = cardsToDownload.takeFirst();
QString picUrl = cardBeingDownloaded.getCurrentUrl();
if (picUrl.isEmpty()) {
downloadRunning = false;
picDownloadFailed();
} else {
QUrl url(picUrl);
qCDebug(PictureLoaderWorkerLog).nospace() << "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: Trying to fetch picture from url " << url.toDisplayString();
makeRequest(url);
}
}
void PictureLoaderWorker::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 (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
mutex.lock();
loadQueue.prepend(cardBeingDownloaded);
mutex.unlock();
} else {
qCWarning(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
imageLoaded(cardBeingDownloaded.getCard(), QImage());
cardBeingDownloaded.clear();
}
emit startLoadQueue();
}
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return md5Blacklist.contains(md5sum);
}
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
{
// Check if the redirect is cached
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString()
<< " to " << cachedRedirect.toDisplayString();
return makeRequest(cachedRedirect); // Use the cached redirect
queueRequest(cachedRedirect, worker);
}
if (cache->metaData(url).isValid()) {
makeRequest(url, worker);
} else {
requestLoadQueue.append(qMakePair(url, worker));
emit imageLoadQueued(url, worker);
}
}
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
{
// Check for cached redirects
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
emit imageLoadSuccessful(url, worker);
return makeRequest(cachedRedirect, worker);
}
QNetworkRequest req(url);
// QNetworkDiskCache leaks file descriptors when downloading compressed
// files, see https://bugreports.qt.io/browse/QTBUG-135641
//
// We can set the Accept-Encoding header manually to disable Qt's automatic
// response decompression, but then we would have to deal with decompression
// ourselves.
//
// Since we are dowloading images that are usually stored in a
// compressed format (e.g. jpeg or webp), it's not clear compression at the
// HTTP level helps; in fact, images are typically returned without
// compression. Redirects, on the other hand, are compressed and cause file
// descriptor leaks -- but since redirects have no payload, we don't really
// care either.
//
// In the end, just do the simple thing and disable HTTP compression.
req.setRawHeader("accept-encoding", "identity");
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}
QNetworkReply *reply = networkManager->get(req);
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
// Connect reply handling
connect(reply, &QNetworkReply::finished, this, [this, reply, url, worker]() {
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (redirectTarget.isValid()) {
QUrl redirectUrl = redirectTarget.toUrl();
if (redirectUrl.isRelative()) {
redirectUrl = url.resolved(redirectUrl);
}
cacheRedirect(url, redirectUrl);
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from " << url.toDisplayString()
<< " to " << redirectUrl.toDisplayString();
}
if (reply->error() == QNetworkReply::NoError) {
worker->picDownloadFinished(reply);
emit imageLoadSuccessful(url, worker);
// If we hit a cached image, we get to make another request for free.
if (reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) {
if (!requestLoadQueue.isEmpty()) {
auto request = requestLoadQueue.takeFirst();
makeRequest(request.first, request.second);
}
}
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
qInfo() << "Too many requests.";
}
reply->deleteLater();
});
return reply;
}
void PictureLoaderWorker::processQueuedRequests()
{
for (int i = 0; i < 10; i++) {
if (!requestLoadQueue.isEmpty()) {
auto request = requestLoadQueue.takeFirst();
makeRequest(request.first, request.second);
}
}
}
void PictureLoaderWorker::enqueueImageLoad(const CardInfoPtr &card)
{
auto worker = new PictureLoaderWorkerWork(this, card);
Q_UNUSED(worker);
}
void PictureLoaderWorker::imageLoadedSuccessfully(CardInfoPtr card, const QImage &image)
{
emit imageLoaded(std::move(card), image);
}
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
{
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
saveRedirectCache();
// saveRedirectCache();
}
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
@ -353,129 +211,6 @@ void PictureLoaderWorker::cleanStaleEntries()
}
}
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error()) {
if (isFromCache) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString() << " and retrying ("
<< reply->errorString() << ")";
networkManager->cache()->remove(reply->url());
makeRequest(reply->url());
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
startNextPicDownload();
}
reply->deleteLater();
return;
}
// 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(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: following " << (isFromCache ? "cached redirect" : "redirect") << " to "
<< redirectUrl.toDisplayString();
makeRequest(redirectUrl);
reply->deleteLater();
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(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
reply->deleteLater();
startNextPicDownload();
return;
}
QImage testImage;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
bool logSuccessMessage = false;
static const 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();
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
logSuccessMessage = true;
} else if (imgReader.read(&testImage)) {
imageLoaded(cardBeingDownloaded.getCard(), testImage);
logSuccessMessage = true;
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Possible " << (isFromCache ? "cached" : "downloaded") << " picture at "
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
picDownloadFailed();
}
if (logSuccessMessage) {
qCInfo(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
<< reply->url().toDisplayString();
}
reply->deleteLater();
startNextPicDownload();
}
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
{
QMutexLocker locker(&mutex);
// avoid queueing the same card more than once
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
return;
}
for (const PictureToLoad &pic : loadQueue) {
if (pic.getCard() == card)
return;
}
for (const PictureToLoad &pic : cardsToDownload) {
if (pic.getCard() == card)
return;
}
loadQueue.append(PictureToLoad(card));
emit startLoadQueue();
}
void PictureLoaderWorker::picDownloadChanged()
{
QMutexLocker locker(&mutex);

View file

@ -1,13 +1,18 @@
#ifndef PICTURE_LOADER_WORKER_H
#define PICTURE_LOADER_WORKER_H
#include "../../../game/cards/card_database.h"
#include "../../../game/cards/card_info.h"
#include "picture_loader_worker_work.h"
#include "picture_to_load.h"
#include <QLoggingCategory>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QObject>
#include <QQueue>
#include <QTimer>
#define REDIRECT_HEADER_NAME "redirects"
#define REDIRECT_ORIGINAL_URL "original"
@ -17,16 +22,23 @@
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
class PictureLoaderWorkerWork;
class PictureLoaderWorker : public QObject
{
Q_OBJECT
public:
explicit PictureLoaderWorker();
~PictureLoaderWorker() override;
void queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker);
void enqueueImageLoad(CardInfoPtr card);
void enqueueImageLoad(const CardInfoPtr &card);
void clearNetworkCache();
public slots:
QNetworkReply *makeRequest(const QUrl &url, PictureLoaderWorkerWork *workThread);
void processQueuedRequests();
void imageLoadedSuccessfully(CardInfoPtr card, const QImage &image);
private:
static QStringList md5Blacklist;
@ -35,6 +47,7 @@ private:
QList<PictureToLoad> loadQueue;
QMutex mutex;
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
@ -42,18 +55,11 @@ private:
PictureToLoad cardBeingLoaded;
PictureToLoad cardBeingDownloaded;
bool picDownload, downloadRunning, loadQueueRunning;
QQueue<QPair<QUrl, PictureLoaderWorkerWork *>> requestLoadQueue;
QList<QPair<QUrl, PictureLoaderWorkerWork *>> requestQueue;
QTimer requestTimer; // Timer for processing delayed requests
bool overrideAllCardArtWithPersonalPreference;
void startNextPicDownload();
/** Emit the `imageLoaded` signal and return `true` if a picture is found on
disk, return `false` otherwise.
If `searchCustomPics` is `true`, the CUSTOM folder is searched for a
matching image first; otherwise, only the set-based folders are used. */
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName, bool searchCustomPics);
bool imageIsBlackListed(const QByteArray &);
QNetworkReply *makeRequest(const QUrl &url);
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
QUrl getCachedRedirect(const QUrl &originalUrl) const;
void loadRedirectCache();
@ -61,18 +67,15 @@ private:
void cleanStaleEntries();
private slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
void picDownloadChanged();
void picsPathChanged();
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
public slots:
void processLoadQueue();
signals:
void startLoadQueue();
void imageLoaded(CardInfoPtr card, const QImage &image);
void imageLoadQueued(const QUrl &url, PictureLoaderWorkerWork *worker);
void imageLoadSuccessful(const QUrl &url, PictureLoaderWorkerWork *worker);
};
#endif // PICTURE_LOADER_WORKER_H

View file

@ -0,0 +1,236 @@
#include "picture_loader_worker_work.h"
#include "../../../game/cards/card_database_manager.h"
#include "../../../settings/cache_settings.h"
#include "picture_loader_worker.h"
#include <QBuffer>
#include <QDirIterator>
#include <QLoggingCategory>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QThread>
// Card back returned by gatherer when card is not found
QStringList PictureLoaderWorkerWork::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
PictureLoaderWorkerWork::PictureLoaderWorkerWork(PictureLoaderWorker *_worker, const CardInfoPtr &toLoad)
: QThread(nullptr), worker(_worker), cardToDownload(toLoad)
{
connect(this, &PictureLoaderWorkerWork::requestImageDownload, worker, &PictureLoaderWorker::queueRequest,
Qt::QueuedConnection);
connect(this, &PictureLoaderWorkerWork::imageLoaded, worker, &PictureLoaderWorker::imageLoadedSuccessfully,
Qt::QueuedConnection);
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
startNextPicDownload();
}
PictureLoaderWorkerWork::~PictureLoaderWorkerWork()
{
pictureLoaderThread->deleteLater();
}
bool PictureLoaderWorkerWork::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
QDirIterator it(SettingsCache::instance().getCustomPicsPath(),
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() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
if (!setName.isEmpty()) {
picsPaths << SettingsCache::instance().getPicsPath() + "/" + setName + "/" + correctedCardname
// We no longer store downloaded images there, but don't just ignore
// stuff that old versions have put there.
<< SettingsCache::instance().getPicsPath() + "/downloadedPics/" + setName + "/" + correctedCardname;
}
// Iterates through the list of paths, searching for images with the desired
// name with any QImageReader-supported
// extension
for (const auto &_picsPath : picsPaths) {
imgReader.setFileName(_picsPath);
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
imageLoaded(cardToDownload.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".full");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
<< " set: " << setName << "]: Picture.full found on disk.";
imageLoaded(cardToDownload.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".xlhq");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
<< " set: " << setName << "]: Picture.xlhq found on disk.";
imageLoaded(cardToDownload.getCard(), image);
return true;
}
}
return false;
}
void PictureLoaderWorkerWork::startNextPicDownload()
{
QString picUrl = cardToDownload.getCurrentUrl();
if (picUrl.isEmpty()) {
downloadRunning = false;
picDownloadFailed();
} else {
QUrl url(picUrl);
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Trying to fetch picture from url "
<< url.toDisplayString();
emit requestImageDownload(url, this);
}
}
void PictureLoaderWorkerWork::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 {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
imageLoaded(cardToDownload.getCard(), QImage());
}
emit startLoadQueue();
}
void PictureLoaderWorkerWork::picDownloadFinished(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error()) {
if (isFromCache) {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: Removing corrupted cache file for url "
<< reply->url().toDisplayString() << " and retrying (" << reply->errorString() << ")";
networkManager->cache()->remove(reply->url());
requestImageDownload(reply->url(), this);
} else {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: " << (picDownload ? "Download" : "Cache search")
<< " failed for url " << reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
startNextPicDownload();
}
reply->deleteLater();
return;
}
// 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(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: following "
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
requestImageDownload(redirectUrl, this);
reply->deleteLater();
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(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
reply->deleteLater();
startNextPicDownload();
return;
}
QImage testImage;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
bool logSuccessMessage = false;
static const 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();
imageLoaded(cardToDownload.getCard(), movie.currentImage());
logSuccessMessage = true;
} else if (imgReader.read(&testImage)) {
imageLoaded(cardToDownload.getCard(), testImage);
logSuccessMessage = true;
} else {
qCDebug(PictureLoaderWorkerWorkLog).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();
}
if (logSuccessMessage) {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: Image successfully "
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url " << reply->url().toDisplayString();
} else {
startNextPicDownload();
}
reply->deleteLater();
}
bool PictureLoaderWorkerWork::imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return md5Blacklist.contains(md5sum);
}

View file

@ -0,0 +1,50 @@
#ifndef PICTURE_LOADER_WORKER_WORK_H
#define PICTURE_LOADER_WORKER_WORK_H
#include "../../../game/cards/card_database.h"
#include "picture_loader_worker.h"
#include "picture_to_load.h"
#include <QLoggingCategory>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QObject>
#include <QThread>
#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(PictureLoaderWorkerWorkLog, "picture_loader.worker");
class PictureLoaderWorker;
class PictureLoaderWorkerWork : public QThread
{
Q_OBJECT
public:
explicit PictureLoaderWorkerWork(PictureLoaderWorker *worker, const CardInfoPtr &toLoad);
~PictureLoaderWorkerWork() override;
PictureLoaderWorker *worker;
PictureToLoad cardToDownload;
public slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
private:
static QStringList md5Blacklist;
QThread *pictureLoaderThread;
QNetworkAccessManager *networkManager;
bool picDownload, downloadRunning, loadQueueRunning;
void startNextPicDownload();
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
bool imageIsBlackListed(const QByteArray &);
signals:
void startLoadQueue();
void imageLoaded(CardInfoPtr card, const QImage &image);
void requestImageDownload(const QUrl &url, PictureLoaderWorkerWork *instance);
};
#endif // PICTURE_LOADER_WORKER_WORK_H

View file

@ -26,6 +26,11 @@ void SettingsButtonWidget::addSettingsWidget(QWidget *toAdd) const
popup->addSettingsWidget(toAdd);
}
void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const
{
popup->removeSettingsWidget(toRemove);
}
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
{
button->setIcon(iconMap);

View file

@ -13,6 +13,7 @@ class SettingsButtonWidget : public QWidget
public:
explicit SettingsButtonWidget(QWidget *parent = nullptr);
void addSettingsWidget(QWidget *toAdd) const;
void removeSettingsWidget(QWidget *toRemove) const;
void setButtonIcon(QPixmap iconMap);
protected:
@ -25,6 +26,8 @@ private slots:
private:
QHBoxLayout *layout;
QToolButton *button;
public:
SettingsPopupWidget *popup;
};

View file

@ -29,6 +29,12 @@ void SettingsPopupWidget::addSettingsWidget(QWidget *toAdd) const
containerLayout->addWidget(toAdd); // Add to containerWidget's layout
}
void SettingsPopupWidget::removeSettingsWidget(QWidget *toRemove) const
{
containerLayout->removeWidget(toRemove);
toRemove->deleteLater();
}
void SettingsPopupWidget::adjustSizeToFitScreen()
{
QScreen *screen = QApplication::screenAt(this->pos());

View file

@ -13,6 +13,7 @@ class SettingsPopupWidget : public QWidget
public:
explicit SettingsPopupWidget(QWidget *parent = nullptr);
void addSettingsWidget(QWidget *toAdd) const;
void removeSettingsWidget(QWidget *toRemove) const;
void adjustSizeToFitScreen();
signals:

View file

@ -21,8 +21,13 @@ set(oracle_SOURCES
../cockatrice/src/game/cards/card_database_manager.cpp
../cockatrice/src/game/cards/card_info.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_status_bar.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_worker.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_worker_work.cpp
../cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
../cockatrice/src/client/ui/widgets/quick_settings/settings_button_widget.cpp
../cockatrice/src/client/ui/widgets/quick_settings/settings_popup_widget.cpp
../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp