// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2024 A-La-Karte Contributors #include "steamimporter.h" #include #include #include #include #include #include SteamImporter::SteamImporter(QObject *parent) : PlatformImporter(parent) { } QString SteamImporter::platformName() const { return QStringLiteral("Steam"); } QString SteamImporter::platformId() const { return QStringLiteral("steam"); } bool SteamImporter::isAvailable() const { return !findSteamPath().isEmpty(); } QString SteamImporter::findSteamPath() const { // Common Steam installation paths on Linux QStringList possiblePaths = {expandPath(QStringLiteral("~/.steam/steam")), expandPath(QStringLiteral("~/.local/share/Steam")), expandPath(QStringLiteral("~/.var/app/com.valvesoftware.Steam/.steam/steam")), expandPath(QStringLiteral("~/.var/app/com.valvesoftware.Steam/.local/share/Steam")), QStringLiteral("/usr/share/steam"), QStringLiteral("/usr/local/share/steam")}; for (const QString &path : possiblePaths) { if (directoryExists(path)) { return path; } } return {}; } QString SteamImporter::steamPath() const { return findSteamPath(); } QStringList SteamImporter::findLibraryFolders() const { QStringList folders; QString steamDir = findSteamPath(); if (steamDir.isEmpty()) { return folders; } // Add main Steam directory QString steamApps = steamDir + QStringLiteral("/steamapps"); if (directoryExists(steamApps)) { folders.append(steamApps); } // Parse libraryfolders.vdf QString libraryFoldersPath = steamApps + QStringLiteral("/libraryfolders.vdf"); QFile file(libraryFoldersPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return folders; } QTextStream in(&file); QString content = in.readAll(); // Simple VDF parser for library folders static QRegularExpression pathRegex(QStringLiteral("\"path\"\\s+\"([^\"]+)\"")); QRegularExpressionMatchIterator it = pathRegex.globalMatch(content); while (it.hasNext()) { QRegularExpressionMatch match = it.next(); QString path = match.captured(1); QString appsPath = path + QStringLiteral("/steamapps"); if (directoryExists(appsPath) && !folders.contains(appsPath)) { folders.append(appsPath); } } return folders; } Game *SteamImporter::parseAppManifest(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return nullptr; } QTextStream in(&file); QString content = in.readAll(); // Simple VDF parser auto getValue = [&content](const QString &key) -> QString { QRegularExpression regex(QStringLiteral("\"%1\"\\s+\"([^\"]+)\"").arg(key)); QRegularExpressionMatch match = regex.match(content); return match.hasMatch() ? match.captured(1) : QString(); }; QString appId = getValue(QStringLiteral("appid")); QString name = getValue(QStringLiteral("name")); QString installDir = getValue(QStringLiteral("installdir")); if (appId.isEmpty() || name.isEmpty()) { return nullptr; } // Skip tools and other non-game content QString stateFlags = getValue(QStringLiteral("StateFlags")); if (stateFlags == QLatin1String("2")) { // Only partially installed } Game *game = new Game(QStringLiteral("steam-%1").arg(appId), name); game->setPlatform(platformName()); game->setPlatformId(appId); game->setLaunchCommand(QStringLiteral("steam://rungameid/%1").arg(appId)); // Find cover image QUrl coverUrl = findCoverImage(appId); if (coverUrl.isValid()) { game->setCoverUrl(coverUrl); } // Set installation directory QFileInfo manifestInfo(path); QString gameDir = manifestInfo.absolutePath() + QStringLiteral("/common/") + installDir; game->setWorkingDirectory(gameDir); game->setInstalled(true); return game; } QUrl SteamImporter::findCoverImage(const QString &appId) const { QString steamDir = findSteamPath(); if (steamDir.isEmpty()) { return {}; } // Check local cache first QStringList coverNames = {QStringLiteral("library_600x900.jpg"), QStringLiteral("library_600x900_2x.jpg"), QStringLiteral("header.jpg"), QStringLiteral("capsule_616x353.jpg")}; QString cacheDir = steamDir + QStringLiteral("/appcache/librarycache"); for (const QString &coverName : coverNames) { QString coverPath = cacheDir + QStringLiteral("/%1_%2").arg(appId, coverName); if (QFile::exists(coverPath)) { return QUrl::fromLocalFile(coverPath); } } // Check alternative grid path QString gridDir = steamDir + QStringLiteral("/userdata"); QDir userDataDir(gridDir); QStringList userDirs = userDataDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString &userDir : userDirs) { QString gridPath = gridDir + QStringLiteral("/%1/config/grid").arg(userDir); QDir grid(gridPath); if (grid.exists()) { QStringList filters = {appId + QStringLiteral("p.*"), appId + QStringLiteral(".*")}; QStringList images = grid.entryList(filters, QDir::Files); if (!images.isEmpty()) { return QUrl::fromLocalFile(gridPath + QStringLiteral("/") + images.first()); } } } // Return Steam CDN URL as fallback return QUrl(QStringLiteral("https://steamcdn-a.akamaihd.net/steam/apps/%1/library_600x900_2x.jpg").arg(appId)); } QList SteamImporter::importGames() { QList games; QStringList libraryFolders = findLibraryFolders(); int total = 0; int current = 0; // Count manifests first for (const QString &folder : libraryFolders) { QDir dir(folder); total += dir.entryList({QStringLiteral("appmanifest_*.acf")}, QDir::Files).count(); } for (const QString &folder : libraryFolders) { QDir dir(folder); QStringList manifests = dir.entryList({QStringLiteral("appmanifest_*.acf")}, QDir::Files); for (const QString &manifest : manifests) { Game *game = parseAppManifest(folder + QStringLiteral("/") + manifest); if (game) { games.append(game); } current++; Q_EMIT importProgress(current, total); } } return games; }