feat: adding possibility to create steam shortcut

This commit is contained in:
Chubby Granny Chaser
2025-05-13 22:57:33 +01:00
parent 00065ab0c9
commit 86de5aa89e
14 changed files with 342 additions and 24 deletions

View File

@@ -35,6 +35,7 @@ import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./library/toggle-automatic-cloud-sync";
import "./library/get-default-wine-prefix-selection-path";
import "./library/create-steam-shortcut";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";

View File

@@ -0,0 +1,131 @@
import { registerEvent } from "../register-event";
import type { GameShop, GameStats } from "@types";
import { gamesSublevel, levelKeys } from "@main/level";
import {
composeSteamShortcut,
getSteamLocation,
getSteamShortcuts,
getSteamUserId,
HydraApi,
logger,
SystemPath,
writeSteamShortcuts,
} from "@main/services";
import fs from "node:fs";
import axios from "axios";
import path from "node:path";
const downloadAsset = async (downloadPath: string, url?: string | null) => {
try {
if (fs.existsSync(downloadPath)) {
return downloadPath;
}
if (!url) {
return null;
}
fs.mkdirSync(path.dirname(downloadPath), { recursive: true });
const response = await axios.get(url, { responseType: "arraybuffer" });
fs.writeFileSync(downloadPath, response.data);
return downloadPath;
} catch (error) {
logger.error("Failed to download asset", error);
return null;
}
};
const createSteamShortcut = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (game) {
if (!game.executablePath) {
throw new Error("No executable path found for game");
}
const { assets } = await HydraApi.get<GameStats>(
`/games/stats?objectId=${objectId}&shop=${shop}`
);
const steamUserId = getSteamUserId();
if (!steamUserId) {
logger.error("No Steam user ID found");
return;
}
logger.info("Got Steam user id", steamUserId);
const steamShortcuts = await getSteamShortcuts(steamUserId);
if (
steamShortcuts.some(
(shortcut) =>
shortcut.Exe === game.executablePath &&
shortcut.appname === game.title
)
) {
return;
}
const icon = await downloadAsset(
path.join(
SystemPath.getPath("userData"),
"Icons",
`${game.shop}-${game.objectId}.ico`
),
assets?.iconUrl
);
const newShortcut = composeSteamShortcut(
game.title,
game.executablePath,
icon
);
const gridPath = path.join(
await getSteamLocation(),
"userdata",
steamUserId.toString(),
"config",
"grid"
);
fs.mkdirSync(gridPath, { recursive: true });
await Promise.allSettled([
downloadAsset(
path.join(gridPath, `${newShortcut.appid}_hero.jpg`),
assets?.libraryHeroImageUrl
),
downloadAsset(
path.join(gridPath, `${newShortcut.appid}_logo.png`),
assets?.logoImageUrl
),
downloadAsset(
path.join(gridPath, `${newShortcut.appid}p.jpg`),
assets?.coverImageUrl
),
downloadAsset(
path.join(gridPath, `${newShortcut.appid}.jpg`),
assets?.libraryImageUrl
),
]);
steamShortcuts.push(newShortcut);
logger.info(newShortcut);
logger.info("Writing Steam shortcuts", steamShortcuts);
return writeSteamShortcuts(steamUserId, steamShortcuts);
}
};
registerEvent("createSteamShortcut", createSteamShortcut);

View File

@@ -29,7 +29,9 @@ export const loadState = async () => {
await import("./events");
Aria2.spawn();
if (process.platform !== "darwin") {
Aria2.spawn();
}
if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences.realDebridApiToken);

View File

@@ -22,12 +22,6 @@ const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
win32: "hydra-python-rpc.exe",
};
const rustBinaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
darwin: "hydra-httpdl",
linux: "hydra-httpdl",
win32: "hydra-httpdl.exe",
};
export class PythonRPC {
public static readonly BITTORRENT_PORT = "5881";
public static readonly RPC_PORT = "8084";
@@ -72,20 +66,6 @@ export class PythonRPC {
rpcPassword,
initialDownload ? JSON.stringify(initialDownload) : "",
initialSeeding ? JSON.stringify(initialSeeding) : "",
app.isPackaged
? path.join(
process.resourcesPath,
rustBinaryNameByPlatform[process.platform]!
)
: path.join(
__dirname,
"..",
"..",
"rust_rpc",
"target",
"debug",
rustBinaryNameByPlatform[process.platform]!
),
];
if (app.isPackaged) {

View File

@@ -1,8 +1,14 @@
import axios from "axios";
import path from "node:path";
import fs from "node:fs";
import { crc32 } from "crc";
import WinReg from "winreg";
import { parseBuffer, writeBuffer } from "steam-shortcut-editor";
import type { SteamAppDetails } from "@types";
import type { SteamAppDetails, SteamShortcut } from "@types";
import { logger } from "./logger";
import { SystemPath } from "./system-path";
export interface SteamAppDetailsResponse {
[key: string]: {
@@ -11,6 +17,36 @@ export interface SteamAppDetailsResponse {
};
}
export const getSteamLocation = async (): Promise<string> => {
if (process.platform === "linux") {
return path.join(SystemPath.getPath("home"), ".local", "share", "Steam");
}
if (process.platform === "darwin") {
return path.join(
SystemPath.getPath("home"),
"Library",
"Application Support",
"Steam"
);
}
const regKey = new WinReg({
hive: WinReg.HKCU,
key: "\\Software\\Valve\\Steam",
});
return new Promise((resolve, reject) => {
regKey.get("SteamPath", (err, value) => {
if (err) {
reject(err);
}
resolve(value.value);
});
});
};
export const getSteamAppDetails = async (
objectId: string,
language: string
@@ -40,3 +76,89 @@ export const getSteamAppDetails = async (
return null;
});
};
export const getSteamUserId = () => {
const userDataPath = path.join(
SystemPath.getPath("appData"),
"Steam",
"userdata"
);
const userIds = fs.readdirSync(userDataPath, { withFileTypes: true });
const [steamUserId] = userIds.filter((dir) => dir.isDirectory());
if (!steamUserId) {
return null;
}
return Number(steamUserId.name);
};
export const getSteamShortcuts = async (steamUserId: number) => {
const shortcuts = parseBuffer(
fs.readFileSync(
path.join(
await getSteamLocation(),
"userdata",
steamUserId.toString(),
"config",
"shortcuts.vdf"
)
)
);
return shortcuts.shortcuts as SteamShortcut[];
};
export const generateSteamShortcutAppId = (
exePath: string,
gameName: string
) => {
const input = exePath + gameName;
const crcValue = crc32(input) >>> 0;
const steamAppId = (crcValue | 0x80000000) >>> 0;
return steamAppId;
};
export const composeSteamShortcut = (
title: string,
executablePath: string,
iconPath: string | null
): SteamShortcut => {
return {
appid: generateSteamShortcutAppId(executablePath, title),
appname: title,
Exe: executablePath,
StartDir: path.dirname(executablePath),
icon: iconPath ?? "",
ShortcutPath: "",
LaunchOptions: "",
IsHidden: false,
AllowDesktopConfig: true,
AllowOverlay: true,
OpenVR: false,
Devkit: false,
DevkitGameID: "",
DevkitOverrideAppID: false,
LastPlayTime: false,
FlatpakAppID: "",
};
};
export const writeSteamShortcuts = async (
steamUserId: number,
shortcuts: SteamShortcut[]
) => {
const buffer = writeBuffer({ shortcuts });
fs.writeFileSync(
path.join(
await getSteamLocation(),
"userdata",
steamUserId.toString(),
"config",
"shortcuts.vdf"
),
buffer
);
};