mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
* Squashed Commits Lint things. Set focus back to deckView after selecting a card to enable keyboard navigation. Bump scrollbar to top when selecting a new card. Update card info on hover. Layout cleanups Add +- to buttons. Merge buttons into card picture. Cleanup size, min 2 cards by default in rows Support layout settings config and set min to 525 so two cols are visible by default for printings, when opened Move Printing Selector to before Deck, and visible true Null safety for setCard. Turn down the dropshadow a little. Make PrintingSelector dockable, don't duplicate sets when bumping them to the front of the list. When swapping cards between mainboard and sideboard, use preferred printing if no uuid is available (i.e. null). Reorder includes... Unwonk an include. Give the card widget a snazzy drop shadow, appease the linter gods. Handle jumping between segments Remember scale factor when initializing new widgets. Cleanup Select Card works (Not M->SB tho) Resize word-wrapped label properly. Fix the layouting, mostly. remove tx Build Fix Squashed Commits Load and store redirects properly. Layouting is fun :) * Group PrintingSelectorCardDisplayWidgets into distinct containers for alignment purposes. Override resizeEvent() properly. Word wrap properly. Keep widget sizes uniform for aesthetic reasons (grid pattern). Label stuff, center card picture widget, allow cardSizeSlider to scale down. Replace cards which have no uuid in the decklist when first selecting a printing. Add buttons for previous and next card in DeckList. Add a card size slider. Move sort options initialization to implementation file. Explicitly nullptr the parent for the PrintingSelector. Address PR comments (minor cleanups). Hook up to the rows removed signal to update card counts properly. Include QDebug. Add labels to the mainboard/sideboard button boxes. Implement a search bar. Expand node recursively when we add a new card. Only create image widgets for the printing selector if it is visible in order to not slow down image loading. Minor Tweaks Invert decklist export logic to write out setName, collectorNumber, providerId value if it is NOT empty. Linting. Update CardCounts properly, update PrintingSelector on Database selection. Initialize sideboard card count label properly. Split mainboard/sideboard display and increment/decrement buttons. Add button to sort all sortOptions by ascending or descending order. Add option to sort by release date in ascending or descending order. Add PrintingSelector to database view. Display placeholder image before loading. Fix deckEditor crash on mainboard/sideboard swap by correcting column index to providerId instead of shortName. Include currentZoneName, fix the column when updating from DeckView indexChanged to be UUID and not setShortName so cards are properly fetched again. The most minor linter change you've ever seen. Null checks are important. Linter again. Linter and refactor to providerId. Sort properly, (We don't need a map, we need a list, a map won't be ordered right [i.e. 1, 10, 11, 2, 3, 4, ..., 9]) Sort alphabetically or by preference. Hook printingSelector up to the CardInfoFrameWidget. Allow info from CardFrame to be retrieved, properly initialize PrintingSelector again. Refactors to reflect CardInfoPicture becoming CardInfoPictureWidget. Make PrintingSelector re-usable by introducing setCard(). Make PrintingSelector use the CardFrame, not the database index. Add a new selector widget for printings. * Support multiple <set> tags per card within the database This will allow us to show off all different printings for cards that might appear multiple times in a set (alt arts, Secret Lairs, etc.) * Support Flip Cards with related art * Minor Cleanup * Minor Cleanup * Release Date DESC default * Load widgets in batches. * Refactor local batch variables to be class variables/defines. * Clear timer on updateDisplay. * Fix Timer & Builds on Qt5 * Not Override * Yes Override * Yes Override * Lint * Can't override in function definition. * Resize setName to picture width on initialization. Also add a new signal to card_info_picture_widget to emit when the scale factor changes. Hook this up to the setName resizing method to ensure card size updates trigger it appropriately after initialization. Clean up unused enter and resize methods that just delegated to base-class. * Add ability to force preferred set art to be loaded for every card. * Show related cards from printing selector by right-clicking card image. * fix build * Fix UST cards * Inc QDebug * Fix Qt5 Builds * Fix Qt5 Builds * Fix Qt5 Builds * Fix Qt5 Builds * Fix Qt5 Builds * Fix cards being able to jump between side and mainboard * Don't hide PrintingSelector button widgets if the deck contains a card from the set. * Update PrintingSelector properly on DeckListModel::dataChanged * Add option to disable bumping sets to the front of the list if the deck contains cards from the set. * Linter behave. * Linter behave. * Fix mocks. * Fix cards without providerIds being counted for all cards. * Flip preference sort so descending means "Most to least preferred". * Set the index correctly when removing a non-providerId printing for a providerId printing to avoid jumping to the next card. * Move the "Next/Previous" card buttons to their own widget. * Move the card size slider to its own widget. * Lint the makelist. * Linter * Crash fix * Move the sorting options to their own widget. * Move the search bar to its own widget. * Minor cleanup * Minor cleanup * Minor cleanup * Only overwrite card in deck if UUID _and_ Number missing * Adjust font size when adjusting card size. * Clean up some imports. * Pivot to a view options toolbar. * Persist sort options and change default to 'preference'. * Lint. * Remember how many cards were originally in deck when replacing with uuid version. * Relabel buttons for clarity. * Fix tests. * Fix tests properly. * Fix dbconverter mock. * Try to wrangle font sizes. * Update mainboard/sideboard labels correctly. * Initialize button sizes correctly. * Label texts are supposed to be white. * Adjust another deckModel->findCard call to include number parameter. * Style buttons again. * Negative currentSize means we don't render the widget yet, return a default value. * Clean up debug statements. * Boop the mainboard/sideboard label and the cardCount after a little bit of delay to make sure they initialize at the right size. * Persist card size slider selection in SettingsCache. * Good Lint Inc. * updateCardCount to get white color in initializer * Make the view display options functional. * Comment ALL the things. * Lint the things. * Brief accidentally nuked some constants. * Proper Qt slot for checkboxes. * Don't use timers, Qt provides ShowEvent for anything necessary before the widget is shown. * Cleanup from Reading * Cleanup Lints * Minor --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de> Co-authored-by: Zach Halpern <zahalpern+github@gmail.com>
835 lines
31 KiB
C++
835 lines
31 KiB
C++
#include "picture_loader.h"
|
|
|
|
#include "../../game/cards/card_database_manager.h"
|
|
#include "../../settings/cache_settings.h"
|
|
|
|
#include <QApplication>
|
|
#include <QBuffer>
|
|
#include <QCryptographicHash>
|
|
#include <QDebug>
|
|
#include <QDirIterator>
|
|
#include <QFileInfo>
|
|
#include <QImageReader>
|
|
#include <QMovie>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkDiskCache>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QPainter>
|
|
#include <QPixmapCache>
|
|
#include <QRegularExpression>
|
|
#include <QScreen>
|
|
#include <QSet>
|
|
#include <QThread>
|
|
#include <QUrl>
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
// never cache more than 300 cards at once for a single deck
|
|
#define CACHED_CARD_PER_DECK_MAX 300
|
|
|
|
PictureToLoad::PictureToLoad(CardInfoPtr _card)
|
|
: card(std::move(_card)), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
|
{
|
|
if (card) {
|
|
for (const auto &cardInfoPerSetList : card->getSets()) {
|
|
for (const auto &set : cardInfoPerSetList) {
|
|
sortedSets << set.getPtr();
|
|
}
|
|
}
|
|
if (sortedSets.empty()) {
|
|
sortedSets << CardSet::newInstance("", "", "", QDate());
|
|
}
|
|
std::sort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator());
|
|
|
|
// 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.
|
|
for (const auto &cardInfoPerSetList : card->getSets()) {
|
|
for (const auto &set : cardInfoPerSetList) {
|
|
if (QLatin1String("card_") + card->getName() + QString("_") + QString(set.getProperty("uuid")) ==
|
|
card->getPixmapCacheKey()) {
|
|
long long setIndex = sortedSets.indexOf(set.getPtr());
|
|
CardSetPtr setForCardProviderID = sortedSets.takeAt(setIndex);
|
|
sortedSets.prepend(setForCardProviderID);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// The first time called, nextSet will also populate the Urls for the first set.
|
|
nextSet();
|
|
}
|
|
}
|
|
|
|
void PictureToLoad::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 = card->getCustomPicURL(currentSet->getShortName());
|
|
|
|
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();
|
|
}
|
|
|
|
bool PictureToLoad::nextSet()
|
|
{
|
|
if (!sortedSets.isEmpty()) {
|
|
currentSet = sortedSets.takeFirst();
|
|
populateSetUrls();
|
|
return true;
|
|
}
|
|
currentSet = {};
|
|
return false;
|
|
}
|
|
|
|
bool PictureToLoad::nextUrl()
|
|
{
|
|
if (!currentSetUrls.isEmpty()) {
|
|
currentUrl = currentSetUrls.takeFirst();
|
|
return true;
|
|
}
|
|
currentUrl = QString();
|
|
return false;
|
|
}
|
|
|
|
QString PictureToLoad::getSetName() const
|
|
{
|
|
if (currentSet) {
|
|
return currentSet->getCorrectedShortName();
|
|
} else {
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
// Card back returned by gatherer when card is not found
|
|
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
|
|
|
PictureLoaderWorker::PictureLoaderWorker()
|
|
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
|
|
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
|
|
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
|
|
{
|
|
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
|
|
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
|
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
|
|
|
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
|
|
auto 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)); });
|
|
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, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *)));
|
|
|
|
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
|
loadRedirectCache();
|
|
cleanStaleEntries();
|
|
|
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
|
|
&PictureLoaderWorker::saveRedirectCache);
|
|
|
|
pictureLoaderThread = new QThread;
|
|
pictureLoaderThread->start(QThread::LowPriority);
|
|
moveToThread(pictureLoaderThread);
|
|
}
|
|
|
|
PictureLoaderWorker::~PictureLoaderWorker()
|
|
{
|
|
pictureLoaderThread->deleteLater();
|
|
}
|
|
|
|
void PictureLoaderWorker::processLoadQueue()
|
|
{
|
|
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();
|
|
|
|
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
|
<< "]: Trying to load picture";
|
|
|
|
if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
|
|
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
|
|
if (cardImageExistsOnDisk(setName, correctedCardName)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
qDebug().nospace() << "PictureLoader: [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)
|
|
{
|
|
QImage image;
|
|
QImageReader imgReader;
|
|
imgReader.setDecideFormatFromContent(true);
|
|
QList<QString> picsPaths = QList<QString>();
|
|
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)) {
|
|
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
|
<< "]: Picture found on disk.";
|
|
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
return true;
|
|
}
|
|
imgReader.setFileName(_picsPath + ".full");
|
|
if (imgReader.read(&image)) {
|
|
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
|
<< "]: Picture.full found on disk.";
|
|
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
return true;
|
|
}
|
|
imgReader.setFileName(_picsPath + ".xlhq");
|
|
if (imgReader.read(&image)) {
|
|
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
|
<< "]: Picture.xlhq found on disk.";
|
|
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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()) {
|
|
qDebug().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) {
|
|
qDebug().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) {
|
|
qDebug().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 PictureToLoad::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->getCorrectedName();
|
|
transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower();
|
|
|
|
// card properties
|
|
if (parse(
|
|
urlTemplate, "prop", cardName, setName, [&](const QString &name) { return card->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 card->getSetProperty(set->getShortName(), 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.
|
|
*/
|
|
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
|
<< "]: Requested information (" << prop << ") for Url template (" << urlTemplate
|
|
<< ") is not available";
|
|
return QString();
|
|
}
|
|
}
|
|
}
|
|
|
|
return transformedUrl;
|
|
}
|
|
|
|
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);
|
|
qDebug().nospace() << "PictureLoader: [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 {
|
|
qDebug().nospace() << "PictureLoader: [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()) {
|
|
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
|
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for "
|
|
<< url.toDisplayString() << " to " << cachedRedirect.toDisplayString();
|
|
return makeRequest(cachedRedirect); // Use the cached redirect
|
|
}
|
|
|
|
QNetworkRequest req(url);
|
|
|
|
if (!picDownload) {
|
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
|
}
|
|
|
|
QNetworkReply *reply = networkManager->get(req);
|
|
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
|
|
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
|
|
|
if (redirectTarget.isValid()) {
|
|
QUrl redirectUrl = redirectTarget.toUrl();
|
|
if (redirectUrl.isRelative()) {
|
|
redirectUrl = url.resolved(redirectUrl);
|
|
}
|
|
|
|
cacheRedirect(url, redirectUrl);
|
|
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
|
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from "
|
|
<< url.toDisplayString() << " to " << redirectUrl.toDisplayString();
|
|
}
|
|
|
|
reply->deleteLater();
|
|
});
|
|
|
|
return reply;
|
|
}
|
|
|
|
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
|
{
|
|
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
|
saveRedirectCache();
|
|
}
|
|
|
|
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
|
{
|
|
if (redirectCache.contains(originalUrl)) {
|
|
return redirectCache[originalUrl].first;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void PictureLoaderWorker::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 PictureLoaderWorker::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 PictureLoaderWorker::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 PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
|
{
|
|
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
|
|
|
if (reply->error()) {
|
|
if (isFromCache) {
|
|
qDebug().nospace() << "PictureLoader: [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 {
|
|
qDebug().nospace() << "PictureLoader: [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();
|
|
qDebug().nospace() << "PictureLoader: [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)) {
|
|
qDebug().nospace() << "PictureLoader: [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 {
|
|
qDebug().nospace() << "PictureLoader: [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) {
|
|
qDebug().nospace() << "PictureLoader: [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);
|
|
picDownload = SettingsCache::instance().getPicDownload();
|
|
}
|
|
|
|
void PictureLoaderWorker::picsPathChanged()
|
|
{
|
|
QMutexLocker locker(&mutex);
|
|
picsPath = SettingsCache::instance().getPicsPath();
|
|
customPicsPath = SettingsCache::instance().getCustomPicsPath();
|
|
}
|
|
|
|
void PictureLoaderWorker::clearNetworkCache()
|
|
{
|
|
networkManager->cache()->clear();
|
|
}
|
|
|
|
PictureLoader::PictureLoader() : QObject(nullptr)
|
|
{
|
|
worker = new PictureLoaderWorker;
|
|
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
|
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
|
|
|
connect(worker, SIGNAL(imageLoaded(CardInfoPtr, const QImage &)), this,
|
|
SLOT(imageLoaded(CardInfoPtr, const QImage &)));
|
|
}
|
|
|
|
PictureLoader::~PictureLoader()
|
|
{
|
|
worker->deleteLater();
|
|
}
|
|
|
|
void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
|
|
{
|
|
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
|
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
|
|
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
QPixmapCache::insert(backCacheKey, pixmap);
|
|
}
|
|
}
|
|
|
|
void PictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
|
|
{
|
|
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
|
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
|
|
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
QPixmapCache::insert(backCacheKey, pixmap);
|
|
}
|
|
}
|
|
|
|
void PictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
|
|
{
|
|
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
|
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
|
|
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
QPixmapCache::insert(backCacheKey, pixmap);
|
|
}
|
|
}
|
|
|
|
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
|
|
{
|
|
if (card == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// search for an exact size copy of the picture in cache
|
|
QString key = card->getPixmapCacheKey();
|
|
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
|
|
if (QPixmapCache::find(sizeKey, &pixmap))
|
|
return;
|
|
|
|
// load the image and create a copy of the correct size
|
|
QPixmap bigPixmap;
|
|
if (QPixmapCache::find(key, &bigPixmap)) {
|
|
QScreen *screen = qApp->primaryScreen();
|
|
qreal dpr = screen->devicePixelRatio();
|
|
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
pixmap.setDevicePixelRatio(dpr);
|
|
QPixmapCache::insert(sizeKey, pixmap);
|
|
return;
|
|
}
|
|
|
|
// add the card to the load queue
|
|
getInstance().worker->enqueueImageLoad(card);
|
|
}
|
|
|
|
void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image)
|
|
{
|
|
if (image.isNull()) {
|
|
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap());
|
|
} else {
|
|
if (card->getUpsideDownArt()) {
|
|
QImage mirrorImage = image.mirrored(true, true);
|
|
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
|
|
} else {
|
|
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(image));
|
|
}
|
|
}
|
|
|
|
card->emitPixmapUpdated();
|
|
}
|
|
|
|
void PictureLoader::clearPixmapCache(CardInfoPtr card)
|
|
{
|
|
if (card) {
|
|
QPixmapCache::remove(card->getPixmapCacheKey());
|
|
}
|
|
}
|
|
|
|
void PictureLoader::clearPixmapCache()
|
|
{
|
|
QPixmapCache::clear();
|
|
}
|
|
|
|
void PictureLoader::clearNetworkCache()
|
|
{
|
|
getInstance().worker->clearNetworkCache();
|
|
}
|
|
|
|
void PictureLoader::cacheCardPixmaps(QList<CardInfoPtr> cards)
|
|
{
|
|
QPixmap tmp;
|
|
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
|
|
for (int i = 0; i < max; ++i) {
|
|
const CardInfoPtr &card = cards.at(i);
|
|
if (!card) {
|
|
continue;
|
|
}
|
|
|
|
QString key = card->getPixmapCacheKey();
|
|
if (QPixmapCache::find(key, &tmp)) {
|
|
continue;
|
|
}
|
|
|
|
getInstance().worker->enqueueImageLoad(card);
|
|
}
|
|
}
|
|
|
|
void PictureLoader::picDownloadChanged()
|
|
{
|
|
QPixmapCache::clear();
|
|
}
|
|
|
|
void PictureLoader::picsPathChanged()
|
|
{
|
|
QPixmapCache::clear();
|
|
}
|