{ 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; }; }; }