initial steam-rom-manager home-manager module
This commit is contained in:
@@ -7,10 +7,13 @@
|
|||||||
{
|
{
|
||||||
imports =
|
imports =
|
||||||
[ # Include the results of the hardware scan.
|
[ # Include the results of the hardware scan.
|
||||||
|
../default.nix
|
||||||
./boot.nix
|
./boot.nix
|
||||||
./jovian.nix
|
./jovian.nix
|
||||||
./steam-shortcuts.nix
|
# ./steam-games.nix
|
||||||
# ./steam-game
|
# ./steam-game
|
||||||
|
# ./steam.nix
|
||||||
|
# ./steam-rom-manager.nix
|
||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -165,27 +168,6 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# services.steamRomManager = {
|
|
||||||
# enable = true;
|
|
||||||
# user = "deck";
|
|
||||||
|
|
||||||
# romSources = [
|
|
||||||
# {
|
|
||||||
# name = "Nintendo Switch ROMs";
|
|
||||||
# path = "/home/deck/Emulation/roms/switch";
|
|
||||||
# platform = "nintendoswitch";
|
|
||||||
# emulator = "ryujinx";
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
|
|
||||||
# emulators = [
|
|
||||||
# {
|
|
||||||
# name = "Ryujinx";
|
|
||||||
# executable = "${pkgs.ryujinx-greemdev}/bin/ryujinx";
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
|
|
||||||
# Some programs need SUID wrappers, can be configured further or are
|
# Some programs need SUID wrappers, can be configured further or are
|
||||||
# started in user sessions.
|
# started in user sessions.
|
||||||
# programs.mtr.enable = true;
|
# programs.mtr.enable = true;
|
||||||
|
|||||||
@@ -22,6 +22,38 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
imports = [
|
||||||
|
./steam-rom-manager.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
programs.steam-rom-manager = {
|
||||||
|
enable = true;
|
||||||
|
steamUsername = "mjallen18";
|
||||||
|
|
||||||
|
# Optional: override default paths if needed
|
||||||
|
romsDirectory = "/home/deck/Emulation/roms";
|
||||||
|
steamDirectory = "/home/deck/.local/share/Steam";
|
||||||
|
|
||||||
|
emulators = {
|
||||||
|
ryujinx = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.ryujinx-greemdev;
|
||||||
|
};
|
||||||
|
|
||||||
|
dolphin-gamecube = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.dolphin-emu;
|
||||||
|
romFolder = "gc";
|
||||||
|
binaryName = "dolphin-emu";
|
||||||
|
fileTypes = [ ".iso" ".ISO" ".gcm" ".GCM" ".ciso" ".CISO" "rvz" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
pcsx2 = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.pcsx2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
home.username = "deck";
|
home.username = "deck";
|
||||||
home.homeDirectory = "/home/deck";
|
home.homeDirectory = "/home/deck";
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
{ pkgs, ... }:
|
|
||||||
{
|
|
||||||
# You'll need to get an API key from https://www.steamgriddb.com/profile/preferences/api
|
|
||||||
steamGridDbApiKey = "ada6729c6cd321a4e3792ddd0b83c1ba";
|
|
||||||
# List your non-Steam games here
|
|
||||||
games = [
|
|
||||||
{
|
|
||||||
name = "Ryujinx";
|
|
||||||
path = "${pkgs.ryujinx-greemdev}/bin/ryujinx";
|
|
||||||
extraLaunchOptions = "";
|
|
||||||
steamGridDbIds = {
|
|
||||||
grid = "121085";
|
|
||||||
hero = "63924";
|
|
||||||
logo = "29735";
|
|
||||||
list = "5517";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "Prismlauncher";
|
|
||||||
path = "${pkgs.prismlauncher}/bin/prismlauncher";
|
|
||||||
extraLaunchOptions = "";
|
|
||||||
steamGridDbIds = {
|
|
||||||
grid = "267141";
|
|
||||||
hero = "64303";
|
|
||||||
logo = "68299";
|
|
||||||
list = "23582";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "Mario Kart 8";
|
|
||||||
path = "${pkgs.ryujinx-greemdev}/bin/ryujinx";
|
|
||||||
extraLaunchOptions = "--fullscreen '/home/deck/Emulation/roms/switch/Mario Kart 8 Deluxe [B+U720896].nsp'";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
321
hosts/deck/steam-rom-manager.nix
Normal file
321
hosts/deck/steam-rom-manager.nix
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# steam-rom-manager.nix
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.programs.steam-rom-manager;
|
||||||
|
|
||||||
|
# Common emulator configurations
|
||||||
|
commonEmulatorConfigs = {
|
||||||
|
ryujinx = {
|
||||||
|
romFolder = "switch";
|
||||||
|
binaryName = "Ryujinx";
|
||||||
|
fileTypes = [ ".nca" ".NCA" ".nro" ".NRO" ".nso" ".NSO" ".nsp" ".NSP" ".xci" ".XCI" ];
|
||||||
|
};
|
||||||
|
yuzu = {
|
||||||
|
romFolder = "switch";
|
||||||
|
binaryName = "yuzu";
|
||||||
|
fileTypes = [ ".nsp" ".NSP" ".xci" ".XCI" ];
|
||||||
|
};
|
||||||
|
pcsx2 = {
|
||||||
|
romFolder = "ps2";
|
||||||
|
binaryName = "pcsx2-qt";
|
||||||
|
fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".chd" ".CHD" ];
|
||||||
|
};
|
||||||
|
rpcs3 = {
|
||||||
|
romFolder = "ps3";
|
||||||
|
binaryName = "rpcs3";
|
||||||
|
fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".pkg" ".PKG" ];
|
||||||
|
};
|
||||||
|
dolphin-emu = {
|
||||||
|
romFolder = "gc";
|
||||||
|
binaryName = "dolphin-emu";
|
||||||
|
fileTypes = [ ".iso" ".ISO" ".gcm" ".GCM" ".ciso" ".CISO" ];
|
||||||
|
};
|
||||||
|
duckstation = {
|
||||||
|
romFolder = "psx";
|
||||||
|
binaryName = "duckstation-qt";
|
||||||
|
fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".chd" ".CHD" ".pbp" ".PBP" ];
|
||||||
|
};
|
||||||
|
melonDS = {
|
||||||
|
romFolder = "nds";
|
||||||
|
binaryName = "melonDS";
|
||||||
|
fileTypes = [ ".nds" ".NDS" ];
|
||||||
|
};
|
||||||
|
cemu = {
|
||||||
|
romFolder = "wiiu";
|
||||||
|
binaryName = "cemu";
|
||||||
|
fileTypes = [ ".wud" ".WUD" ".wux" ".WUX" ".rpx" ".RPX" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getFileTypes = name: emu:
|
||||||
|
if (emu.fileTypes or []) != []
|
||||||
|
then emu.fileTypes
|
||||||
|
else commonEmulatorConfigs.${name}.fileTypes or [];
|
||||||
|
|
||||||
|
mkParserConfig = name: emu:
|
||||||
|
let
|
||||||
|
# We create an ordered list of key-value pairs that will maintain the exact order
|
||||||
|
# in the generated JSON output. Each field is documented for clarity.
|
||||||
|
orderedConfig = [
|
||||||
|
# Basic parser configuration
|
||||||
|
{ name = "parserType"; value = "Glob"; }
|
||||||
|
{ name = "configTitle"; value = name; }
|
||||||
|
{ name = "steamDirectory"; value = "\${steamdirglobal}"; }
|
||||||
|
{ name = "romDirectory"; value = "${cfg.romsDirectory}/${if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder}"; }
|
||||||
|
{ name = "steamCategories"; value = [""]; }
|
||||||
|
|
||||||
|
# Executable configuration
|
||||||
|
{ name = "executableArgs"; value = emu.extraArgs; }
|
||||||
|
{ name = "executableModifier"; value = "\"\${exePath}\""; }
|
||||||
|
{ name = "startInDirectory"; value = "${cfg.romsDirectory}/${if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder}"; }
|
||||||
|
{ name = "titleModifier"; value = "\${fuzzyTitle}"; }
|
||||||
|
|
||||||
|
# Controller settings
|
||||||
|
{ name = "fetchControllerTemplatesButton"; value = null; }
|
||||||
|
{ name = "removeControllersButton"; value = null; }
|
||||||
|
{ name = "steamInputEnabled"; value = "1"; }
|
||||||
|
|
||||||
|
# Image provider configuration
|
||||||
|
{ name = "imageProviders"; value = [ "sgdb" ]; }
|
||||||
|
{ name = "onlineImageQueries"; value = [ "\${fuzzyTitle}" ]; }
|
||||||
|
{ name = "imagePool"; value = "\${fuzzyTitle}"; }
|
||||||
|
|
||||||
|
# DRM and user account settings
|
||||||
|
{ name = "drmProtect"; value = false; }
|
||||||
|
{ name = "userAccounts"; value = {
|
||||||
|
specifiedAccounts = [ "Global" ];
|
||||||
|
}; }
|
||||||
|
|
||||||
|
# Parser-specific settings
|
||||||
|
{ name = "parserInputs"; value = {
|
||||||
|
glob = "\${title}@(${concatStringsSep "|" (getFileTypes name emu)})";
|
||||||
|
}; }
|
||||||
|
|
||||||
|
# Executable details
|
||||||
|
{ name = "executable"; value = {
|
||||||
|
path = "${emu.package}/bin/${if emu.binaryName != "" then emu.binaryName else commonEmulatorConfigs.${name}.binaryName}";
|
||||||
|
shortcutPassthrough = false;
|
||||||
|
appendArgsToExecutable = true;
|
||||||
|
}; }
|
||||||
|
|
||||||
|
# Title and fuzzy matching configuration
|
||||||
|
{ name = "titleFromVariable"; value = {
|
||||||
|
limitToGroups = [];
|
||||||
|
caseInsensitiveVariables = false;
|
||||||
|
skipFileIfVariableWasNotFound = false;
|
||||||
|
}; }
|
||||||
|
|
||||||
|
{ name = "fuzzyMatch"; value = {
|
||||||
|
replaceDiacritics = true;
|
||||||
|
removeCharacters = true;
|
||||||
|
removeBrackets = true;
|
||||||
|
}; }
|
||||||
|
|
||||||
|
# Controller configuration
|
||||||
|
{ name = "controllers"; value = {
|
||||||
|
ps4 = null;
|
||||||
|
ps5 = null;
|
||||||
|
ps5_edge = null;
|
||||||
|
xbox360 = null;
|
||||||
|
xboxone = null;
|
||||||
|
xboxelite = null;
|
||||||
|
switch_joycon_left = null;
|
||||||
|
switch_joycon_right = null;
|
||||||
|
switch_pro = null;
|
||||||
|
neptune = null;
|
||||||
|
steamcontroller_gordon = null;
|
||||||
|
}; }
|
||||||
|
|
||||||
|
# Image provider API configuration
|
||||||
|
{ name = "imageProviderAPIs"; value = {
|
||||||
|
sgdb = {
|
||||||
|
nsfw = false;
|
||||||
|
humor = false;
|
||||||
|
styles = [];
|
||||||
|
stylesHero = [];
|
||||||
|
stylesLogo = [];
|
||||||
|
stylesIcon = [];
|
||||||
|
imageMotionTypes = [ "static" ];
|
||||||
|
sizes = [];
|
||||||
|
sizesHero = [];
|
||||||
|
sizesTall = null;
|
||||||
|
sizesIcon = [];
|
||||||
|
};
|
||||||
|
}; }
|
||||||
|
|
||||||
|
# Default and local image settings
|
||||||
|
{ name = "defaultImage"; value = {
|
||||||
|
tall = "";
|
||||||
|
long = "";
|
||||||
|
hero = "";
|
||||||
|
logo = "";
|
||||||
|
icon = "";
|
||||||
|
}; }
|
||||||
|
|
||||||
|
{ name = "localImages"; value = {
|
||||||
|
tall = "";
|
||||||
|
long = "";
|
||||||
|
hero = "";
|
||||||
|
logo = "";
|
||||||
|
icon = "";
|
||||||
|
}; }
|
||||||
|
|
||||||
|
# Parser identification
|
||||||
|
{ name = "parserId"; value = name; }
|
||||||
|
{ name = "version"; value = 25; }
|
||||||
|
];
|
||||||
|
|
||||||
|
# Function to convert our ordered list into properly formatted JSON
|
||||||
|
makeOrderedJSON = pairs:
|
||||||
|
let
|
||||||
|
# Create JSON key-value pairs while preserving order
|
||||||
|
joined = builtins.concatStringsSep ","
|
||||||
|
(map (pair: "\"${pair.name}\":${builtins.toJSON pair.value}") pairs);
|
||||||
|
in
|
||||||
|
"{${joined}}";
|
||||||
|
in
|
||||||
|
makeOrderedJSON orderedConfig;
|
||||||
|
|
||||||
|
# Create AppImage package
|
||||||
|
steam-rom-manager = pkgs.fetchurl {
|
||||||
|
name = "steam-rom-manager-2.5.29.AppImage";
|
||||||
|
url = "https://github.com/SteamGridDB/steam-rom-manager/releases/download/v2.5.29/Steam-ROM-Manager-2.5.29.AppImage";
|
||||||
|
hash = "sha256-6ZJ+MGIgr2osuQuqD6N9NnPiJFNq/HW6ivG8tyXUhvs=";
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
options.programs.steam-rom-manager = {
|
||||||
|
enable = mkEnableOption "Steam ROM Manager";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = steam-rom-manager;
|
||||||
|
description = "Steam ROM Manager package";
|
||||||
|
};
|
||||||
|
|
||||||
|
steamUsername = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Steam username for configuration";
|
||||||
|
};
|
||||||
|
|
||||||
|
romsDirectory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${config.home.homeDirectory}/Emulation/roms";
|
||||||
|
description = "Base directory for ROM files";
|
||||||
|
};
|
||||||
|
|
||||||
|
steamDirectory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${config.home.homeDirectory}/.local/share/Steam";
|
||||||
|
description = "Steam installation directory";
|
||||||
|
};
|
||||||
|
|
||||||
|
emulators = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
enable = mkEnableOption "emulator configuration";
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
description = "Emulator package";
|
||||||
|
};
|
||||||
|
binaryName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Name of the emulator binary (defaults to common configuration)";
|
||||||
|
};
|
||||||
|
romFolder = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Name of the ROM folder (defaults to common configuration)";
|
||||||
|
};
|
||||||
|
fileTypes = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = "List of ROM file types (defaults to common configuration)";
|
||||||
|
};
|
||||||
|
extraArgs = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "--fullscreen \"\${filePath}\"";
|
||||||
|
description = "Additional emulator arguments";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = {};
|
||||||
|
description = "Emulator configurations";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
|
home.packages = [ pkgs.appimage-run ]
|
||||||
|
++ mapAttrsToList (_: v: v.package) (filterAttrs (_: v: v.enable) cfg.emulators);
|
||||||
|
|
||||||
|
xdg.desktopEntries.steam-rom-manager = {
|
||||||
|
name = "Steam ROM Manager";
|
||||||
|
exec = "${pkgs.appimage-run}/bin/appimage-run ${cfg.package}";
|
||||||
|
icon = "steam";
|
||||||
|
categories = [ "Game" "Utility" ];
|
||||||
|
type = "Application";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configuration files using xdg.configFile
|
||||||
|
xdg.configFile = {
|
||||||
|
# User settings configuration
|
||||||
|
"steam-rom-manager/userData/userSettings.json".text = builtins.toJSON {
|
||||||
|
fuzzyMatcher = {
|
||||||
|
timestamps = {
|
||||||
|
check = 0;
|
||||||
|
download = 0;
|
||||||
|
};
|
||||||
|
verbose = false;
|
||||||
|
filterProviders = true;
|
||||||
|
};
|
||||||
|
environmentVariables = {
|
||||||
|
steamDirectory = cfg.steamDirectory;
|
||||||
|
userAccounts = "\${${cfg.steamUsername}}";
|
||||||
|
romsDirectory = cfg.romsDirectory;
|
||||||
|
retroarchPath = "";
|
||||||
|
raCoresDirectory = "";
|
||||||
|
localImagesDirectory = "";
|
||||||
|
};
|
||||||
|
previewSettings = {
|
||||||
|
retrieveCurrentSteamImages = true;
|
||||||
|
disableCategories = false;
|
||||||
|
deleteDisabledShortcuts = false;
|
||||||
|
imageZoomPercentage = 30;
|
||||||
|
preload = false;
|
||||||
|
hideUserAccount = false;
|
||||||
|
};
|
||||||
|
enabledProviders = [ "sgdb" "steamCDN" ];
|
||||||
|
batchDownloadSize = 50;
|
||||||
|
dnsServers = [];
|
||||||
|
language = "en-US";
|
||||||
|
theme = "Deck";
|
||||||
|
emudeckInstall = false;
|
||||||
|
autoUpdate = false;
|
||||||
|
offlineMode = false;
|
||||||
|
navigationWidth = 0;
|
||||||
|
clearLogOnTest = true;
|
||||||
|
version = 8;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Parser configurations
|
||||||
|
"steam-rom-manager/userData/userConfigurations.json".text =
|
||||||
|
let
|
||||||
|
# Generate configurations
|
||||||
|
configs = mapAttrsToList (name: emu:
|
||||||
|
mkParserConfig name (emu // {
|
||||||
|
romFolder = if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder;
|
||||||
|
binaryName = if emu.binaryName != "" then emu.binaryName else commonEmulatorConfigs.${name}.binaryName;
|
||||||
|
})
|
||||||
|
) (filterAttrs (_: v: v.enable) cfg.emulators);
|
||||||
|
|
||||||
|
# Join configurations into a valid JSON array
|
||||||
|
configsJson = "[${concatStringsSep "," configs}]";
|
||||||
|
in
|
||||||
|
configsJson;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
# Import the games configuration
|
|
||||||
gamesConfig = import ./steam-games.nix { inherit pkgs; };
|
|
||||||
|
|
||||||
# Create the VDF generator script
|
|
||||||
vdfGeneratorScript = pkgs.writeText "generate_vdf.py" ''
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import struct
|
|
||||||
import time
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
def generate_app_id(name):
|
|
||||||
"""Generate Steam app ID for a game using Steam's apparent format."""
|
|
||||||
# Get the current timestamp
|
|
||||||
timestamp = int(time.time())
|
|
||||||
|
|
||||||
# Create a hash of the name and timestamp
|
|
||||||
hash_input = f"{name}{timestamp}".encode('utf-8')
|
|
||||||
hash_obj = hashlib.md5(hash_input)
|
|
||||||
|
|
||||||
# Take first 6 bytes of hash and combine with a fixed pattern
|
|
||||||
hash_bytes = hash_obj.digest()[:6]
|
|
||||||
|
|
||||||
# Create a 64-bit number where:
|
|
||||||
# - First 6 bytes come from hash
|
|
||||||
# - Last 2 bytes are zeros (matching Steam's pattern)
|
|
||||||
full_bytes = hash_bytes + b'\x00\x00'
|
|
||||||
|
|
||||||
# Convert to 64-bit integer
|
|
||||||
app_id = struct.unpack(">Q", full_bytes)[0]
|
|
||||||
|
|
||||||
# Ensure the number is in the correct range by setting specific bits
|
|
||||||
app_id |= (1 << 62) # Set the second-to-highest bit
|
|
||||||
app_id &= ~(1 << 63) # Clear the highest bit
|
|
||||||
|
|
||||||
return app_id
|
|
||||||
|
|
||||||
def write_vdf(shortcuts):
|
|
||||||
"""Write VDF format matching the exact byte sequence."""
|
|
||||||
output = bytearray()
|
|
||||||
|
|
||||||
# Header
|
|
||||||
output.extend(b'\x00shortcuts\x00')
|
|
||||||
|
|
||||||
for i, shortcut in enumerate(shortcuts):
|
|
||||||
# Index
|
|
||||||
output.extend(f'\x00{i}\x00'.encode('utf-8'))
|
|
||||||
|
|
||||||
# AppName
|
|
||||||
output.extend(b'\x01appname\x00')
|
|
||||||
output.extend(shortcut['AppName'].encode('utf-8') + b'\x00')
|
|
||||||
|
|
||||||
# App ID (this is the key addition)
|
|
||||||
output.extend(b'\x02appid\x00')
|
|
||||||
output.extend(f'"{shortcut["Id"]}"'.encode('utf-8') + b'\x00')
|
|
||||||
|
|
||||||
# Exe
|
|
||||||
output.extend(b'\x01exe\x00')
|
|
||||||
output.extend(f'"{shortcut["ExePath"]}"'.encode('utf-8') + b'\x00')
|
|
||||||
|
|
||||||
# StartDir
|
|
||||||
output.extend(b'\x01StartDir\x00')
|
|
||||||
output.extend(b'"/home/deck"\x00')
|
|
||||||
|
|
||||||
# Icon
|
|
||||||
output.extend(b'\x01icon\x00\x00')
|
|
||||||
|
|
||||||
# ShortcutPath
|
|
||||||
output.extend(b'\x01ShortcutPath\x00\x00')
|
|
||||||
|
|
||||||
# LaunchOptions
|
|
||||||
output.extend(b'\x01LaunchOptions\x00')
|
|
||||||
output.extend(f'"{shortcut["LaunchOptions"]}"'.encode('utf-8') + b'\x00')
|
|
||||||
|
|
||||||
# IsHidden
|
|
||||||
output.extend(b'\x02IsHidden\x00\x00\x00\x00\x00')
|
|
||||||
|
|
||||||
# AllowDesktopConfig
|
|
||||||
output.extend(b'\x02AllowDesktopConfig\x00\x01\x00\x00\x00')
|
|
||||||
|
|
||||||
# AllowOverlay
|
|
||||||
output.extend(b'\x02AllowOverlay\x00\x01\x00\x00\x00')
|
|
||||||
|
|
||||||
# OpenVR
|
|
||||||
output.extend(b'\x02OpenVR\x00\x00\x00\x00\x00')
|
|
||||||
|
|
||||||
# Tags
|
|
||||||
output.extend(b'\x00tags\x00\x08')
|
|
||||||
|
|
||||||
# Entry terminator
|
|
||||||
output.extend(b'\x08')
|
|
||||||
|
|
||||||
# Final terminators
|
|
||||||
output.extend(b'\x08\x08')
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
shortcuts = json.load(sys.stdin)
|
|
||||||
vdf_data = write_vdf(shortcuts)
|
|
||||||
sys.stdout.buffer.write(vdf_data)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Create the SteamGridDB image downloader script
|
|
||||||
steamGridDbScript = pkgs.writeText "fetch_steamgriddb.py" ''
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
import hashlib
|
|
||||||
import struct
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def generate_app_id(name):
|
|
||||||
"""Generate Steam app ID for a game using Steam's apparent format."""
|
|
||||||
# Get the current timestamp
|
|
||||||
timestamp = int(time.time())
|
|
||||||
|
|
||||||
# Create a hash of the name and timestamp
|
|
||||||
hash_input = f"{name}{timestamp}".encode('utf-8')
|
|
||||||
hash_obj = hashlib.md5(hash_input)
|
|
||||||
|
|
||||||
# Take first 6 bytes of hash and combine with a fixed pattern
|
|
||||||
hash_bytes = hash_obj.digest()[:6]
|
|
||||||
|
|
||||||
# Create a 64-bit number where:
|
|
||||||
# - First 6 bytes come from hash
|
|
||||||
# - Last 2 bytes are zeros (matching Steam's pattern)
|
|
||||||
full_bytes = hash_bytes + b'\x00\x00'
|
|
||||||
|
|
||||||
# Convert to 64-bit integer
|
|
||||||
app_id = struct.unpack(">Q", full_bytes)[0]
|
|
||||||
|
|
||||||
# Ensure the number is in the correct range by setting specific bits
|
|
||||||
app_id |= (1 << 62) # Set the second-to-highest bit
|
|
||||||
app_id &= ~(1 << 63) # Clear the highest bit
|
|
||||||
|
|
||||||
return app_id
|
|
||||||
|
|
||||||
def download_image(url, path, headers=None):
|
|
||||||
"""Download an image from a URL to a specified path."""
|
|
||||||
if url:
|
|
||||||
try:
|
|
||||||
response = requests.get(url) # Don't use auth headers for actual download
|
|
||||||
if response.status_code == 200:
|
|
||||||
path.write_bytes(response.content)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error downloading: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def fetch_images(api_key, game_name, grid_ids, output_dir):
|
|
||||||
"""Fetch images for a game from SteamGridDB."""
|
|
||||||
print(f"\nProcessing images for {game_name}")
|
|
||||||
headers = {"Authorization": f"Bearer {api_key}"}
|
|
||||||
base_url = "https://www.steamgriddb.com/api/v2"
|
|
||||||
app_id = generate_app_id(game_name)
|
|
||||||
|
|
||||||
# Create output directories
|
|
||||||
grid_dir = output_dir / "config/grid"
|
|
||||||
hero_dir = output_dir / "config/heroes"
|
|
||||||
logo_dir = output_dir / "config/logos"
|
|
||||||
|
|
||||||
print(f"Using directories:")
|
|
||||||
print(f"Grid: {grid_dir}")
|
|
||||||
print(f"Hero: {hero_dir}")
|
|
||||||
print(f"Logo: {logo_dir}")
|
|
||||||
|
|
||||||
grid_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
hero_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
logo_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Test API connection
|
|
||||||
test_response = requests.get(f"{base_url}/grids/game/1", headers=headers)
|
|
||||||
print(f"API test response: {test_response.status_code}")
|
|
||||||
if test_response.status_code == 401:
|
|
||||||
print("API authentication failed. Check your API key.")
|
|
||||||
return
|
|
||||||
|
|
||||||
image_types = {
|
|
||||||
"grid": (grid_dir / f"{app_id}.jpg", grid_ids.get("grid"), "grids"),
|
|
||||||
"hero": (hero_dir / f"{app_id}.jpg", grid_ids.get("hero"), "heroes"),
|
|
||||||
"logo": (logo_dir / f"{app_id}.png", grid_ids.get("logo"), "logos"),
|
|
||||||
"list": (grid_dir / f"{app_id}_list.jpg", grid_ids.get("list"), "grids")
|
|
||||||
}
|
|
||||||
|
|
||||||
for img_type, (output_path, specific_id, asset_type) in image_types.items():
|
|
||||||
if specific_id:
|
|
||||||
# Get direct file URL from the API
|
|
||||||
url = f"{base_url}/{asset_type}/{specific_id}"
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if data.get("success"):
|
|
||||||
image_url = data["data"]["url"]
|
|
||||||
if download_image(image_url, output_path):
|
|
||||||
print(f"Successfully downloaded {img_type} image")
|
|
||||||
else:
|
|
||||||
print(f"Failed to download {img_type} image")
|
|
||||||
else:
|
|
||||||
print(f"API error: {data}")
|
|
||||||
else:
|
|
||||||
print(f"API request failed: status {response.status_code}")
|
|
||||||
if response.headers.get('content-type', "").startswith('application/json'):
|
|
||||||
print(f"Response: {response.json()}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing {img_type}: {e}")
|
|
||||||
time.sleep(1) # Rate limiting
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) != 4:
|
|
||||||
print("Usage: script.py API_KEY game_data.json output_dir", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
api_key = sys.argv[1]
|
|
||||||
game_data_file = sys.argv[2]
|
|
||||||
output_dir = Path(sys.argv[3])
|
|
||||||
|
|
||||||
print(f"Loading game data from: {game_data_file}")
|
|
||||||
print(f"Output directory: {output_dir}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(game_data_file) as f:
|
|
||||||
games = json.load(f)
|
|
||||||
print(f"Loaded {len(games)} games")
|
|
||||||
|
|
||||||
for game in games:
|
|
||||||
if "steamGridDbIds" in game:
|
|
||||||
fetch_images(api_key, game["name"], game["steamGridDbIds"], output_dir)
|
|
||||||
else:
|
|
||||||
print(f"No steamGridDbIds for {game['name']}, skipping")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Python environment with required packages
|
|
||||||
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
|
||||||
requests
|
|
||||||
]);
|
|
||||||
|
|
||||||
# Helper function to convert hex char to decimal
|
|
||||||
hexCharToInt = c:
|
|
||||||
let
|
|
||||||
hexChars = {
|
|
||||||
"0" = 0; "1" = 1; "2" = 2; "3" = 3; "4" = 4;
|
|
||||||
"5" = 5; "6" = 6; "7" = 7; "8" = 8; "9" = 9;
|
|
||||||
"a" = 10; "b" = 11; "c" = 12; "d" = 13; "e" = 14; "f" = 15;
|
|
||||||
"A" = 10; "B" = 11; "C" = 12; "D" = 13; "E" = 14; "F" = 15;
|
|
||||||
};
|
|
||||||
in hexChars.${c};
|
|
||||||
|
|
||||||
# Convert hex string to decimal number
|
|
||||||
hexToInt = str:
|
|
||||||
let
|
|
||||||
len = builtins.stringLength str;
|
|
||||||
chars = lib.stringToCharacters str;
|
|
||||||
go = pos: nums:
|
|
||||||
if pos == len then 0
|
|
||||||
else
|
|
||||||
let
|
|
||||||
char = builtins.elemAt chars pos;
|
|
||||||
num = hexCharToInt char;
|
|
||||||
exp = len - pos - 1;
|
|
||||||
factor = builtins.foldl' (a: b: a * 16) 1 (lib.range 1 exp);
|
|
||||||
in
|
|
||||||
(num * factor) + (go (pos + 1) nums);
|
|
||||||
in go 0 chars;
|
|
||||||
|
|
||||||
# Helper function for the ID generation - handling large numbers
|
|
||||||
generateAppId = name:
|
|
||||||
let
|
|
||||||
hash = builtins.hashString "md5" name;
|
|
||||||
# Instead of trying to create a massive number, we'll generate a unique pattern
|
|
||||||
# that will create a consistent large number when interpreted by Python
|
|
||||||
sixteenHex = builtins.substring 0 16 hash;
|
|
||||||
# Format it as a string that Python will interpret as a large number
|
|
||||||
shortcutAppId = "0x${sixteenHex}0000";
|
|
||||||
in shortcutAppId;
|
|
||||||
|
|
||||||
# Convert games to shortcuts format
|
|
||||||
gameToShortcut = game: {
|
|
||||||
Id = generateAppId game.name;
|
|
||||||
AppName = game.name;
|
|
||||||
ExePath = game.path;
|
|
||||||
LaunchOptions = game.extraLaunchOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Script to update shortcuts and fetch images
|
|
||||||
updateSteamShortcuts = pkgs.writeShellScriptBin "update-steam-shortcuts" ''
|
|
||||||
STEAM_DIR="$HOME/.local/share/Steam"
|
|
||||||
USERDATA_DIR="$STEAM_DIR/userdata"
|
|
||||||
CACHE_DIR="$HOME/.cache/steam-shortcuts"
|
|
||||||
|
|
||||||
if [ ! -d "$USERDATA_DIR" ]; then
|
|
||||||
echo "Steam userdata directory not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make sure Steam is not running
|
|
||||||
if pgrep -x "steam" > /dev/null; then
|
|
||||||
echo "Please close Steam before updating shortcuts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
|
||||||
|
|
||||||
# Process each Steam user directory
|
|
||||||
for USER_DIR in "$USERDATA_DIR"/*; do
|
|
||||||
if [ -d "$USER_DIR" ]; then
|
|
||||||
USER_ID="$(basename "$USER_DIR")"
|
|
||||||
CONFIG_DIR="$USER_DIR/config"
|
|
||||||
mkdir -p "$CONFIG_DIR"
|
|
||||||
|
|
||||||
echo "Processing Steam user $USER_ID..."
|
|
||||||
|
|
||||||
# Create games JSON file
|
|
||||||
GAMES_JSON="$CACHE_DIR/games.json"
|
|
||||||
echo '${builtins.toJSON (map gameToShortcut gamesConfig.games)}' > "$GAMES_JSON"
|
|
||||||
|
|
||||||
# Generate shortcuts.vdf
|
|
||||||
cat "$GAMES_JSON" | ${pythonEnv}/bin/python3 ${vdfGeneratorScript} > "$CONFIG_DIR/shortcuts.vdf"
|
|
||||||
|
|
||||||
# Download images if API key is set
|
|
||||||
if [ "${gamesConfig.steamGridDbApiKey}" != "YOUR_API_KEY_HERE" ]; then
|
|
||||||
echo '${builtins.toJSON gamesConfig.games}' > "$CACHE_DIR/games_full.json"
|
|
||||||
${pythonEnv}/bin/python3 ${steamGridDbScript} "${gamesConfig.steamGridDbApiKey}" "$CACHE_DIR/games_full.json" "$USER_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Updated shortcuts and images for $USER_ID"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Done! You can now start Steam."
|
|
||||||
'';
|
|
||||||
|
|
||||||
in {
|
|
||||||
# Add the update script to system packages
|
|
||||||
environment.systemPackages = [ updateSteamShortcuts ];
|
|
||||||
|
|
||||||
# Optional: Create a systemd user service to update shortcuts on login
|
|
||||||
systemd.user.services.update-steam-shortcuts = {
|
|
||||||
enable = true;
|
|
||||||
description = "Update Steam shortcuts";
|
|
||||||
wantedBy = [ "graphical-session.target" ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
ExecStart = "${updateSteamShortcuts}/bin/update-steam-shortcuts";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user