[PictureLoader] Store per-host limits readably and show them per URL

This commit is contained in:
Lukas Brübach 2026-09-18 04:35:16 +02:00
parent d0a5c5d1e0
commit 8a16b43a90
3 changed files with 137 additions and 22 deletions

View file

@ -10,6 +10,7 @@
#include <QInputDialog>
#include <QLineEdit>
#include <QMessageBox>
#include <QSet>
#include <QToolBar>
#include <QUrl>
#include <libcockatrice/settings/download_settings.h>
@ -17,8 +18,6 @@
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/macros.h>
static constexpr int UNLOCKED_HOST_LIMIT_MAX = 50; ///< Upper bound for rate limits on hosts unlocked by the developer
DeckEditorSettingsPage::DeckEditorSettingsPage()
{
picDownloadCheckBox.setChecked(SettingsCache::instance().downloads().getPicDownload());
@ -54,7 +53,9 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
urlList->setDragDropMode(QAbstractItemView::InternalMove);
connect(urlList->model(), &QAbstractItemModel::rowsMoved, this, &DeckEditorSettingsPage::urlListChanged);
urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
for (const QString &url : SettingsCache::instance().downloads().getAllURLs()) {
addUrlItem(url);
}
aAdd = new QAction(this);
aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
@ -125,7 +126,9 @@ void DeckEditorSettingsPage::resetDownloadedURLsButtonClicked()
{
SettingsCache::instance().downloads().resetToDefaultURLs();
urlList->clear();
urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
for (const QString &url : SettingsCache::instance().downloads().getAllURLs()) {
addUrlItem(url);
}
QMessageBox::information(this, tr("Success"), tr("Download URLs have been reset."));
}
@ -134,7 +137,7 @@ void DeckEditorSettingsPage::actAddURL()
bool ok;
QString msg = QInputDialog::getText(this, tr("Add URL"), tr("URL:"), QLineEdit::Normal, QString(), &ok);
if (ok) {
urlList->addItem(msg);
addUrlItem(msg);
storeSettings();
}
}
@ -149,12 +152,14 @@ void DeckEditorSettingsPage::actRemoveURL()
void DeckEditorSettingsPage::actEditURL()
{
if (urlList->currentItem()) {
QString oldText = urlList->currentItem()->text();
QListWidgetItem *item = urlList->currentItem();
if (item) {
const QString oldText = urlForItem(item);
bool ok;
QString msg = QInputDialog::getText(this, tr("Edit URL"), tr("URL:"), QLineEdit::Normal, oldText, &ok);
if (ok) {
urlList->currentItem()->setText(msg);
item->setData(Qt::UserRole, msg);
item->setText(urlLabel(msg));
storeSettings();
}
}
@ -166,10 +171,77 @@ void DeckEditorSettingsPage::storeSettings()
QStringList downloadUrls;
for (int i = 0; i < urlList->count(); i++) {
qInfo() << "Priority" << i << ":" << urlList->item(i)->text();
downloadUrls << urlList->item(i)->text();
const QString url = urlForItem(urlList->item(i));
qInfo() << "Priority" << i << ":" << url;
downloadUrls << url;
}
SettingsCache::instance().downloads().setDownloadUrls(downloadUrls);
// Drop per-host limits whose host is no longer referenced by any configured URL, so removing
// a URL doesn't leave a stale throttle behind that reactivates if the host is re-added.
QSet<QString> usedHosts;
for (const QString &url : downloadUrls) {
const QString host = QUrl(url).host();
if (!host.isEmpty()) {
usedHosts.insert(host);
}
}
QHash<QString, int> limits = SettingsCache::instance().downloads().getHostRequestLimits();
bool limitsChanged = false;
for (auto it = limits.begin(); it != limits.end();) {
if (!usedHosts.contains(it.key())) {
it = limits.erase(it);
limitsChanged = true;
} else {
++it;
}
}
if (limitsChanged) {
SettingsCache::instance().downloads().setHostRequestLimits(limits);
}
refreshUrlItems();
}
QListWidgetItem *DeckEditorSettingsPage::addUrlItem(const QString &url)
{
auto *item = new QListWidgetItem(urlLabel(url));
item->setData(Qt::UserRole, url);
urlList->addItem(item);
return item;
}
QString DeckEditorSettingsPage::urlForItem(const QListWidgetItem *item) const
{
return item->data(Qt::UserRole).toString();
}
QString DeckEditorSettingsPage::urlLabel(const QString &url) const
{
const QString host = QUrl(url).host();
if (host.isEmpty()) {
return url;
}
const QHash<QString, int> limits = SettingsCache::instance().downloads().getHostRequestLimits();
const int devCap =
DownloadSettings::getDeveloperHostCaps().value(host, DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT);
if (devCap == DownloadSettings::UNLIMITED_HOST_QUOTA && !limits.contains(host)) {
return tr("%1 (unlimited)").arg(url);
}
const int requested = limits.value(
host, devCap == DownloadSettings::UNLIMITED_HOST_QUOTA ? DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT : devCap);
const int effective = SettingsCache::instance().downloads().clampHostRequestLimit(host, requested);
return tr("%1 (%2/s)").arg(url).arg(effective);
}
void DeckEditorSettingsPage::refreshUrlItems()
{
for (int i = 0; i < urlList->count(); ++i) {
QListWidgetItem *item = urlList->item(i);
item->setText(urlLabel(urlForItem(item)));
}
}
void DeckEditorSettingsPage::actAdjustRateLimit()
@ -179,7 +251,7 @@ void DeckEditorSettingsPage::actAdjustRateLimit()
return;
}
const QString host = QUrl(urlList->currentItem()->text()).host();
const QString host = QUrl(urlForItem(urlList->currentItem())).host();
if (host.isEmpty()) {
QMessageBox::information(this, tr("Adjust Rate Limit"), tr("The selected URL does not have a valid host."));
return;
@ -194,19 +266,21 @@ void DeckEditorSettingsPage::actAdjustRateLimit()
int minimum;
int maximum;
int defaultValue;
QString prompt;
if (unlocked) {
minimum = 0; // 0 means "unlimited"
maximum = UNLOCKED_HOST_LIMIT_MAX;
maximum = DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT;
defaultValue = currentLimits.value(host, 0);
prompt = tr("Requests per second (0 = unlimited, fastest; up to %1):").arg(maximum);
} else {
minimum = DownloadSettings::MIN_HOST_REQUEST_LIMIT;
maximum = devCap;
defaultValue = currentLimits.value(host, devCap);
prompt = tr("Requests per second (developer maximum is %1):").arg(maximum);
}
const int value = QInputDialog::getInt(this, tr("Adjust Rate Limit for %1").arg(host),
tr("Requests per second (developer maximum is %1):").arg(maximum),
defaultValue, minimum, maximum, 1, &ok);
const int value = QInputDialog::getInt(this, tr("Adjust Rate Limit for %1").arg(host), prompt, defaultValue,
minimum, maximum, 1, &ok);
if (!ok) {
return;
}
@ -218,6 +292,7 @@ void DeckEditorSettingsPage::actAdjustRateLimit()
limits.insert(host, value);
}
SettingsCache::instance().downloads().setHostRequestLimits(limits);
refreshUrlItems();
}
void DeckEditorSettingsPage::urlListChanged(const QModelIndex &, int, int, const QModelIndex &, int)
@ -301,4 +376,7 @@ void DeckEditorSettingsPage::retranslateUi()
aEdit->setText(tr("Edit URL"));
aRemove->setText(tr("Remove URL"));
aRateLimit->setText(tr("Adjust Rate Limit"));
// The per-URL rate limit suffixes are translated, so refresh them when the language changes.
refreshUrlItems();
}

View file

@ -47,6 +47,18 @@ private:
QLabel infoOnSpoilersLabel;
QPushButton *mpSpoilerPathButton;
QPushButton *updateNowButton;
/** @brief Adds a list item for the given URL, storing the raw URL alongside its displayed label. */
QListWidgetItem *addUrlItem(const QString &url);
/** @brief Returns the raw URL stored on a list item. */
[[nodiscard]] QString urlForItem(const QListWidgetItem *item) const;
/** @brief Returns the display label for a URL, including its current effective rate limit. */
[[nodiscard]] QString urlLabel(const QString &url) const;
/** @brief Refreshes the displayed label of every URL item after limits or settings change. */
void refreshUrlItems();
};
#endif // COCKATRICE_DECK_EDITOR_SETTINGS_PAGE_H

View file

@ -84,21 +84,46 @@ void DownloadSettings::setDownloadSpoilerStatus(bool _spoilerStatus)
QHash<QString, int> DownloadSettings::getHostRequestLimits() const
{
const QVariantMap stored = getValue("hostRequestLimits").toMap();
auto settings = getSettings();
if (!defaultGroup.isEmpty()) {
settings.beginGroup(defaultGroup);
}
settings.beginGroup("hostRequestLimits");
QHash<QString, int> hostRequestLimits;
for (auto it = stored.cbegin(); it != stored.cend(); ++it) {
hostRequestLimits.insert(it.key(), it.value().toInt());
const QStringList hosts = settings.childKeys();
for (const QString &host : hosts) {
hostRequestLimits.insert(host, settings.value(host).toInt());
}
settings.endGroup();
if (!defaultGroup.isEmpty()) {
settings.endGroup();
}
return hostRequestLimits;
}
void DownloadSettings::setHostRequestLimits(const QHash<QString, int> &hostRequestLimits)
{
QVariantMap stored;
for (auto it = hostRequestLimits.cbegin(); it != hostRequestLimits.cend(); ++it) {
stored.insert(it.key(), it.value());
auto settings = getSettings();
if (!defaultGroup.isEmpty()) {
settings.beginGroup(defaultGroup);
}
setValue(stored, "hostRequestLimits");
// Drop the legacy single-key form (an opaque @Variant blob) written by earlier builds so each
// host is stored as a plain, hand-editable key in its own subgroup.
settings.remove("hostRequestLimits");
settings.beginGroup("hostRequestLimits");
settings.remove(QString());
for (auto it = hostRequestLimits.cbegin(); it != hostRequestLimits.cend(); ++it) {
settings.setValue(it.key(), it.value());
}
settings.endGroup();
if (!defaultGroup.isEmpty()) {
settings.endGroup();
}
settings.sync();
emit hostRequestLimitsChanged();
}