// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors #include "steamgriddb.h" #include "app.h" #include "game.h" #include #include #include #include #include #include #include #include #include #include static const QString STEAMGRIDDB_API_BASE = QStringLiteral("https://www.steamgriddb.com/api/v2"); SteamGridDB::SteamGridDB(QObject *parent) : QObject(parent) , m_networkManager(new QNetworkAccessManager(this)) { loadSettings(); } QString SteamGridDB::apiKey() const { return m_apiKey; } void SteamGridDB::setApiKey(const QString &key) { if (m_apiKey != key) { m_apiKey = key; saveSettings(); Q_EMIT apiKeyChanged(); } } bool SteamGridDB::enabled() const { return m_enabled; } void SteamGridDB::setEnabled(bool enabled) { if (m_enabled != enabled) { m_enabled = enabled; saveSettings(); Q_EMIT enabledChanged(); } } bool SteamGridDB::preferSteamGridDB() const { return m_preferSteamGridDB; } void SteamGridDB::setPreferSteamGridDB(bool prefer) { if (m_preferSteamGridDB != prefer) { m_preferSteamGridDB = prefer; saveSettings(); Q_EMIT preferSteamGridDBChanged(); } } bool SteamGridDB::busy() const { return m_busy; } void SteamGridDB::fetchCover(Game *game) { if (!game || m_apiKey.isEmpty() || !m_enabled) { return; } // Skip if game already has a cover and we don't prefer SteamGridDB if (game->coverUrl().isValid() && !m_preferSteamGridDB) { return; } m_pendingGames.append(game); if (!m_busy) { m_totalGames = m_pendingGames.count(); m_processedGames = 0; processNextGame(); } } void SteamGridDB::fetchAllCovers() { if (m_apiKey.isEmpty() || !m_enabled) { return; } GameModel *gameModel = App::instance()->gameModel(); for (int i = 0; i < gameModel->rowCount(); ++i) { Game *game = gameModel->gameAt(i); if (game && (!game->coverUrl().isValid() || m_preferSteamGridDB)) { m_pendingGames.append(game); } } if (!m_pendingGames.isEmpty() && !m_busy) { m_totalGames = m_pendingGames.count(); m_processedGames = 0; processNextGame(); } } void SteamGridDB::processNextGame() { if (m_pendingGames.isEmpty()) { m_busy = false; m_currentGame = nullptr; Q_EMIT busyChanged(); return; } m_busy = true; Q_EMIT busyChanged(); m_currentGame = m_pendingGames.takeFirst(); searchGame(m_currentGame); } void SteamGridDB::searchGame(Game *game) { QString encodedName = QString::fromUtf8(QUrl::toPercentEncoding(game->name())); QUrl url(STEAMGRIDDB_API_BASE + QStringLiteral("/search/autocomplete/") + encodedName); QNetworkRequest request(url); request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(m_apiKey).toUtf8()); QNetworkReply *reply = m_networkManager->get(request); reply->setProperty("game", QVariant::fromValue(game)); connect(reply, &QNetworkReply::finished, this, &SteamGridDB::onSearchFinished); } void SteamGridDB::onSearchFinished() { QNetworkReply *reply = qobject_cast(sender()); if (!reply) { processNextGame(); return; } reply->deleteLater(); Game *game = reply->property("game").value(); if (!game) { processNextGame(); return; } if (reply->error() != QNetworkReply::NoError) { Q_EMIT fetchError(game, reply->errorString()); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); QJsonObject root = doc.object(); if (!root[QStringLiteral("success")].toBool()) { Q_EMIT fetchError(game, QStringLiteral("Search failed")); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } QJsonArray data = root[QStringLiteral("data")].toArray(); if (data.isEmpty()) { Q_EMIT fetchError(game, QStringLiteral("No results found")); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } // Use the first result QJsonObject firstResult = data.first().toObject(); int gameId = firstResult[QStringLiteral("id")].toInt(); fetchGrids(game, gameId); } void SteamGridDB::fetchGrids(Game *game, int gameId) { // Request vertical grids (600x900) for game covers QUrl url(STEAMGRIDDB_API_BASE + QStringLiteral("/grids/game/%1").arg(gameId)); QUrlQuery query; query.addQueryItem(QStringLiteral("dimensions"), QStringLiteral("600x900")); query.addQueryItem(QStringLiteral("types"), QStringLiteral("static")); url.setQuery(query); QNetworkRequest request(url); request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(m_apiKey).toUtf8()); QNetworkReply *reply = m_networkManager->get(request); reply->setProperty("game", QVariant::fromValue(game)); connect(reply, &QNetworkReply::finished, this, &SteamGridDB::onGridsFinished); } void SteamGridDB::onGridsFinished() { QNetworkReply *reply = qobject_cast(sender()); if (!reply) { processNextGame(); return; } reply->deleteLater(); Game *game = reply->property("game").value(); if (!game) { processNextGame(); return; } if (reply->error() != QNetworkReply::NoError) { Q_EMIT fetchError(game, reply->errorString()); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); QJsonObject root = doc.object(); if (!root[QStringLiteral("success")].toBool()) { Q_EMIT fetchError(game, QStringLiteral("Failed to fetch grids")); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } QJsonArray data = root[QStringLiteral("data")].toArray(); if (data.isEmpty()) { Q_EMIT fetchError(game, QStringLiteral("No covers available")); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } // Use the first (highest rated) result QJsonObject firstGrid = data.first().toObject(); QString imageUrl = firstGrid[QStringLiteral("url")].toString(); downloadImage(game, QUrl(imageUrl)); } void SteamGridDB::downloadImage(Game *game, const QUrl &imageUrl) { QNetworkRequest request(imageUrl); QNetworkReply *reply = m_networkManager->get(request); reply->setProperty("game", QVariant::fromValue(game)); connect(reply, &QNetworkReply::finished, this, &SteamGridDB::onImageDownloaded); } void SteamGridDB::onImageDownloaded() { QNetworkReply *reply = qobject_cast(sender()); if (!reply) { processNextGame(); return; } reply->deleteLater(); Game *game = reply->property("game").value(); if (!game) { processNextGame(); return; } if (reply->error() != QNetworkReply::NoError) { Q_EMIT fetchError(game, reply->errorString()); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } // Save image to covers directory QString coversPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/covers"); QDir dir(coversPath); if (!dir.exists()) { if (!dir.mkpath(coversPath)) { Q_EMIT fetchError(game, QStringLiteral("Failed to create covers directory")); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } } QString fileName = game->id() + QStringLiteral(".jpg"); QString filePath = coversPath + QStringLiteral("/") + fileName; const QByteArray payload = reply->readAll(); QSaveFile file(filePath); if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { if (file.write(payload) != payload.size() || !file.commit()) { Q_EMIT fetchError(game, QStringLiteral("Failed to save cover image")); m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); return; } QUrl localUrl = QUrl::fromLocalFile(filePath); game->setCoverUrl(localUrl); App::instance()->saveLibrary(); Q_EMIT coverFetched(game, localUrl); } else { Q_EMIT fetchError(game, QStringLiteral("Failed to save cover image")); } m_processedGames++; Q_EMIT fetchProgress(m_processedGames, m_totalGames); processNextGame(); } void SteamGridDB::saveSettings() { const auto config = KSharedConfig::openConfig(QStringLiteral("alakarterc")); KConfigGroup group(config, QStringLiteral("SteamGridDB")); group.writeEntry(QStringLiteral("apiKey"), m_apiKey); group.writeEntry(QStringLiteral("enabled"), m_enabled); group.writeEntry(QStringLiteral("preferSteamGridDB"), m_preferSteamGridDB); config->sync(); } void SteamGridDB::loadSettings() { const auto config = KSharedConfig::openConfig(QStringLiteral("alakarterc")); const KConfigGroup group(config, QStringLiteral("SteamGridDB")); m_apiKey = group.readEntry(QStringLiteral("apiKey"), QString()); m_enabled = group.readEntry(QStringLiteral("enabled"), false); m_preferSteamGridDB = group.readEntry(QStringLiteral("preferSteamGridDB"), false); }