2026-01-18 12:13:07 +00:00
|
|
|
// 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 <KConfigGroup>
|
|
|
|
|
#include <KSharedConfig>
|
|
|
|
|
#include <QDir>
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
#include <QNetworkReply>
|
2026-02-13 12:20:45 +00:00
|
|
|
#include <QSaveFile>
|
2026-01-18 12:13:07 +00:00
|
|
|
#include <QStandardPaths>
|
|
|
|
|
#include <QUrlQuery>
|
|
|
|
|
|
|
|
|
|
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<QNetworkReply *>(sender());
|
|
|
|
|
if (!reply) {
|
|
|
|
|
processNextGame();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
|
|
|
|
Game *game = reply->property("game").value<Game *>();
|
|
|
|
|
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<QNetworkReply *>(sender());
|
|
|
|
|
if (!reply) {
|
|
|
|
|
processNextGame();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
|
|
|
|
Game *game = reply->property("game").value<Game *>();
|
|
|
|
|
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<QNetworkReply *>(sender());
|
|
|
|
|
if (!reply) {
|
|
|
|
|
processNextGame();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
|
|
|
|
Game *game = reply->property("game").value<Game *>();
|
|
|
|
|
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()) {
|
2026-02-13 12:20:45 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString fileName = game->id() + QStringLiteral(".jpg");
|
|
|
|
|
QString filePath = coversPath + QStringLiteral("/") + fileName;
|
|
|
|
|
|
2026-02-13 12:20:45 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|