906 lines
34 KiB
Python
906 lines
34 KiB
Python
#!/usr/bin/env python
|
|
from steamctl.clients import CachingSteamClient
|
|
import sys
|
|
import json
|
|
import requests
|
|
from pathlib import Path
|
|
from argparse import Namespace
|
|
from io import StringIO
|
|
import winreg
|
|
|
|
steamClient = False
|
|
userdata = None
|
|
|
|
class App:
|
|
"""
|
|
A class representing an app added to Steam.
|
|
|
|
This information is pulled from the shortcuts.vdf file with the
|
|
#parseShortcuts method.
|
|
|
|
Attributes
|
|
----------
|
|
appId : int
|
|
The ID of the application as represented in steam
|
|
name : str
|
|
The display name of the app
|
|
executable : str
|
|
The link to the executable file.
|
|
start : str, Path
|
|
The path to start the app in
|
|
icon : str
|
|
The path to the icon file that Steam uses. If empty, then the icon of the executable is used in Windows installations.
|
|
shortcut : str
|
|
options : str
|
|
Command-line arguments to use when launching the executable.
|
|
hidden : bool
|
|
Whether or not the app is hidden from the client. Defaults to false.
|
|
desktopConfig : bool
|
|
overlay : bool
|
|
Whether or not the steam overlay can display on this game.
|
|
vr : bool
|
|
Whether or not the app is VR enabled.
|
|
devkit : bool
|
|
devkitId: str
|
|
playTime: long
|
|
The date that the application was last launched, in seconds since January 1, 1970 Universal Time
|
|
"""
|
|
|
|
appIndx = 0
|
|
appId = 0 # appid
|
|
name = "" # AppName
|
|
executable = "" # Exe
|
|
start = "" # StartDir
|
|
icon = "" # icon
|
|
shortcut = "" # ShortcutPath
|
|
options = "" # LaunchOptions
|
|
hidden = False # IsHidden
|
|
desktopConfig = True # AllowDesktopConfig
|
|
overlay = True # AllowOverlay
|
|
vr = False # OpenVR
|
|
devkit = False # Devkit
|
|
devkitId = "" # DevkitGameID
|
|
devkitOverride = False # DevkitOverrideAppID
|
|
playTime = 0 # LastPlayTime
|
|
#tags = ""
|
|
|
|
def __init__(self, appId):
|
|
self.appId = appId
|
|
|
|
def __repr__(self):
|
|
return "App %s (%s at %s)" % (self.appId, self.name, self.executable)
|
|
|
|
def __hash__(self):
|
|
return self.appId
|
|
|
|
def __eq__(self, other):
|
|
if self.appId != other.appId:
|
|
return False
|
|
if self.name != other.name:
|
|
return False
|
|
if self.executable != other.executable:
|
|
return False
|
|
if self.start != other.start:
|
|
return False
|
|
if self.icon != other.icon:
|
|
return False
|
|
if self.shortcut != other.shortcut:
|
|
return False
|
|
if self.options != other.options:
|
|
return False
|
|
if self.hidden != other.hidden:
|
|
return False
|
|
if self.desktopConfig != other.desktopConfig:
|
|
return False
|
|
if self.overlay != other.overlay:
|
|
return False
|
|
if self.vr != other.vr:
|
|
return False
|
|
if self.devkit != other.devkit:
|
|
return False
|
|
if self.devkitId != other.devkitId:
|
|
return False
|
|
if self.devkitOverride != other.devkitOverride:
|
|
return False
|
|
if self.playTime != other.playTime:
|
|
return False
|
|
return True
|
|
|
|
def parseShortcuts(shortcutFile):
|
|
"""Parses the "userdata/<userId>/config/shortcuts.vdf" file.
|
|
|
|
This file contains all the information about what non-steam apps are
|
|
installed in the steam client. This method parses the bytecode and returns
|
|
the apps obtained.
|
|
|
|
Parameters
|
|
----------
|
|
shortcutFile : str, Path
|
|
The path to the shortcuts.vdf file to parse.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list of apps contained within the shortcut file.
|
|
"""
|
|
|
|
data = []
|
|
with open(shortcutFile, 'rb') as shortcutData:
|
|
rawData = shortcutData.read()
|
|
i = 0
|
|
buffer = bytearray()
|
|
appIndx = 0
|
|
app = None
|
|
while i < len(rawData):
|
|
buffer.append(rawData[i])
|
|
if rawData[i] == 0 and len(buffer) > 1 or rawData[i] == 8:
|
|
try:
|
|
buffCont = buffer.decode()
|
|
buffCont = buffCont[:-1]
|
|
except UnicodeDecodeError:
|
|
#print(rawData[:i])
|
|
#print("Clearing buffer %s after %d" % (buffer[:-1].decode(), buffer[-1]))
|
|
buffer.clear()
|
|
buffCont = ""
|
|
found = True
|
|
try:
|
|
if buffCont.endswith("shortcuts") or rawData[i] == 8:
|
|
buffer.clear()
|
|
i += 2
|
|
if rawData[i] != 8:
|
|
# We are done here if it is 8
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
appIndx = int.from_bytes(buffer, "little")
|
|
elif buffCont.endswith("appid"):
|
|
buffer.clear()
|
|
i += 1
|
|
# Some app names do have a 1 for one of their bits
|
|
while rawData[i] != 1:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app = App(int.from_bytes(buffer, "little"))
|
|
app.appIndx = appIndx
|
|
appIndx = 0
|
|
data.append(app)
|
|
elif buffCont.endswith("AppName"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 1:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.name = buffer[:-1].decode()
|
|
elif buffCont.endswith("Exe"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 1:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.executable = buffer[:-1].decode()
|
|
elif buffCont.endswith("StartDir"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 1:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.start = buffer[:-1].decode()
|
|
elif buffCont.endswith("icon"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 1:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.icon = buffer[:-1].decode()
|
|
elif buffCont.endswith("ShortcutPath"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 1:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.shortcut = buffer[:-1].decode()
|
|
elif buffCont.endswith("LaunchOptions"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.options = buffer[:-1].decode()
|
|
elif buffCont.endswith("IsHidden"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.hidden = bool.from_bytes(buffer, "little")
|
|
elif buffCont.endswith("AllowDesktopConfig"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.desktopConfig = bool.from_bytes(buffer, "little")
|
|
elif buffCont.endswith("AllowOverlay"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.overlay = bool.from_bytes(buffer, "little")
|
|
elif buffCont.endswith("OpenVR"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.vr = bool.from_bytes(buffer, "little")
|
|
elif buffCont.endswith("Devkit"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 1:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.devkit = bool.from_bytes(buffer, "little")
|
|
elif buffCont.endswith("DevkitGameID"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.devkitId = buffer[:-1].decode()
|
|
elif buffCont.endswith("DevkitOverrideAppID"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 2:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.devkitOverride = bool.from_bytes(buffer, "little")
|
|
elif buffCont.endswith("LastPlayTime"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 116: # The TAGS thing seems to delimit this
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
app.playTime = int.from_bytes(buffer, "little")
|
|
i -= 1
|
|
elif buffCont.endswith("tags"):
|
|
buffer.clear()
|
|
i += 1
|
|
while rawData[i] != 8:
|
|
buffer.append(rawData[i])
|
|
i += 1
|
|
#app.tags = buffer.decode()
|
|
|
|
else:
|
|
found = False
|
|
buffer.clear()
|
|
except:
|
|
print("Exception when parsing!", file=sys.stderr)
|
|
print("Buffer data: %s + %d" % (buffCont, len(buffer)), file=sys.stderr)
|
|
print("Index %d / %d" % (i, len(rawData)), file=sys.stderr)
|
|
raise
|
|
#print('\r' + buffCont, end='')
|
|
if found:
|
|
#try:
|
|
#print(": %s" % buffer.decode())
|
|
#except UnicodeDecodeError:
|
|
#print(": %s" % int.from_bytes(buffer, "little"))
|
|
buffer.clear()
|
|
i += 1
|
|
return data
|
|
|
|
def writeShortcuts(apps, shortcutFile):
|
|
"""Writes app data to the shortcut file.
|
|
|
|
The shortcut file will be completely overwritten. As such, we can take the
|
|
opporunity to add whatever apps we want.
|
|
|
|
Parameters
|
|
----------
|
|
apps : list
|
|
A list of apps to write.
|
|
shortcutFile : str, Path
|
|
The path to the shortcuts.vdf file to write. It will be created if
|
|
needed.
|
|
"""
|
|
|
|
encoding = "utf-8"
|
|
data = bytearray([0x00])
|
|
data += bytearray("shortcuts", encoding)
|
|
data.append(0x00);
|
|
i = 1
|
|
for app in apps:
|
|
data.append(0x00)
|
|
if app.appIndx > 0 and app.appIndx < 256:
|
|
data.append(app.appIndx)
|
|
else:
|
|
data.append(i)
|
|
i += 1
|
|
data += bytearray([0x00, 0x02])
|
|
data += bytearray("appid", encoding)
|
|
data.append(0x00)
|
|
data += int(app.appId).to_bytes(4, "little")
|
|
data.append(0x01)
|
|
data += bytearray("AppName", encoding)
|
|
data.append(0x00)
|
|
data += bytearray(app.name, encoding)
|
|
data.append(0x00)
|
|
data.append(0x01)
|
|
data += bytearray("Exe", encoding)
|
|
data.append(0x00)
|
|
path = app.executable
|
|
if not path is str:
|
|
path = str(path)
|
|
if path[0] != '"':
|
|
# Wrap the path in quotes as needed
|
|
data += bytearray("\"", encoding)
|
|
data += bytearray(path, encoding)
|
|
if path[-1] != '"':
|
|
data += bytearray("\"", encoding)
|
|
data.append(0x00)
|
|
data.append(0x01)
|
|
data += bytearray("StartDir", encoding)
|
|
data.append(0x00)
|
|
path = app.start
|
|
if not path is str:
|
|
path = str(path)
|
|
if path[0] != '"':
|
|
data += bytearray("\"", encoding)
|
|
data += bytearray(app.start, encoding)
|
|
if path[-1] != '"':
|
|
data += bytearray("\"", encoding)
|
|
data.append(0x00)
|
|
data.append(0x01)
|
|
data += bytearray("icon", encoding)
|
|
data.append(0x00)
|
|
data += bytearray(app.icon, encoding)
|
|
data.append(0x00)
|
|
data.append(0x01)
|
|
data += bytearray("ShortcutPath", encoding)
|
|
data.append(0x00)
|
|
data += bytearray(app.shortcut, encoding)
|
|
data.append(0x00)
|
|
data.append(0x01)
|
|
data += bytearray("LaunchOptions", encoding)
|
|
data.append(0x00)
|
|
data += bytearray(app.options, encoding)
|
|
data.append(0x00)
|
|
data.append(0x02)
|
|
data += bytearray("IsHidden", encoding)
|
|
data.append(0x00)
|
|
data += bool(app.hidden).to_bytes(4, "little")
|
|
data.append(0x02)
|
|
data += bytearray("AllowDesktopConfig", encoding)
|
|
data.append(0x00)
|
|
data += bool(app.desktopConfig).to_bytes(4, "little")
|
|
data.append(0x02)
|
|
data += bytearray("AllowOverlay", encoding)
|
|
data.append(0x00)
|
|
data += bool(app.overlay).to_bytes(4, "little")
|
|
data.append(0x02)
|
|
data += bytearray("OpenVR", encoding)
|
|
data.append(0x00)
|
|
data += bool(app.vr).to_bytes(4, "little")
|
|
data.append(0x02)
|
|
data += bytearray("Devkit", encoding)
|
|
data.append(0x00)
|
|
data += bool(app.devkit).to_bytes(4, "little")
|
|
data.append(0x01)
|
|
data += bytearray("DevkitGameID", encoding)
|
|
data.append(0x00)
|
|
data += bytearray(app.devkitId, encoding)
|
|
data.append(0x00)
|
|
data.append(0x02)
|
|
data += bytearray("DevkitOverrideAppID", encoding)
|
|
data.append(0x00)
|
|
data += bool(app.devkitOverride).to_bytes(4, "little")
|
|
data.append(0x02)
|
|
data += bytearray("LastPlayTime", encoding)
|
|
data.append(0x00)
|
|
data += int(app.playTime).to_bytes(4, "little")
|
|
data.append(0x00)
|
|
data += bytearray("tags", encoding)
|
|
data.append(0x00)
|
|
data.append(0x08)
|
|
data.append(0x08)
|
|
#data += bytearray(app.tags, encoding)
|
|
data.append(0x08)
|
|
data.append(0x08)
|
|
with open(shortcutFile, 'wb') as stream:
|
|
stream.write(data)
|
|
|
|
def getSteamArgs():
|
|
"""Logs into the steam client as an anonymous user. This allows us to
|
|
download information on apps not installed.
|
|
|
|
Returns
|
|
-------
|
|
The interface to Steam.
|
|
"""
|
|
|
|
global steamClient
|
|
if not steamClient:
|
|
args = Namespace()
|
|
args.user = None
|
|
args.anonymous = True
|
|
args.skip_licenses = True
|
|
steamClient = CachingSteamClient()
|
|
print("Logging into steam")
|
|
steamClient.login_from_args(args)
|
|
return steamClient
|
|
|
|
def getHeader(code):
|
|
"""Obtains the URL to the header image of a game.
|
|
|
|
Parameters
|
|
----------
|
|
code : int, str
|
|
The app ID of the game to get the header for.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The URL to the game's header image.
|
|
"""
|
|
|
|
return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/header.jpg" % code
|
|
|
|
def getLibraryTall(code):
|
|
"""Obtains the URL to the box art image of a game.
|
|
|
|
Parameters
|
|
----------
|
|
code : int, str
|
|
The app ID of the game to get the box art for.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The URL to the game's box art image.
|
|
"""
|
|
|
|
return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/library_600x900.jpg" % code
|
|
|
|
def getBackground(code):
|
|
"""Obtains the URL to the page background image of a game.
|
|
|
|
Parameters
|
|
----------
|
|
code : int, str
|
|
The app ID of the game to get the background for.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The URL to the game's page background image.
|
|
"""
|
|
|
|
return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/library_hero.jpg" % code
|
|
|
|
def getLogo(code):
|
|
"""Obtains the URL to the logo image of a game.
|
|
|
|
Parameters
|
|
----------
|
|
code : int, str
|
|
The app ID of the game to get the logo for.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The URL to the game's logo image.
|
|
"""
|
|
|
|
return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/logo.png" % code
|
|
|
|
def getGameInfo(code):
|
|
"""Deprecated"""
|
|
fromContent = requests.get(fromURL, allow_redirects=True)
|
|
return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/logo.png" % code
|
|
|
|
def copyContent(name, content, byte=True):
|
|
"""
|
|
Adds a file to all users.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
The name of the file to add.
|
|
content : str, list
|
|
The content to write to the file.
|
|
byte : bool, optional
|
|
Whether or not to write in bytenode. Defaults to true.
|
|
"""
|
|
|
|
for user in userdata.iterdir():
|
|
toFile = Path(user, name)
|
|
if not toFile.parent.is_dir():
|
|
toFile.parent.mkdir(parents=True)
|
|
print("Writing to %s" % (toFile))
|
|
mode = 'w'
|
|
if byte:
|
|
mode += 'b'
|
|
with open(toFile, mode) as stream:
|
|
stream.write(content)
|
|
|
|
def copyFile(fromURL, toCode):
|
|
"""Downloads an image from one game on Steam and registers it under another
|
|
game.
|
|
|
|
This essentially just downloads the image and stores it under the
|
|
appropriate name for the Steam client to find.
|
|
|
|
Parameters
|
|
----------
|
|
fromURL : str, URL
|
|
The URL of the image to download. The name of the file must be
|
|
"header.jpg," "library_600x900.jpg," "library_hero.jpg," or "logo.png."
|
|
toCode : int
|
|
The app ID of the game to register the images to.
|
|
"""
|
|
|
|
fromURLComp = fromURL.split('/')
|
|
fromName = fromURLComp[6]
|
|
fromCode = fromURLComp[5]\
|
|
|
|
# Gets the final file name
|
|
if fromName == "library_hero.jpg":
|
|
toName = "_hero.jpg"
|
|
elif fromName == "library_600x900.jpg":
|
|
toName = "p.jpg"
|
|
elif fromName == "header.jpg":
|
|
toName = ".jpg"
|
|
else:
|
|
toName = "_" + fromName
|
|
toName = toCode + toName
|
|
|
|
print("Copying %s (%s) to %s" % (fromName, fromCode, toName))
|
|
fromContent = requests.get(fromURL, allow_redirects=True)
|
|
|
|
copyContent(Path("config", "grid", toName), fromContent.content)
|
|
|
|
def copyContents(ids):
|
|
"""Goes through a list of app ID mappings, downloading information on
|
|
"from" titles and mapping it to "to" titles, before writting that
|
|
information to the disk for the steam client to find.
|
|
|
|
Parameters
|
|
----------
|
|
ids : list
|
|
A two-dimensional list of numbers, where the first index of each
|
|
sub-list contains the app to download information from, and the second
|
|
is the ID of the app to store that information under, or App objects
|
|
"""
|
|
|
|
client = getSteamArgs()
|
|
fromCodes = []
|
|
toCodes = []
|
|
reverse = {}
|
|
apps = {}
|
|
for id in ids:
|
|
# Get the mappings
|
|
fromCode = id[0]
|
|
toCode = id[1]
|
|
if isinstance(toCode, App):
|
|
apps[toCode.appId] = toCode
|
|
toCode = toCode.appId
|
|
reverse[toCode] = fromCode
|
|
fromCodes.append(fromCode)
|
|
toCodes.append(toCode)
|
|
print("Copying %s to %s" % (fromCode, toCode))
|
|
# Copy the appropriate images from online
|
|
copyFile(getHeader(str(fromCode)), str(toCode))
|
|
copyFile(getLibraryTall(str(fromCode)), str(toCode))
|
|
copyFile(getBackground(str(fromCode)), str(toCode))
|
|
copyFile(getLogo(str(fromCode)), str(toCode))
|
|
# Obtains information on the apps to download from, notably logo positions
|
|
# and such.
|
|
data = client.get_product_info(apps=fromCodes)
|
|
i = 0
|
|
for i in range(len(ids)):
|
|
if not ids[i][0] in data["apps"]:
|
|
print("Could not find app ID %s for %s" % (ids[i][0], ids[i][1]), file=sys.stderr)
|
|
continue
|
|
# Generate the positioning data for logos and such
|
|
assets = data["apps"][ids[i][0]]
|
|
if not "common" in assets:
|
|
print("Could not find common data for %s from %s" % (ids[i], assets), file=sys.stderr)
|
|
continue
|
|
if not "library_assets" in assets["common"]:
|
|
print("Could not find common data for %s from %s" % (ids[i], assets), file=sys.stderr)
|
|
continue
|
|
if "logo_position" in assets["common"]["library_assets"]:
|
|
logo = assets["common"]["library_assets"]["logo_position"]
|
|
logo2 = {
|
|
"nVersion": 1,
|
|
"logoPosition": {
|
|
"pinnedPosition": logo["pinned_position"],
|
|
"nWidthPct": logo["width_pct"],
|
|
"nHeightPct": logo["height_pct"]
|
|
}
|
|
}
|
|
toCode = ids[i][1]
|
|
if isinstance(toCode, App):
|
|
toCode = toCode.appId
|
|
# Stores the logo positioning to file.
|
|
copyContent(Path("config", "grid", str(toCode) + ".json"), json.dumps(logo2), byte=False)
|
|
|
|
# Generate the data for other stuff.
|
|
logo2 = [[
|
|
"achievements", {
|
|
"version":2,
|
|
"data": {
|
|
"vecHighlight": [],
|
|
"vecUnachieved": [],
|
|
"vecAchievedHidden": [],
|
|
"nTotal": 0,
|
|
"nAchieved": 0
|
|
}
|
|
}
|
|
], [
|
|
"customimage", {
|
|
"version": 1,
|
|
"data": logo2["logoPosition"]
|
|
}
|
|
]
|
|
]
|
|
copyContent(Path("config", "librarycache", str(toCode) + ".json"), json.dumps(logo2), byte=False)
|
|
else:
|
|
print("No logo for %s %s" % (ids[i], json.dumps(assets, indent=4)), file=sys.stderr)
|
|
|
|
# Write the actual shortcut data.
|
|
for user in userdata.iterdir():
|
|
shortcutPath = Path(user, "config", "shortcuts.vdf")
|
|
if shortcutPath.is_file():
|
|
shortcutData = parseShortcuts(shortcutPath)
|
|
else:
|
|
shortcutData = []
|
|
toWrite = []
|
|
for app in apps:
|
|
toWrite.append(apps[app])
|
|
for app in shortcutData:
|
|
# Add existing apps back to the queue
|
|
if app.appId in apps:
|
|
eApp = apps[app.appId]
|
|
eApp.appIndx = app.appIndx or eApp.appIndx
|
|
eApp.name = eApp.name.strip() or app.name.strip()
|
|
eApp.executable = eApp.executable.strip() or app.executable.strip()
|
|
eApp.start = eApp.start.strip() or app.start.strip()
|
|
eApp.icon = eApp.icon.strip() or str(Path(user, "config", "grid", str(eApp.appId) + ".jpg")) or app.icon.strip()
|
|
eApp.shortcut = eApp.shortcut.strip() or app.shortcut.strip()
|
|
eApp.options = eApp.options.strip() or app.options.strip()
|
|
eApp.hidden = eApp.hidden or app.hidden
|
|
# The default is true. This sets it to whichever is false, if any
|
|
eApp.desktopConfig = not (not eApp.desktopConfig or not app.desktopConfig)
|
|
eApp.overlay = not (not eApp.overlay or not app.overlay)
|
|
eApp.vr = eApp.vr or app.vr
|
|
eApp.devKit = eApp.devkit or app.devkit
|
|
eApp.devKitId = eApp.devkitId or app.devkitId
|
|
eApp.devKitOverride = eApp.devkitOverride or app.devkitOverride
|
|
eApp.playTime = eApp.playTime or app.playTime
|
|
else:
|
|
toWrite.append(app)
|
|
writeShortcuts(toWrite, shortcutPath)
|
|
|
|
def printHelp():
|
|
print("""%s [OPTIONS] (<fromId> <toId>)...
|
|
Downloads information on one steam app and registers it under another.
|
|
%s [OPTIONS] <term>...
|
|
Searches for installed steam apps that match the provided search terms and prints information to the output.
|
|
|
|
ARGUMENTS
|
|
fromId
|
|
The game to change the client information for. This can be obtained from the steam client by going to the game properties and checking the "Updates" menu. It will be found under "App ID."
|
|
This must always be paired with toId
|
|
|
|
toId
|
|
The game to download client information about. This can be found by finding the game on www.steamdb.com and finding the App ID in the information table.
|
|
Note that the ID does not necessarily need to exist already. If an ID that is not within the list of shortcuts is added, a new one will be created and added to the client. In this case, using flags like --name, --target, and --dir must be used to add information about the executable manually.
|
|
This must always be paired with a preceding fromId.
|
|
|
|
term
|
|
Providing search terms has the application search for installed apps, printing information to the output. Use -a to just print all applications.
|
|
|
|
OPTIONS
|
|
-h, --help
|
|
Print this message
|
|
--name
|
|
The name a new app will be assigned.
|
|
--executable
|
|
The link to the application executable.
|
|
--start
|
|
The path the application will start in.
|
|
--options
|
|
Command-line options for when launching the executable.
|
|
--vr
|
|
Whether or not the app is VR enabled.
|
|
-a
|
|
Prints information on all installed apps.
|
|
-l
|
|
Prints detailed information found on applications found in the search.
|
|
-v
|
|
Verbose output
|
|
--args
|
|
Prints all arguments.
|
|
""" % (Path(__file__).name, Path(__file__).name))
|
|
|
|
if __name__ == "__main__":
|
|
keys = []
|
|
ids = []
|
|
ops = []
|
|
opArgs = {}
|
|
#for term in sys.argv[1:]:
|
|
# print(term)
|
|
# Gather the parameters
|
|
for term in sys.argv[1:]:
|
|
if not term.startswith('-'):
|
|
# All non-flag arguments are added to keys, whether or not they are
|
|
# a steam game ID.
|
|
keys.append(term)
|
|
# Append the ids
|
|
if term.isnumeric():
|
|
if len(ids) > 0 and type(ids[-1]) == int:
|
|
ids[-1] = (ids[-1], int(term))
|
|
else:
|
|
ids.append(int(term))
|
|
else:
|
|
# If we run into an argument that isn't either a flag or a steam
|
|
# id, then we must not have any IDs, and we are searching for a
|
|
# game.
|
|
ids = None
|
|
else:
|
|
if term[1] == '-':
|
|
if '=' in term:
|
|
ops.append(term[2:term.index('=')])
|
|
opArgs[ops[-1]] = term[term.index('=') + 1:]
|
|
if len(opArgs[ops[-1]]) > 0 and (opArgs[ops[-1]][0] == '"' and opArgs[ops[-1]][-1] == '"' or opArgs[ops[-1]][0] == '\'' and opArgs[ops[-1]][-1] == '\''):
|
|
# Remove quotation marks used to demark the argument
|
|
opArgs[ops[-1]] = opArgs[ops[-1]][1:-1]
|
|
else:
|
|
ops.append(term[2:])
|
|
if ops[-1] == "name" or ops[-1] == "executable" or ops[-1] == "icon" or ops[-1] == "start" or ops[-1] == "shortcut" or ops[-1] == "options" or ops[-1] == "hidden" or ops[-1] == "desktopConfig" or ops[-1] == "overlay" or ops[-1] == "vr" or ops[-1] == "devkit" or ops[-1] == "devkitId" or ops[-1] == "playTime" or ops[-1] == "tags":
|
|
if ids and len(ids) > 0 and type(ids[-1]) == tuple and (isinstance(ids[-1][1], App) or str(ids[-1][1]) == keys[-1]):
|
|
# If the previous key was the toCode for an app...
|
|
if type(ids[-1][1]) == int:
|
|
# Convert it to an app
|
|
ids[-1] = (ids[-1][0], App(ids[-1][1]))
|
|
if ops[-1] == "name":
|
|
ids[-1][1].name = opArgs[ops[-1]]
|
|
elif ops[-1] == "executable":
|
|
ids[-1][1].executable = "\"" + str(Path(opArgs[ops[-1]]).resolve()) + "\""
|
|
elif ops[-1] == "start":
|
|
ids[-1][1].start = "\"" + str(Path(opArgs[ops[-1]]).resolve()) + "\""
|
|
elif ops[-1] == "icon":
|
|
ids[-1][1].icon = opArgs[ops[-1]]
|
|
elif ops[-1] == "shortcut":
|
|
ids[-1][1].shortcut = opArgs[ops[-1]]
|
|
elif ops[-1] == "options":
|
|
ids[-1][1].options = opArgs[ops[-1]]
|
|
elif ops[-1] == "hidden":
|
|
ids[-1][1].hidden = bool(opArgs[ops[-1]])
|
|
elif ops[-1] == "desktopConfig":
|
|
ids[-1][1].desktopConfig = bool(opArgs[ops[-1]])
|
|
elif ops[-1] == "overlay":
|
|
ids[-1][1].overlay = bool(opArgs[ops[-1]])
|
|
elif ops[-1] == "vr":
|
|
ids[-1][1].vr = bool(opArgs[ops[-1]])
|
|
elif ops[-1] == "devkit":
|
|
ids[-1][1].devkit = bool(opArgs[ops[-1]])
|
|
elif ops[-1] == "devkitId":
|
|
ids[-1][1].devkitId = opArgs[ops[-1]]
|
|
elif ops[-1] == "playTime":
|
|
ids[-1][1].playTime = int(opArgs[ops[-1]])
|
|
else:
|
|
print("App definition options must follow a toCode (%s followed %s, last id was %s)" % (term, keys[-1], ids[-1]), file=sys.stderr)
|
|
exit(1)
|
|
else:
|
|
for op in term[1:]:
|
|
ops.append(op)
|
|
|
|
if "args" in ops:
|
|
print("Keys:")
|
|
for key in keys:
|
|
print("\t%s" % key)
|
|
print("Flags:")
|
|
for op in ops:
|
|
if op in opArgs:
|
|
print("\t%s = %s" % (op, opArgs[op]))
|
|
else:
|
|
print("\t%s" % op)
|
|
print()
|
|
print("IDs:")
|
|
for appId in ids:
|
|
if type(appId) == int:
|
|
print("\t%d" % (appId))
|
|
else:
|
|
print("\t%s" % (appId,))
|
|
print()
|
|
|
|
if "h" in ops or "help" in ops:
|
|
printHelp()
|
|
exit(0)
|
|
|
|
if len(keys) < 1 and not "a" in ops:
|
|
print("%s [OPTIONS] (<fromId> <toId>)..." % __file__)
|
|
print("%s [OPTIONS] <term>..." % __file__)
|
|
exit(1)
|
|
|
|
# Find the steam installation directory.
|
|
try:
|
|
import winreg
|
|
hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\WOW6432Node\Valve\Steam")
|
|
if hkey:
|
|
userdata = Path(winreg.QueryValueEx(hkey, "InstallPath")[0], "userdata")
|
|
except:
|
|
hkey = None
|
|
if not userdata:
|
|
userdata = Path(Path.home(), ".steam", "root", "userdata")
|
|
if not userdata:
|
|
print("Could not find Steam installation directory.", file=sys.stderr)
|
|
exit(1)
|
|
|
|
if ids:
|
|
# In this event, we make sure that all IDs have a mapping, by checking
|
|
# the last index.
|
|
if type(ids[-1]) == int:
|
|
print("Must have an even number of fromID - toId pairs")
|
|
exit(2)
|
|
|
|
copyContents(ids)
|
|
else:
|
|
tempApps = {}
|
|
apps = []
|
|
|
|
# Loop through all user folders
|
|
for user in userdata.iterdir():
|
|
# The shortcut file contain a collection of games that the client recognizes
|
|
shortcutData = parseShortcuts(Path(user, "config", "shortcuts.vdf"))
|
|
# Obtain
|
|
for app in shortcutData:
|
|
if type(app) != dict:
|
|
if "a" in ops:
|
|
# If the all flag was specified, then add all apps to
|
|
# the list
|
|
apps.append(app)
|
|
else:
|
|
for term in keys:
|
|
# Otherwise, record how many of the search terms
|
|
# appear in the app title.
|
|
if term.lower() in app.name.lower():
|
|
if not app in tempApps:
|
|
tempApps[app] = 1
|
|
else:
|
|
tempApps[app] += 1
|
|
if not "a" in ops:
|
|
# If we aren't listing all apps, find which apps are at least a 70%
|
|
# match.
|
|
for app in tempApps:
|
|
if tempApps[app] / len(keys) >= 0.7:
|
|
apps.append(app)
|
|
if len(apps) == 0:
|
|
if "v" in ops:
|
|
# Verbose output.
|
|
print("No matches found")
|
|
exit(0)
|
|
elif len(apps) == 1 and not "v" in ops and not "l" in ops:
|
|
# If there is only one match and we aren't looking for details, we
|
|
# just print the app ID and be done with it.
|
|
print(apps[0].appId)
|
|
else:
|
|
for app in apps:
|
|
print("%d - %s" % (app.appId, app.name))
|
|
if "l" in ops:
|
|
print("\tExecutable: %s" % app.executable)
|
|
print("\tRuntime Directory: %s" % app.start)
|
|
print("\tIcon: %s" % app.icon)
|
|
print("\tShortcut: %s" % app.shortcut)
|
|
print("\tRuntime Options: %s" % app.options)
|
|
print("\tIs Hidden: %s" % app.hidden)
|
|
print("\tDesktop Configuration: %s" % app.desktopConfig)
|
|
print("\tOverlay Enabled: %s" % app.overlay)
|
|
print("\tIs VR: %s" % app.vr)
|
|
print("\tDevkit: %s" % app.devkit)
|
|
print("\tDevkit ID: %s" % app.devkitId)
|
|
print("\tDevkit Override App ID: %s" % app.devkitOverride)
|
|
print("\tDate of last Play: %s" % app.playTime)
|