Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch SteamGridDB assets when adding game to Steam #3662

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
10 changes: 7 additions & 3 deletions bottles/backend/managers/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from bottles.backend.models.config import BottleConfig
from bottles.backend.utils import yaml

from bottles.backend.utils.manager import ManagerUtils
from bottles.backend.logger import Logger
from bottles.backend.globals import Paths
from bottles.backend.managers.steamgriddb import SteamGridDBManager
Expand Down Expand Up @@ -74,7 +74,10 @@ def add_to_library(self, data: dict, config: BottleConfig):
logging.info(f"Adding new entry to library: {_uuid}")

if not data.get("thumbnail"):
data["thumbnail"] = SteamGridDBManager.get_game_grid(data["name"], config)
grids_path = os.path.join(ManagerUtils.get_bottle_path(config), "grids")
data["thumbnail"] = SteamGridDBManager.get_steam_game_asset(
data["name"], grids_path
)

self.__library[_uuid] = data
self.save_library()
Expand All @@ -87,7 +90,8 @@ def download_thumbnail(self, _uuid: str, config: BottleConfig):
return False

data = self.__library.get(_uuid)
value = SteamGridDBManager.get_game_grid(data["name"], config)
os.path.join(ManagerUtils.get_bottle_path(config), "grids")
value = SteamGridDBManager.get_steam_game_asset(data["name"], config)

if not value:
return False
Expand Down
78 changes: 59 additions & 19 deletions bottles/backend/managers/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@
import shlex
import shutil
import uuid
from binascii import crc32
from datetime import datetime
from functools import lru_cache
from glob import glob
from pathlib import Path
from typing import Union, Dict, Optional

from requests.exceptions import HTTPError, RequestException

from bottles.backend.globals import Paths
from bottles.backend.managers.steamgriddb import SteamGridDBManager
from bottles.backend.models.config import BottleConfig
from bottles.backend.models.result import Result
from bottles.backend.models.samples import Samples
Expand Down Expand Up @@ -520,22 +524,69 @@ def launch_app(prefix: str):
SignalManager.send(Signals.GShowUri, Result(data=uri))

def add_shortcut(self, program_name: str, program_path: str):
def __add_to_user_conf(conf: str) -> bool:
logging.info(f"Searching SteamGridDB for {program_name} assets…")
asset_suffixes = {
"grids": "p",
"hgrids": "",
"heroes": "_hero",
"logos": "_logo",
"icons": "_icon",
}
for asset_type, suffix in asset_suffixes.items():
base_filename = f"{appid}{suffix}"
asset_path = os.path.join(conf, "grid", base_filename)
try:
filename = SteamGridDBManager.get_steam_game_asset(
program_name, asset_path, asset_type, reraise_exceptions=True
)
except HTTPError:
# Usually missing asset (404), keep trying for the rest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment can be removed

continue
except:
# Unreachable host or issue saving files, nothing we can do
break
Comment on lines +546 to +548
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it'll be better to specify which exception(s).


if asset_type == "icons":
shortcut["icon"] = os.path.join(conf, "grid", filename)

s = asset_type[:-1] if asset_type != "heroes" else "hero"
logging.info(f"Added {s.capitalize()} asset ({filename})")

try:
with open(os.path.join(conf, "shortcuts.vdf"), "rb") as f:
_existing = vdf.binary_loads(f.read()).get("shortcuts", {})

_all = list(_existing.values()) + [shortcut]
_shortcuts = {"shortcuts": {str(i): s for i, s in enumerate(_all)}}

with open(os.path.join(conf, "shortcuts.vdf"), "wb") as f:
f.write(vdf.binary_dumps(_shortcuts))

except (OSError, IOError) as e:
logging.error(e)
return False

return True

logging.info(f"Adding shortcut for {program_name}")
cmd = "xdg-open"
args = "bottles:run/'{0}'/'{1}'"
args = f"bottles:run/'{self.config.Name}'/'{program_name}'"
appid = crc32(str.encode(self.config.Name + program_name)) | 0x80000000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0x80000000 should have an accompanying source/context


if self.userdata_path is None:
logging.warning("Userdata path is not set")
return Result(False)

confs = glob(os.path.join(self.userdata_path, "*/config/"))
shortcut = {
"appid": appid - 0x100000000,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0x100000000 should have an accompanying source/context

"AppName": program_name,
"Exe": cmd,
"StartDir": ManagerUtils.get_bottle_path(self.config),
"icon": ManagerUtils.extract_icon(self.config, program_name, program_path),
"ShortcutPath": "",
"LaunchOptions": args.format(self.config.Name, program_name),
"LaunchOptions": args,
"IsHidden": 0,
"AllowDesktopConfig": 1,
"AllowOverlay": 1,
Expand All @@ -547,22 +598,11 @@ def add_shortcut(self, program_name: str, program_path: str):
"tags": {"0": "Bottles"},
}

for c in confs:
_shortcuts = {}
_existing = {}

if os.path.exists(os.path.join(c, "shortcuts.vdf")):
with open(os.path.join(c, "shortcuts.vdf"), "rb") as f:
try:
_existing = vdf.binary_loads(f.read()).get("shortcuts", {})
except:
continue

_all = list(_existing.values()) + [shortcut]
_shortcuts = {"shortcuts": {str(i): s for i, s in enumerate(_all)}}
ok = False
for conf in confs:
ok |= __add_to_user_conf(conf)

with open(os.path.join(c, "shortcuts.vdf"), "wb") as f:
f.write(vdf.binary_dumps(_shortcuts))
if ok:
logging.info(f"Added shortcut for {program_name}")

logging.info(f"Added shortcut for {program_name}")
return Result(True)
return Result(ok)
59 changes: 36 additions & 23 deletions bottles/backend/managers/steamgriddb.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
#

import os
import uuid
from typing import Optional

import requests
from requests.exceptions import HTTPError, RequestException

from bottles.backend.logger import Logger
from bottles.backend.models.config import BottleConfig
Expand All @@ -27,31 +29,42 @@


class SteamGridDBManager:
@staticmethod
def get_game_grid(name: str, config: BottleConfig):
def get_steam_game_asset(
program_name: str,
asset_path: str,
asset_type: Optional[str] = None,
reraise_exceptions: bool = False,
) -> Optional[str]:
try:
res = requests.get(f"https://steamgrid.usebottles.com/api/search/{name}")
except:
return
# url = f"https://steamgrid.usebottles.com/api/search/{program_name}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this is commented?

url = f"http://127.0.0.1:8000/api/search/{program_name}"
if asset_type:
url = f"{url}/{asset_type}"
res = requests.get(url, timeout=5)
res.raise_for_status()
filename = SteamGridDBManager.__save_asset_to_steam(res.json(), asset_path)

if res.status_code == 200:
return SteamGridDBManager.__save_grid(res.json(), config)
except Exception as e:
if isinstance(e, HTTPError):
logging.warning(str(e))
else:
logging.error(str(e))
if reraise_exceptions:
raise

@staticmethod
def __save_grid(url: str, config: BottleConfig):
grids_path = os.path.join(ManagerUtils.get_bottle_path(config), "grids")
if not os.path.exists(grids_path):
os.makedirs(grids_path)
return filename

ext = url.split(".")[-1]
filename = str(uuid.uuid4()) + "." + ext
path = os.path.join(grids_path, filename)
@staticmethod
def __save_asset_to_steam(url: str, asset_path: str) -> str:
asset_dir = os.path.dirname(asset_path)
if not os.path.exists(asset_dir):
os.makedirs(asset_dir)

try:
r = requests.get(url)
with open(path, "wb") as f:
f.write(r.content)
except Exception:
return
res = requests.get(url)
res.raise_for_status()
ext = os.path.splitext(url)[-1]
asset_path += ext
with open(asset_path, "wb") as img:
img.write(res.content)
Comment on lines +63 to +68
Copy link
Member

@TheEvilSkeleton TheEvilSkeleton Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For increased readability, I'd avoid abbreviating variable names, i.e. use result as opposed to res, etc.


return f"grid:{filename}"
return os.path.basename(asset_path)
12 changes: 6 additions & 6 deletions build-aux/pypi-deps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ sources:
url: https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl
sha256: e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970
- type: file
url: https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
sha256: 90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b
url: https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
sha256: 8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15
only-arches:
- x86_64
- type: file
Expand All @@ -39,16 +39,16 @@ sources:
url: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl
sha256: 946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
- type: file
url: https://files.pythonhosted.org/packages/a0/6b/34e6904ac99df811a06e42d8461d47b6e0c9b86e2fe7ee84934df6e35f0d/orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
sha256: a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09
url: https://files.pythonhosted.org/packages/bb/f0/1d89c199aca0b00a35a5bd55f892093f25acc8c5a0334096d77a91c4d6a2/orjson-3.10.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
sha256: e1e91b90c0c26bd79593967c1adef421bcff88c9e723d49c93bb7ad8af80bc6b
only-arches:
- x86_64
- type: file
url: https://files.pythonhosted.org/packages/d3/5e/76a9d08b4b4e4583f269cb9f64de267f9aeae0dacef23307f53a14211716/pathvalidate-3.2.1-py3-none-any.whl
sha256: 9a6255eb8f63c9e2135b9be97a5ce08f10230128c4ae7b3e935378b82b22c4c9
- type: file
url: https://files.pythonhosted.org/packages/0e/44/192ede8c7f935643e4c8a56545fcac6ae1b8c50a77f54b2b1c4ab9fcae49/patool-3.0.0-py2.py3-none-any.whl
sha256: 928070d5f82a776534a290a52f4758e2c0dd9cd5a633e3f63f7270c8982833b8
url: https://files.pythonhosted.org/packages/df/cc/67b2a7ad09ae95ae789c0c731c057a19c69592e3b6c66d53fc3e39a51961/patool-3.0.1-py2.py3-none-any.whl
sha256: 0d712d479c90bd3798e8c713069a45b893122578cbaf05241e6183ba4267ada5
- type: file
url: https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl
sha256: 76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ chardet==5.2.0
requests[use_chardet_on_py3]==2.32.3
Markdown==3.7
icoextract==0.1.5
patool==3.0.0
patool==3.0.1
pathvalidate==3.2.1
FVS==0.3.4
orjson==3.10.7
orjson==3.10.9
pycairo==1.27.0
PyGObject==3.50.0
charset-normalizer==3.3.2
charset-normalizer==3.4.0
idna==3.10
urllib3==2.2.3
certifi==2024.8.30
Expand Down
Loading