init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
.venv
|
||||
@@ -0,0 +1 @@
|
||||
3.11
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[](https://asciinema.org/a/qfN1Nc4Sy7swPJVXUeWsmSDqx)
|
||||
@@ -0,0 +1,66 @@
|
||||
# CLI app to find Steam game paths with Compatdata
|
||||
from rich import print
|
||||
from prompt_toolkit import prompt
|
||||
from rich.console import Console
|
||||
from rich.columns import Columns
|
||||
from rich.panel import Panel
|
||||
import vdfparser as vd
|
||||
from prompt_helper import PromptHelper
|
||||
|
||||
class SteamGamePathTool:
|
||||
def __init__(self):
|
||||
self.steam_path = vd.fetch_steam_path()
|
||||
self.steam_vdf_path = vd.fetch_steam_vdf()
|
||||
|
||||
if self.steam_vdf_path is None:
|
||||
print("Steam VDF file not found")
|
||||
# Add user input to attempt to find
|
||||
self.steam_vdf_path = prompt("Enter the path to the Steam VDF file: ", default=self.steam_path)
|
||||
|
||||
self.steam_vdf = vd.parse_vdf(self.steam_vdf_path)
|
||||
self.steam_library_locations = vd.find_extra_locations(self.steam_vdf)
|
||||
|
||||
for library in self.steam_library_locations:
|
||||
print(f"[+] Found Steam library at: {library}")
|
||||
|
||||
games = vd.fetchall_vdfs(self.steam_vdf)
|
||||
games = self.sort_games(games)
|
||||
|
||||
console = Console()
|
||||
game_rend = [Panel(self.get_game_content(game), expand=True) for game in games]
|
||||
console.print(Columns(game_rend))
|
||||
|
||||
self.prompter = PromptHelper(games)
|
||||
self.prompt_user()
|
||||
|
||||
|
||||
def prompt_user(self):
|
||||
game = self.prompter.prompt_game(text="Enter a game (name | appid) to search for: ")
|
||||
if game is None:
|
||||
return
|
||||
console = Console()
|
||||
console.print(Panel(self.get_game_content(game), expand=True))
|
||||
|
||||
|
||||
def sort_games(self, games):
|
||||
return sorted(games, key=lambda x: x['name'])
|
||||
|
||||
def get_game_content(self, game):
|
||||
"""
|
||||
Takes a game dictionary and returns a string that formats game information into a string renderable by the console in the rich library.
|
||||
|
||||
:param game: A dictionary containing information about a Steam game
|
||||
:return: string formatted for table using rich library
|
||||
"""
|
||||
# Spaces must be replaced with %20 otherwise they won't link properly
|
||||
|
||||
string = f"""[b]{game['name']}[/b]
|
||||
[white]Game ID: [yellow]{game['appid']}
|
||||
[white]Game Size: [red]{int(game['SizeOnDisk'])/(1024*1024)/1024:.2f} GB[/red]
|
||||
[white]Game acf: [green][link=file://{game['acf_path'].replace(' ', '%20')}]file[/link][/green]
|
||||
[white]Game Path: [green][link=file://{game['true_path'].replace(' ', '%20')}]dir[/link][/green]"""
|
||||
if game['compatdata_path']:
|
||||
string += f"\n\t[white]Compatdata dir: [blue][link=file://{game['compatdata_path'].replace(' ', '%20')}]dir[/link][/blue]"
|
||||
return string
|
||||
if __name__ == "__main__":
|
||||
steam_path_tool = SteamGamePathTool()
|
||||
@@ -0,0 +1,29 @@
|
||||
import prompt_toolkit as pt
|
||||
from prompt_toolkit.completion import WordCompleter, FuzzyCompleter
|
||||
|
||||
def generate_completer(game_list):
|
||||
g_list = [x['name'] for x in game_list] + [x['appid'] for x in game_list]
|
||||
return FuzzyCompleter(WordCompleter(g_list))
|
||||
|
||||
class PromptHelper:
|
||||
def __init__(self, game_list):
|
||||
self.game_list = game_list
|
||||
self.completer = generate_completer(game_list)
|
||||
|
||||
def find_game_str(self, game_name):
|
||||
for game in self.game_list:
|
||||
if game['name'] == game_name:
|
||||
return game
|
||||
def find_game_num(self, appid: int):
|
||||
for game in self.game_list:
|
||||
if game['appid'] == appid:
|
||||
return game
|
||||
|
||||
def prompt_game(self, text, default="None"):
|
||||
response = pt.prompt(text, completer=self.completer, complete_while_typing=True)
|
||||
if response == "":
|
||||
return None
|
||||
elif response[0].isalpha():
|
||||
return self.find_game_str(response)
|
||||
else:
|
||||
return self.find_game_num(int(response))
|
||||
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "steamgamepath"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"prompt-toolkit>=3.0.51",
|
||||
"rich>=14.0.0",
|
||||
"vdf>=3.4",
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.51"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "steamgamepath"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "rich" },
|
||||
{ name = "vdf" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.51" },
|
||||
{ name = "rich", specifier = ">=14.0.0" },
|
||||
{ name = "vdf", specifier = ">=3.4" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vdf"
|
||||
version = "3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/7f/74192f47d67c8bf3c47bf0d8487b3457614c2c98d58b6617721d217f3f79/vdf-3.4.tar.gz", hash = "sha256:fd5419f41e07a1009e5ffd027c7dcbe43d1f7e8ef453aeaa90d9d04b807de2af", size = 11132 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/60/6456b687cf55cf60020dcd01f9bc51561c3cc84f05fd8e0feb71ce60f894/vdf-3.4-py2.py3-none-any.whl", hash = "sha256:68c1a125cc49e343d535af2dd25074e9cb0908c6607f073947c4a04bbe234534", size = 10357 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
|
||||
]
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import vdf
|
||||
|
||||
def parse_vdf(steam_vdf_path) -> dict:
|
||||
"""
|
||||
Reads the Steam vdf file at the given path and returns the corresponding dict.
|
||||
|
||||
:param steam_vdf_path: Path to the Steam vdf file
|
||||
:return: Dict representing the vdf data
|
||||
"""
|
||||
vdf_data = vdf.loads(open(steam_vdf_path, 'r').read())
|
||||
return vdf_data
|
||||
|
||||
def fetch_ids(vdf_json: dict):
|
||||
"""
|
||||
Prints all Steam app IDs from the given vdf data.
|
||||
|
||||
:param vdf_json: vdf data as a dict
|
||||
"""
|
||||
for library in vdf_json["libraryfolders"]:
|
||||
print(f"Library {library}: {vdf_json['libraryfolders'][library]['apps']}")
|
||||
for app_id in vdf_json['libraryfolders'][library]['apps']:
|
||||
print(f"{app_id}")
|
||||
print()
|
||||
|
||||
def fetch_steam_path() -> str:
|
||||
"""
|
||||
Determines the Steam installation path by checking common locations.
|
||||
|
||||
Returns the path to the Steam 'steamapps' directory.
|
||||
Prefers the path used by the apt installation if it exists.
|
||||
Otherwise, returns the path used by the Flatpak installation.
|
||||
|
||||
:return: Path to the Steam 'steamapps' directory
|
||||
"""
|
||||
|
||||
apt_steam_path = os.path.expanduser(r"~/.steam/steam/steamapps/")
|
||||
flatpak_steam_path = os.path.expanduser(r"~/.var/app/com.valvesoftware.Steam/data/Steam/steamapps/")
|
||||
return apt_steam_path if os.path.exists(apt_steam_path) else flatpak_steam_path
|
||||
|
||||
def fetch_steam_vdf() -> str | None:
|
||||
"""
|
||||
Determines the path to the Steam libraryfolders.vdf file by checking common locations.
|
||||
|
||||
Returns the path to the libraryfolders.vdf file if it exists.
|
||||
Otherwise, returns None.
|
||||
|
||||
:return: Path to the libraryfolders.vdf file or None
|
||||
"""
|
||||
steam_vdf_path = os.path.join(fetch_steam_path(), "libraryfolders.vdf")
|
||||
return steam_vdf_path if os.path.exists(steam_vdf_path) else None
|
||||
|
||||
def validate_steam_path(path) -> bool:
|
||||
if not os.path.exists(path):
|
||||
print(f"Steam path does not exist (could be not mounted): {path}")
|
||||
return False
|
||||
return True
|
||||
|
||||
# Scans vdf file for additional steam library paths... e.g. USB drive, external HDD
|
||||
def find_extra_locations(vdf_json):
|
||||
"""
|
||||
Scans the vdf_json for additional Steam library paths and validates them.
|
||||
|
||||
:param vdf_json: A dictionary representing the parsed Steam VDF data.
|
||||
:return: A list of valid paths to additional Steam library locations.
|
||||
"""
|
||||
|
||||
steam_library_locations = []
|
||||
for library in vdf_json["libraryfolders"]:
|
||||
path = vdf_json['libraryfolders'][library]['path']
|
||||
# print(path)
|
||||
if validate_steam_path(path):
|
||||
steam_library_locations.append(path)
|
||||
else:
|
||||
print(f"[-] Invalid Steam path: {path}")
|
||||
return steam_library_locations
|
||||
|
||||
# Reads manifest of individual game vdfs
|
||||
def read_game_vdf(gameID: int, steam_vdf_json: dict, path, game) -> dict:
|
||||
with open(os.path.join(path, game), 'r') as f:
|
||||
game_vdf = vdf.loads(f.read())
|
||||
return game_vdf['AppState']
|
||||
|
||||
def fetchall_vdfs(steam_vdf_json: dict):
|
||||
"""
|
||||
Reads all Steam game manifests in the given Steam VDF data and returns a list of dictionaries containing information about each game.
|
||||
|
||||
:param steam_vdf_json: A dictionary representing the parsed Steam VDF data.
|
||||
:return: A list of dictionaries containing information about each Steam game.
|
||||
"""
|
||||
games = []
|
||||
for library in steam_vdf_json["libraryfolders"]:
|
||||
path = steam_vdf_json['libraryfolders'][library]['path']
|
||||
steamapps = os.path.join(path, "steamapps")
|
||||
for game in os.listdir(steamapps):
|
||||
if game.endswith(".acf"):
|
||||
gameID = int(game.split('.')[0].split('_')[1])
|
||||
parsed_game = read_game_vdf(gameID, steam_vdf_json, steamapps, game)
|
||||
parsed_game['acf_path'] = os.path.join(steamapps, game)
|
||||
parsed_game['root_steam_folder'] = path
|
||||
parsed_game['true_path'] = os.path.join(steamapps, "common", parsed_game['installdir'])
|
||||
parsed_game['compatdata_path'] = os.path.join(steamapps, "compatdata", str(gameID)) if os.path.exists(os.path.join(steamapps, "compatdata", str(gameID))) else None
|
||||
games.append(parsed_game)
|
||||
# print("Game name:", parsed_game['name'], "ID:", gameID, "Path:", parsed_game['true_path'])
|
||||
return games
|
||||
Reference in New Issue
Block a user