[PictureLoader] Allow saving downloaded images to local storage and not just the QNetworkManager cache (#6620)

* [PictureLoader] Allow saving downloaded images to local storage and not just the QNetworkManager cache.

Took 1 hour 11 minutes

Took 4 seconds


Took 25 seconds

* Give people options from a dropdown.

Took 1 hour 6 minutes

Took 3 seconds

* Simplify directory removal code.

Took 5 minutes

Took 8 seconds

* Merge pull request #8

* Create new category for new settings

* Split off storage settings

Took 47 minutes

Took 4 seconds

* Allow toggling between caching methods.

Took 1 hour 30 minutes

Took 9 seconds

* Adjust settings dialog.

Took 5 minutes

Took 59 seconds

Took 22 seconds

Took 6 seconds

* tr() strings

Took 1 minute

Took 6 seconds

* Readjust layout, default naming scheme.

Took 5 minutes

* Add stretch.

Took 9 minutes

* Make scrollable.

Took 2 minutes

* Add icon.

Took 7 minutes

* Change naming to be uniform.

Took 3 minutes

Took 3 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
This commit is contained in:
BruebachL 2026-05-16 20:15:10 +02:00 committed by GitHub
parent 20cd7ce73d
commit f8ce5c2e39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1382 additions and 132 deletions

View file

@ -150,9 +150,10 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
{
QPixmap finalPixmap;
if (image.isNull()) {
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap());
} else {
if (card.getInfo().getUiAttributes().upsideDownArt) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
@ -160,12 +161,19 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
#else
QImage mirrorImage = image.mirrored(true, true);
#endif
QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
finalPixmap = QPixmap::fromImage(mirrorImage);
} else {
QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap::fromImage(image));
finalPixmap = QPixmap::fromImage(image);
}
}
QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap);
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) {
saveCardImageToLocalStorage(card, finalPixmap);
}
// 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.
@ -175,6 +183,88 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
card.emitPixmapUpdated();
}
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
{
if (pixmap.isNull() || !card) {
return;
}
const QString picsRoot = SettingsCache::instance().getPicsPath();
CardPictureLoaderLocalSchemes::NamingScheme scheme =
SettingsCache::instance().getLocalCardImageStorageNamingScheme();
QString pattern;
for (const auto &s : CardPictureLoaderLocalSchemes::exportSchemes()) {
if (s.id == scheme) {
pattern = s.pattern;
break;
}
}
if (picsRoot.isEmpty() || pattern.isEmpty()) {
return;
}
// Base directory: <picsPath>/downloadedPics
QDir baseDir(picsRoot);
if (!baseDir.exists("downloadedPics")) {
baseDir.mkpath("downloadedPics");
}
baseDir.cd("downloadedPics");
// Collect card metadata
const QString cardName = card.getInfo().getCorrectedName();
QString setName;
QString collectorNumber;
QString uuid;
PrintingInfo printing = card.getPrinting();
if (printing.getSet()) {
setName = printing.getSet()->getCorrectedShortName();
collectorNumber = printing.getProperty("num");
uuid = printing.getUuid();
}
// Build path from scheme
QString relativePath =
CardPictureLoaderLocalSchemes::expandPattern(pattern, cardName, setName, collectorNumber, uuid);
if (relativePath.isEmpty()) {
return;
}
// append extension
relativePath += ".png";
// Normalize slashes
relativePath = QDir::cleanPath(relativePath);
QFileInfo outInfo(baseDir.filePath(relativePath));
// Do not overwrite existing files
if (outInfo.exists()) {
return;
}
QDir outDir = outInfo.dir();
// Ensure directory exists
if (!outDir.exists()) {
if (!baseDir.mkpath(outDir.path())) {
qCWarning(CardPictureLoaderLog) << "Failed to create directory for downloaded card image:" << outDir.path();
return;
}
}
// Save image
QImage image = pixmap.toImage();
if (!image.save(outInfo.absoluteFilePath(), "PNG")) {
qCWarning(CardPictureLoaderLog) << "Failed to save card image to" << outInfo.absoluteFilePath();
}
}
void CardPictureLoader::clearPixmapCache()
{
QPixmapCache::clear();

View file

@ -117,6 +117,7 @@ public slots:
* @param image Loaded QImage.
*/
void imageLoaded(const ExactCard &card, const QImage &image);
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap);
private slots:
/**

View file

@ -0,0 +1,31 @@
#ifndef COCKATRICE_CARD_PICTURE_LOADER_CACHE_METHOD_H
#define COCKATRICE_CARD_PICTURE_LOADER_CACHE_METHOD_H
#include <QCoreApplication>
#include <QList>
#include <QString>
namespace CardPictureLoaderCacheMethod
{
enum class CacheMethod
{
NETWORK_CACHE,
FILESYSTEM_CACHE
};
struct CacheMethodInfo
{
CacheMethod id;
QString displayName;
};
static inline const QList<CacheMethodInfo> methods()
{
static QList<CacheMethodInfo> all = {
{CacheMethod::NETWORK_CACHE, QCoreApplication::translate("CardPictureLoaderCacheMethod", "Network Cache")},
{CacheMethod::FILESYSTEM_CACHE, QCoreApplication::translate("CardPictureLoaderCacheMethod", "Filesystem")},
};
return all;
}
} // namespace CardPictureLoaderCacheMethod
#endif // COCKATRICE_CARD_PICTURE_LOADER_CACHE_METHOD_H

View file

@ -1,6 +1,7 @@
#include "card_picture_loader_local.h"
#include "../../client/settings/cache_settings.h"
#include "card_picture_loader_local_schemes.h"
#include "card_picture_to_load.h"
#include <QDirIterator>
@ -77,26 +78,8 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
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;
QStringList nameVariants =
CardPictureLoaderLocalSchemes::generateImportVariants(correctedCardName, setName, collectorNumber, providerId);
for (const QString &nameVariant : nameVariants) {
if (nameVariant.isEmpty()) {

View file

@ -0,0 +1,109 @@
#ifndef COCKATRICE_CARD_PICTURE_LOADER_LOCAL_SCHEMES_H
#define COCKATRICE_CARD_PICTURE_LOADER_LOCAL_SCHEMES_H
#include <QList>
#include <QRegularExpression>
#include <QString>
#include <QStringList>
namespace CardPictureLoaderLocalSchemes
{
enum class NamingScheme
{
NameOnly,
Name_Set,
Name_Set_Collector,
Set_Collector_Name,
Name_ProviderId,
Set_Folder_Name_ProviderId,
Set_Folder_Name_Set_Collector
};
struct NamingSchemeInfo
{
NamingScheme id;
QString displayName;
QString pattern;
};
inline const QList<NamingSchemeInfo> &importSchemes()
{
static QList<NamingSchemeInfo> list = {
{NamingScheme::Name_ProviderId, "Card Name + Provider ID", "{name}_{providerId}"},
{NamingScheme::Name_Set_Collector, "Card Name + Set + Collector", "{name}_{set}_{collector}"},
{NamingScheme::Set_Collector_Name, "Set + Collector + Card Name", "{set}_{collector}_{name}"},
{NamingScheme::Name_Set, "Card Name + Set", "{name}_{set}"},
{NamingScheme::NameOnly, "Card Name", "{name}"},
};
return list;
}
inline const QList<NamingSchemeInfo> &exportSchemes()
{
static QList<NamingSchemeInfo> list = {
{NamingScheme::Set_Folder_Name_ProviderId, "Set Folder / Name + Provider ID", "{set}/{name}_{providerId}"},
{NamingScheme::Set_Folder_Name_Set_Collector, "Set Folder / Name + Set Name + Collector",
"{set}/{name}_{set}_{collector}"},
{NamingScheme::Name_ProviderId, "Card Name + Provider ID", "{name}_{providerId}"},
{NamingScheme::Name_Set_Collector, "Card Name + Set + Collector", "{name}_{set}_{collector}"},
{NamingScheme::Set_Collector_Name, "Set + Collector + Card Name", "{set}_{collector}_{name}"},
};
return list;
}
inline QString expandPattern(const QString &pattern,
const QString &name,
const QString &set,
const QString &collector,
const QString &providerId)
{
QString result = pattern;
auto replaceIfPresent = [&](const QString &token, const QString &value) -> bool {
if (!result.contains(token))
return true;
if (value.isEmpty())
return false;
result.replace(token, value);
return true;
};
if (!replaceIfPresent("{name}", name))
return {};
if (!replaceIfPresent("{set}", set))
return {};
if (!replaceIfPresent("{collector}", collector))
return {};
if (!replaceIfPresent("{providerId}", providerId))
return {};
return result;
}
inline QStringList
generateImportVariants(const QString &name, const QString &set, const QString &collector, const QString &providerId)
{
QStringList variants;
const QStringList separators = {"_", "-"};
for (const auto &scheme : importSchemes()) {
for (const QString &sep : separators) {
QString pattern = scheme.pattern;
pattern.replace("_", sep);
QString v = expandPattern(pattern, name, set, collector, providerId);
if (!v.isEmpty())
variants << v;
}
}
return variants;
}
} // namespace CardPictureLoaderLocalSchemes
#endif // COCKATRICE_CARD_PICTURE_LOADER_LOCAL_SCHEMES_H

View file

@ -26,10 +26,14 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
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)); });
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache, [this](int newSizeInMB) {
if (cache)
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);
@ -65,14 +69,19 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
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();
return;
}
if (SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
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);
return;
}
requestLoadQueue.append(qMakePair(url, worker));
emit imageRequestQueued(url, worker->cardToDownload.getCard(), worker->cardToDownload.getSetName());
processQueuedRequests();
}
QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker)
@ -87,9 +96,12 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}
bool useNetworkCache = !picDownload && SettingsCache::instance().getCardPictureLoaderCacheMethod() ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
QNetworkReply *reply = networkManager->get(req);