|
1 | | -''' |
2 | | -get registry values for steam games |
3 | | -''' |
| 1 | +""" |
| 2 | +Get installed Steam games across platforms. |
| 3 | +
|
| 4 | +Supports: |
| 5 | +- Linux: ~/.steam, ~/.local/share/Steam |
| 6 | +- macOS: ~/Library/Application Support/Steam |
| 7 | +- Windows: Registry + Program Files |
| 8 | +""" |
4 | 9 |
|
5 | 10 | import os |
6 | | -import xbmc |
7 | | -import io |
8 | | -from .util import * |
| 11 | +import sys |
| 12 | + |
| 13 | +from .util import log, show_error |
| 14 | + |
| 15 | +# Add bundled libraries to path |
| 16 | +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib')) |
| 17 | +import vdf |
9 | 18 |
|
10 | 19 | if os.name == 'nt': |
11 | 20 | import winreg |
12 | 21 |
|
13 | 22 |
|
14 | | -# https://github.com/lutris/lutris/blob/master/lutris/util/steam.py |
15 | | -def vdf_parse(steam_config_file, config): |
16 | | - """Parse a Steam config file and return the contents as a dict with lowercase keys. |
17 | | - The motivation behind returning lowercase keys is that the case is not consistent between environments it seems. |
| 23 | +def get_default_steam_paths(): |
18 | 24 | """ |
19 | | - line = " " |
20 | | - while line: |
| 25 | + Returns a list of possible Steam installation paths for the current platform. |
| 26 | + Used as fallback when steam-path setting is not configured. |
| 27 | + """ |
| 28 | + paths = [] |
| 29 | + |
| 30 | + if sys.platform == 'darwin': |
| 31 | + # macOS |
| 32 | + paths.append(os.path.expanduser('~/Library/Application Support/Steam')) |
| 33 | + elif os.name == 'nt': |
| 34 | + # Windows - try to get from registry first |
21 | 35 | try: |
22 | | - line = steam_config_file.readline() |
23 | | - except UnicodeDecodeError: |
24 | | - log("Error while reading Steam VDF file {}. Returning {}".format(steam_config_file, config), xbmc.LOGERROR) |
25 | | - return config |
26 | | - if not line or line.strip() == "}": |
27 | | - return config |
28 | | - while not line.strip().endswith("\""): |
29 | | - nextline = steam_config_file.readline() |
30 | | - if not nextline: |
31 | | - break |
32 | | - line = line[:-1] + nextline |
33 | | - |
34 | | - line_elements = line.strip().split("\"") |
35 | | - if len(line_elements) == 3: |
36 | | - key = line_elements[1].lower() |
37 | | - steam_config_file.readline() # skip '{' |
38 | | - config[key] = vdf_parse(steam_config_file, {}) |
39 | | - else: |
40 | | - try: |
41 | | - config[line_elements[1].lower()] = line_elements[3] |
42 | | - except IndexError: |
43 | | - log('Malformed config file: {}'.format(line), xbmc.LOGERROR) |
44 | | - return config |
| 36 | + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\Valve\Steam') |
| 37 | + steam_path, _ = winreg.QueryValueEx(key, 'SteamPath') |
| 38 | + winreg.CloseKey(key) |
| 39 | + if steam_path: |
| 40 | + paths.append(steam_path) |
| 41 | + except WindowsError: |
| 42 | + pass |
| 43 | + # Fallback paths |
| 44 | + paths.extend([ |
| 45 | + os.path.expandvars(r'%ProgramFiles(x86)%\Steam'), |
| 46 | + os.path.expandvars(r'%ProgramFiles%\Steam'), |
| 47 | + ]) |
| 48 | + else: |
| 49 | + # Linux and other Unix-like systems |
| 50 | + paths.extend([ |
| 51 | + os.path.expanduser('~/.steam/steam'), |
| 52 | + os.path.expanduser('~/.steam'), |
| 53 | + os.path.expanduser('~/.local/share/Steam'), |
| 54 | + ]) |
| 55 | + |
| 56 | + return paths |
| 57 | + |
| 58 | + |
| 59 | +def find_libraryfolders_vdf(steam_path): |
| 60 | + """ |
| 61 | + Finds the libraryfolders.vdf file given a Steam path. |
| 62 | + Returns the path if found, None otherwise. |
| 63 | + """ |
| 64 | + # Handle symlinks (common on Linux where ~/.steam/steam -> ~/.local/share/Steam) |
| 65 | + if os.path.islink(steam_path): |
| 66 | + steam_path = os.path.realpath(steam_path) |
| 67 | + |
| 68 | + possible_locations = [ |
| 69 | + os.path.join(steam_path, 'steamapps', 'libraryfolders.vdf'), |
| 70 | + os.path.join(steam_path, 'steam', 'steamapps', 'libraryfolders.vdf'), |
| 71 | + os.path.join(steam_path, 'Steam', 'steamapps', 'libraryfolders.vdf'), |
| 72 | + ] |
| 73 | + |
| 74 | + for path in possible_locations: |
| 75 | + if os.path.isfile(path): |
| 76 | + return path |
| 77 | + |
| 78 | + return None |
45 | 79 |
|
46 | 80 |
|
47 | 81 | def is_installed_win(app_id): |
48 | 82 | """ |
49 | | - Gets whether an app with the given app id is installed, on Windows |
| 83 | + Gets whether an app with the given app id is installed, on Windows. |
50 | 84 | :param app_id: app_id to check |
51 | 85 | :return: True if the app is installed, false otherwise |
52 | 86 | """ |
53 | 87 | try: |
54 | | - app = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam\\Apps\\" + app_id) |
55 | | - print(winreg.QueryInfoKey(app)[1]) |
| 88 | + app = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\Valve\Steam\Apps\{}'.format(app_id)) |
56 | 89 | for i in range(winreg.QueryInfoKey(app)[1]): |
57 | | - name, value, type = winreg.EnumValue(app, i) |
58 | | - if name == "Installed": |
| 90 | + name, value, _ = winreg.EnumValue(app, i) |
| 91 | + if name == 'Installed': |
| 92 | + winreg.CloseKey(app) |
59 | 93 | return value == 1 |
60 | | - |
| 94 | + winreg.CloseKey(app) |
61 | 95 | except WindowsError: |
62 | 96 | pass |
63 | | - # Sometimes the key "Installed" does not exist, and we get out of the loop without returning anything,so we return False at the end of the function |
64 | 97 | return False |
65 | 98 |
|
66 | 99 |
|
67 | | -def get_installed_steam_apps(registry_path): |
| 100 | +def get_installed_steam_apps(steam_path): |
68 | 101 | """ |
69 | | - Obtains the steam games/apps installed on the computer. |
70 | | - :param registry_path: Path to the registry.vdf file |
71 | | - :return: an array of appids that are installed. |
| 102 | + Obtains the Steam games/apps installed on the computer. |
| 103 | + :param steam_path: Path to the Steam folder (from settings, or auto-detected) |
| 104 | + :return: a list of appids that are installed. |
72 | 105 | """ |
73 | 106 | installed_apps = [] |
74 | 107 |
|
| 108 | + # Build list of paths to try: user setting first, then defaults |
| 109 | + paths_to_try = [] |
| 110 | + if steam_path and os.path.isdir(steam_path): |
| 111 | + paths_to_try.append(steam_path) |
| 112 | + paths_to_try.extend(get_default_steam_paths()) |
| 113 | + |
| 114 | + # Windows: Try registry first |
75 | 115 | if os.name == 'nt': |
76 | 116 | try: |
77 | | - apps = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam\\Apps") |
78 | | - print(winreg.QueryInfoKey(apps)[0]) |
79 | | - for i in range(winreg.QueryInfoKey(apps)[0]): |
| 117 | + apps = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\Valve\Steam\Apps') |
| 118 | + num_apps = winreg.QueryInfoKey(apps)[0] |
| 119 | + log("Found {} apps in Windows registry".format(num_apps)) |
| 120 | + for i in range(num_apps): |
80 | 121 | app_id = winreg.EnumKey(apps, i) |
81 | 122 | if is_installed_win(app_id): |
82 | 123 | installed_apps.append(app_id) |
| 124 | + winreg.CloseKey(apps) |
| 125 | + return installed_apps |
| 126 | + except WindowsError: |
| 127 | + log("Windows registry method failed, falling back to libraryfolders.vdf") |
83 | 128 |
|
84 | | - except WindowsError as e: |
85 | | - show_error(e, "Error while reading Windows registry") |
86 | | - pass |
87 | | - else: |
88 | | - with io.open(registry_path, 'r', encoding="utf-8") as file: |
| 129 | + # Try libraryfolders.vdf method (all platforms) |
| 130 | + for path in paths_to_try: |
| 131 | + libraryfolders_path = find_libraryfolders_vdf(path) |
| 132 | + if libraryfolders_path: |
89 | 133 | try: |
90 | | - vdf = vdf_parse(file, {}) |
91 | | - apps = vdf['registry']['hkcu']['software']['valve']['steam']['apps'] |
| 134 | + with open(libraryfolders_path, 'r', encoding='utf-8') as f: |
| 135 | + data = vdf.load(f) |
| 136 | + |
| 137 | + libraryfolders = data.get('libraryfolders', {}) |
| 138 | + |
| 139 | + # Each library folder entry (0, 1, 2, etc.) contains an 'apps' dict |
| 140 | + for folder_id, folder_info in libraryfolders.items(): |
| 141 | + if isinstance(folder_info, dict) and 'apps' in folder_info: |
| 142 | + installed_apps.extend(folder_info['apps'].keys()) |
| 143 | + |
| 144 | + log("Found {} installed apps via {}".format(len(installed_apps), libraryfolders_path)) |
| 145 | + return installed_apps |
92 | 146 |
|
93 | | - # apparently case of 'installed' differs depending on ... ? |
94 | | - # We create a list of the apps that have a "installed" key equal to "1". |
95 | | - installed_apps = [appid for (appid, information) in apps.items() if (information.get('installed', '0') == '1')] |
96 | | - except KeyError as e: |
97 | | - show_error(e, "Error finding the values from registry.vdf") |
98 | | - pass |
| 147 | + except (SyntaxError, IOError, KeyError) as e: |
| 148 | + log("Error reading {}: {}".format(libraryfolders_path, e)) |
| 149 | + continue |
99 | 150 |
|
| 151 | + show_error( |
| 152 | + FileNotFoundError("libraryfolders.vdf not found"), |
| 153 | + "Could not find Steam library folders. Please check your Steam path setting." |
| 154 | + ) |
100 | 155 | return installed_apps |
0 commit comments