Adds comments and documentation to the file.

This commit is contained in:
William Hubbard
2022-02-05 18:04:15 +00:00
committed by GitHub
parent 93c94e1742
commit 571f918c87

View File

@@ -10,6 +10,41 @@ from io import StringIO
steamClient = False steamClient = False
class App: 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.
tags: str
"""
appId = 0 appId = 0
name = "" name = ""
executable = "" executable = ""
@@ -69,6 +104,23 @@ class App:
return True return True
def parseShortcuts(shortcutFile): 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 = [] data = []
with open(shortcutFile, 'rb') as shortcutData: with open(shortcutFile, 'rb') as shortcutData:
rawData = shortcutData.read() rawData = shortcutData.read()
@@ -216,6 +268,20 @@ def parseShortcuts(shortcutFile):
return data return data
def writeShortcuts(apps, shortcutFile): 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" encoding = "utf-8"
data = bytearray([0x00]) data = bytearray([0x00])
for app in apps: for app in apps:
@@ -301,6 +367,14 @@ def writeShortcuts(apps, shortcutFile):
stream.write(data) stream.write(data)
def getSteamArgs(): 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 global steamClient
if not steamClient: if not steamClient:
args = Namespace() args = Namespace()
@@ -313,22 +387,88 @@ def getSteamArgs():
return steamClient return steamClient
def getHeader(code): 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 return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/header.jpg" % code
def getLibraryTall(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 return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/library_600x900.jpg" % code
def getBackground(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 return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/library_hero.jpg" % code
def getLogo(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 return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/logo.png" % code
def getGameInfo(code): def getGameInfo(code):
"""Deprecated"""
fromContent = requests.get(fromURL, allow_redirects=True) fromContent = requests.get(fromURL, allow_redirects=True)
return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/logo.png" % code return "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/logo.png" % code
def copyContent(name, content, byte=True): 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.
"""
userdata = Path(Path.home(), ".steam", "root", "userdata") userdata = Path(Path.home(), ".steam", "root", "userdata")
for user in userdata.iterdir(): for user in userdata.iterdir():
toFile = Path(user, name) toFile = Path(user, name)
@@ -340,6 +480,21 @@ def copyContent(name, content, byte=True):
stream.write(content) stream.write(content)
def copyFile(fromURL, toCode): 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('/') fromURLComp = fromURL.split('/')
fromName = fromURLComp[6] fromName = fromURLComp[6]
fromCode = fromURLComp[5] fromCode = fromURLComp[5]
@@ -361,23 +516,40 @@ def copyFile(fromURL, toCode):
copyContent(Path("config", "grid", toName), fromContent.content) copyContent(Path("config", "grid", toName), fromContent.content)
def copyContents(ids): 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.
"""
client = getSteamArgs() client = getSteamArgs()
fromCodes = [] fromCodes = []
toCodes = [] toCodes = []
reverse = {} reverse = {}
for id in ids: for id in ids:
# Get the mappings
fromCode = id[0] fromCode = id[0]
toCode = id[1] toCode = id[1]
reverse[toCode] = fromCode reverse[toCode] = fromCode
fromCodes.append(fromCode) fromCodes.append(fromCode)
toCodes.append(toCode) toCodes.append(toCode)
# Copy the appropriate images from online
copyFile(getHeader(str(fromCode)), str(toCode)) copyFile(getHeader(str(fromCode)), str(toCode))
copyFile(getLibraryTall(str(fromCode)), str(toCode)) copyFile(getLibraryTall(str(fromCode)), str(toCode))
copyFile(getBackground(str(fromCode)), str(toCode)) copyFile(getBackground(str(fromCode)), str(toCode))
copyFile(getLogo(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) data = client.get_product_info(apps=fromCodes)
i = 0 i = 0
for i in range(len(ids)): for i in range(len(ids)):
# Generate the positioning data for logos and such
logo = data["apps"][ids[i][0]]["common"]["library_assets"]["logo_position"] logo = data["apps"][ids[i][0]]["common"]["library_assets"]["logo_position"]
logo2 = { logo2 = {
"nversion": 1, "nversion": 1,
@@ -387,8 +559,10 @@ def copyContents(ids):
"nHeightPct": logo["height_pct"] "nHeightPct": logo["height_pct"]
} }
} }
# Stores the logo positioning to file.
copyContent(Path("config", "grid", str(ids[i][1]) + ".json"), json.dumps(logo2), byte=False) copyContent(Path("config", "grid", str(ids[i][1]) + ".json"), json.dumps(logo2), byte=False)
# Generate the data for other stuff.
logo2 = [[ logo2 = [[
"achievements", { "achievements", {
"version":2, "version":2,
@@ -410,6 +584,7 @@ def copyContents(ids):
copyContent(Path("config", "librarycache", str(ids[i][1]) + ".json"), json.dumps(logo2), byte=False) copyContent(Path("config", "librarycache", str(ids[i][1]) + ".json"), json.dumps(logo2), byte=False)
userdata = Path(Path.home(), ".steam", "root", "userdata") userdata = Path(Path.home(), ".steam", "root", "userdata")
# Write the actual shortcut data.
for user in userdata.iterdir(): for user in userdata.iterdir():
shortcutData = parseShortcuts(Path(user, "config", "shortcuts.vdf")) shortcutData = parseShortcuts(Path(user, "config", "shortcuts.vdf"))
for app in shortcutData: for app in shortcutData:
@@ -426,15 +601,22 @@ if __name__ == "__main__":
keys = [] keys = []
ids = [] ids = []
ops = [] ops = []
# Gather the parameters
for term in sys.argv[1:]: for term in sys.argv[1:]:
if not term.startswith('-'): if not term.startswith('-'):
# All non-flag arguments are added to keys, whether or not they are
# a steam game ID.
keys.append(term) keys.append(term)
# Append the ids
if term.isnumeric(): if term.isnumeric():
if len(ids) > 0 and type(ids[-1]) == int: if len(ids) > 0 and type(ids[-1]) == int:
ids[-1] = (ids[-1], int(term)) ids[-1] = (ids[-1], int(term))
else: else:
ids.append(int(term)) ids.append(int(term))
else: 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 ids = None
else: else:
if term[1] == '-': if term[1] == '-':
@@ -447,6 +629,8 @@ if __name__ == "__main__":
print("%s [OPTIONS] <term>..." % __file__) print("%s [OPTIONS] <term>..." % __file__)
exit(1) exit(1)
if ids: if ids:
# In this event, we make sure that all IDs have a mapping, by checking
# the last index.
if type(ids[-1]) == int: if type(ids[-1]) == int:
print("Must have an even number of fromID - toId pairs") print("Must have an even number of fromID - toId pairs")
exit(2) exit(2)
@@ -455,29 +639,41 @@ if __name__ == "__main__":
else: else:
tempApps = {} tempApps = {}
apps = [] apps = []
# Find the steam installation directory.
userdata = Path(Path.home(), ".steam", "root", "userdata") userdata = Path(Path.home(), ".steam", "root", "userdata")
# Loop through all user folders
for user in userdata.iterdir(): for user in userdata.iterdir():
# The shortcut file contain a collection of games that the client recognizes
shortcutData = parseShortcuts(Path(user, "config", "shortcuts.vdf")) shortcutData = parseShortcuts(Path(user, "config", "shortcuts.vdf"))
for app in shortcutData: for app in shortcutData:
if type(app) != dict: if type(app) != dict:
if "a" in ops: if "a" in ops:
# If the all flag was specified, then add all apps to
# the list
apps.append(app) apps.append(app)
else: else:
for term in keys: 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 term.lower() in app.name.lower():
if not app in tempApps: if not app in tempApps:
tempApps[app] = 1 tempApps[app] = 1
else: else:
tempApps[app] += 1 tempApps[app] += 1
if not "a" in ops: 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: for app in tempApps:
if tempApps[app] / len(keys) >= 0.7: if tempApps[app] / len(keys) >= 0.7:
apps.append(app) apps.append(app)
if len(apps) == 0: if len(apps) == 0:
if "v" in ops: if "v" in ops:
# Verbose output.
print("No matches found") print("No matches found")
exit(0) exit(0)
elif len(apps) == 1 and not "v" in ops and not "l" in ops: 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) print(apps[0].appId)
else: else:
for app in apps: for app in apps: