a-la-karte/src/steamgriddb.cpp
Marco Allegretti 747b02035a alakarte: Initial import
Initial release of A-La-Karte, a unified game launcher for KDE Plasma.

Includes the QML UI, platform importers, AppStream metadata, icons,

and developer documentation.
2026-01-18 13:13:07 +01:00

342 lines
9.3 KiB
C++

// 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 <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#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()) {
dir.mkpath(coversPath);
}
QString fileName = game->id() + QStringLiteral(".jpg");
QString filePath = coversPath + QStringLiteral("/") + fileName;
QFile file(filePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(reply->readAll());
file.close();
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);
}