Commit e96ebca9 authored by Yankovskiy Georgiy's avatar Yankovskiy Georgiy

Commits 918563db, a19804a8 refactor (part 1 of 2)

parent e9d90e00
...@@ -9,9 +9,11 @@ from typing import AnyStr, Union ...@@ -9,9 +9,11 @@ from typing import AnyStr, Union
from PySide6 import QtCore from PySide6 import QtCore
from os.path import expanduser from os.path import expanduser
from desktop_parser import DesktopFile from desktop_parser import DesktopFile
from platformdirs import user_cache_dir, user_config_dir
from ingame.models.Gamepad import Gamepad from ingame.models.Gamepad import Gamepad
from ingame.models.GamesModel import Game, GamesModel from ingame.models.GamesModel import GamesModel
from ingame.models.GameEntry import GameEntry
from ingame.models.GameAgent import GameAgent from ingame.models.GameAgent import GameAgent
from PySide6.QtCore import Property, Signal, Slot, QObject, Qt from PySide6.QtCore import Property, Signal, Slot, QObject, Qt
...@@ -24,11 +26,12 @@ class GameShortcut: ...@@ -24,11 +26,12 @@ class GameShortcut:
class App(QtCore.QObject): class App(QtCore.QObject):
app_name = "ingame"
app_author = "foss"
game_started = Signal(bool, name="gameStarted") game_started = Signal(bool, name="gameStarted")
game_ended = Signal(bool, name="gameEnded") game_ended = Signal(bool, name="gameEnded")
data_found = Signal(dict, name="gotGameData") data_found = Signal(dict, name="gotGameData")
gamepad_clicked_LB = Signal(bool, name="gamepadClickedLB") gamepad_clicked_LB = Signal(bool, name="gamepadClickedLB")
gamepad_clicked_RB = Signal(bool, name="gamepadClickedRB") gamepad_clicked_RB = Signal(bool, name="gamepadClickedRB")
gamepad_clicked_apply = Signal(bool, name="gamepadClickedApply") gamepad_clicked_apply = Signal(bool, name="gamepadClickedApply")
...@@ -38,9 +41,12 @@ class App(QtCore.QObject): ...@@ -38,9 +41,12 @@ class App(QtCore.QObject):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.games_model: GamesModel = GamesModel()
self.home: AnyStr = expanduser('~') self.home: AnyStr = expanduser('~')
self.config_location: str = '/.config/PortProton.conf' self.config_path = user_config_dir(App.app_name, App.app_author)
self.cache_path = user_cache_dir(App.app_name, App.app_author)
self.games_model: GamesModel = GamesModel()
self.portproton_config_location: str = '/.config/PortProton.conf'
self.portproton_location: str = '' self.portproton_location: str = ''
self.running_game_process: Union[subprocess.Popen, None] = None self.running_game_process: Union[subprocess.Popen, None] = None
...@@ -52,13 +58,13 @@ class App(QtCore.QObject): ...@@ -52,13 +58,13 @@ class App(QtCore.QObject):
self.gamepad.r_clicked = lambda: self.gamepad_axis_right.emit(True) self.gamepad.r_clicked = lambda: self.gamepad_axis_right.emit(True)
self.gamepad.back_clicked = lambda: self.gamepad_clicked_back.emit(True) self.gamepad.back_clicked = lambda: self.gamepad_clicked_back.emit(True)
self.agent = GameAgent() self.agent = GameAgent(self.config_path, self.cache_path)
self.setup() self.setup()
def setup(self): def setup(self):
try: try:
with open(self.home + self.config_location, 'r') as file: with open(self.home + self.portproton_config_location, 'r') as file:
self.portproton_location = file.read().strip() self.portproton_location = file.read().strip()
print(f'Current PortProton location: {self.portproton_location}') print(f'Current PortProton location: {self.portproton_location}')
...@@ -67,79 +73,73 @@ class App(QtCore.QObject): ...@@ -67,79 +73,73 @@ class App(QtCore.QObject):
for val in files: for val in files:
desktop_file = DesktopFile.from_file(val) desktop_file = DesktopFile.from_file(val)
data = desktop_file.data desktop_file_data = desktop_file.data
entry = data['Desktop Entry'] desktop_entry = desktop_file_data['Desktop Entry']
_name = entry['Name'] or 'generic' entry_name = desktop_entry['Name'] or 'generic'
_exec = 'Exec' in entry and entry['Exec'] or '' entry_exec = 'Exec' in desktop_entry and desktop_entry['Exec'] or ''
_icon = entry['Icon'] entry_icon = desktop_entry['Icon']
assert (isinstance(_name, str) assert (isinstance(entry_name, str)
and isinstance(_exec, str) and isinstance(entry_exec, str)
and isinstance(_icon, str)) and isinstance(entry_icon, str))
exec_split = _exec.split(' ') entry_exec_split = entry_exec.split(' ')
# Ignore extra non-related desktop entries # Ignore extra non-related desktop entries
if (len(exec_split) <= 1 or if (len(entry_exec_split) <= 1 or
('data/scripts/start.sh' not in exec_split[1] or '%F' in exec_split[-1])): ('data/scripts/start.sh' not in entry_exec_split[1] or '%F' in entry_exec_split[-1])):
continue continue
# TODO parse product name # TODO parse product name
_icon = (os.path.isfile(_icon) and _icon entry_icon = (os.path.isfile(entry_icon) and entry_icon) or ''
or os.path.realpath(f"{Path(__file__).resolve().parent}../../../qml/images/PUBG.png"))
# Автозапуск игры:
# PW_GUI_DISABLED_CS=1
# START_FROM_STEAM=1
# Remove extra env in the beginning # Remove extra env in the beginning
_exec = _exec[4:len(_exec)] entry_exec = f"env START_FROM_STEAM=1 {entry_exec[4:len(entry_exec)]}"
_exec = f"env START_FROM_STEAM=1 {_exec}"
self.games_model.add_game(Game(name=_name, icon=_icon, exec=_exec)) self.games_model.add_game(GameEntry(name=entry_name, icon=entry_icon, exec=entry_exec))
self.gamepad.run() self.gamepad.run()
self.retrieve_games_details()
except FileNotFoundError: except FileNotFoundError:
print('File not found') print('File not found')
except Exception as e: except Exception as e:
print('An error occurred', e) print('An error occurred', e)
pass pass
### CALLBACKS ### # TODO: refactor!
def retrieve_games_details(self):
def retrieve_games_details_thread(t):
game_entry: GameEntry
for game_entry in self.games_model.games_list:
game_description = t.agent.retrieve_game_description(game_entry.name)
game_entry.icon = game_description['image_location_path'] or game_entry.icon
thread = threading.Thread(target=retrieve_games_details_thread, args=(self,))
thread.start()
''' CALLBACKS '''
def close_event(self): def close_event(self):
# do stuff
# if can_exit:
self.gamepad.terminate() self.gamepad.terminate()
# event.accept() # let the window close
# else:
# event.ignore()
self.agent.clean_data() self.agent.clean_data()
# self.agent.save_db()
### SLOTS ### ''' SLOTS '''
@Slot(str, result=dict) @Slot(str, result=dict)
def get_game_data(self, game_name): def get_game_data(self, game_name):
#print(game_name)
def search_thread(t, name): def search_thread(t, name):
search_result = t.agent.retrieve_game_description(name)
search_result = t.agent.search_game(name)
t.data_found.emit(search_result) t.data_found.emit(search_result)
return return
thread = threading.Thread(target=search_thread, args=(self, game_name)) thread = threading.Thread(target=search_thread, args=(self, game_name))
thread.start() thread.start()
pass
@Slot(str) @Slot(str)
def start_game(self, exec): def start_game(self, _exec):
self.game_started.emit(True) self.game_started.emit(True)
def run_in_thread(t, _exec): def run_in_thread(t, _exec):
...@@ -152,18 +152,12 @@ class App(QtCore.QObject): ...@@ -152,18 +152,12 @@ class App(QtCore.QObject):
) )
t.running_game_process.wait() t.running_game_process.wait()
t.game_ended.emit(True) t.game_ended.emit(True)
# output = self.running_game_process.stdout.read()
# self.running_game_process.stdout.close()
return return
thread = threading.Thread(target=run_in_thread, args=(self, exec)) thread = threading.Thread(target=run_in_thread, args=(self, _exec))
thread.start() thread.start()
pass ''' PROPERTIES '''
### PROPERTIES ###
@Property(QObject, constant=True) @Property(QObject, constant=True)
def games(self): def games(self):
......
import os import os
import pickle import pickle
import requests
from steam_web_api import Steam from steam_web_api import Steam
from steamgrid import SteamGridDB
# TODO: from ingame.models.GameDescription import GameDescription
# [?] Определиться, используется ли Lutris. Если да, вместо этого будет обращение к нему. Если нет,
# продумать "рыбу" более логично.
# [?] Починить отображение системных требований (точнее, разобраться, что именно возвращает API.
# [done 1/2] Додумать форматированные данные, что именно мы видим на странице игры?
class GameAgent: class GameAgent:
generic_name = "Risk of rain 2" # generic_name = "Risk of rain 2"
datapath = ".agent-data" # scenario = 0
all_data = dict() # db_storage_path = ".agent-data"
scenario = 0 # data = dict()
def __init__(self): def __init__(self, config_path, cache_path):
super().__init__() super().__init__()
agent_key = "SOME_KEY_HERE_I_GUESS" # TODO: move API tokens to GUI settings tab / environmental variables
self.steam_process = Steam(agent_key) self.steam_grid_db_client = SteamGridDB('66827eabea66de47d036777ed2be87b2')
self.get_all_data() self.steam_client = Steam("SOME_KEY_HERE_I_GUESS")
self.config_path = config_path
self.cache_path = cache_path
self.db_storage_path = config_path + "/.agent-data"
self.steam_grid_db_images_path = cache_path + "/steam_grid_db_images"
self.data = dict()
os.makedirs(self.config_path, exist_ok=True, mode=0o755)
os.makedirs(self.steam_grid_db_images_path, exist_ok=True, mode=0o755)
self.load_db()
''' USAGE '''
def retrieve_game_description(self, game_name):
if game_name not in self.data:
# TODO: checkup for failed requests
search_results = self.steam_client.apps.search_games(game_name)
self.add_game_description(search_results, game_name)
game_description = self.data[game_name]
return game_description.as_dict()
def steam_grid_db_retrieve_image(self, game_name):
try:
save_path = f"{self.steam_grid_db_images_path}/{game_name}.png"
if os.path.exists(save_path):
return save_path
# TODO: checkup for failed requests
result = self.steam_grid_db_client.search_game(game_name)
grids = self.steam_grid_db_client.get_grids_by_gameid(list([result[0].id]))
# TODO: too slow, replace loop o(n) with o(1) if possible
for grid in grids:
if grid.height == 900 and grid.width == 600:
url_img = grid.url
response = requests.get(url_img)
with open(save_path, 'wb') as file:
file.write(response.content)
# return url_img
return save_path
return ''
except:
return ''
''' DATABASE '''
def add_game_description(self, search_results, game_name):
game_description = GameDescription()
# Steam game info
if search_results['apps']:
game_id = search_results['apps'][0]['id'][0]
# TODO: checkup for failed requests
app_details = self.steam_client.apps.get_app_details(game_id)
app_data = app_details[str(game_id)]['data']
def add_game_info(self, data, name): game_description.title = app_data['name']
if not data['apps']: game_description.desc = app_data['short_description']
self.all_data[name] = 0 game_description.reqs = ((app_data['linux_requirements']
else: and (
game_id = data['apps'][0]['id'] app_data['linux_requirements']['minimum'] or
data = self.steam_process.apps.get_app_details(game_id) app_data['linux_requirements']['recommended']
self.all_data[name] = data[str(game_id[0])]['data'] ))
with open(self.datapath, "wb+") as datafile: or (app_data['pc_requirements']
pickle.dump(self.all_data, datafile) and (
self.get_all_data() app_data['pc_requirements']['minimum'] or
app_data['pc_requirements']['recommended']
))
or '-')
game_description.languages = app_data['supported_languages']
def search_game(self, game_name): # Steam Grid DB image retrieving
game_description.image_location_path = self.steam_grid_db_retrieve_image(game_name)
self.get_all_data() self.data[game_name] = game_description
self.save_db()
if game_name in self.all_data: def save_db(self):
print("ITS HERE!") with open(self.db_storage_path, "wb+") as datafile:
else: pickle.dump(self.data, datafile)
search_results = self.steam_process.apps.search_games(game_name)
self.add_game_info(search_results, game_name)
return self.format_game_data(self.all_data[game_name])
def get_all_data(self): def load_db(self):
try: try:
with open(self.datapath, "rb") as datafile: with open(self.db_storage_path, "rb") as datafile:
self.all_data = pickle.load(datafile) self.data = pickle.load(datafile)
except FileNotFoundError: except FileNotFoundError:
self.all_data = dict() self.data = dict()
def format_game_data(self, game_data):
formatted_data = dict()
if game_data != 0:
formatted_data['title'] = game_data['name']
formatted_data['desc'] = game_data['short_description']
formatted_data['languages'] = game_data['supported_languages']
formatted_data['reqs'] = game_data['linux_requirements']
# for key, value in formatted_data.items():
# print("{0}: {1}".format(key, value))
else:
#TODO исправить это недоразумение, временная затычка
formatted_data['title'] = "Информация не найдена!"
formatted_data['desc'] = "Информация не найдена!"
formatted_data['languages'] = "Информация не найдена!"
formatted_data['reqs'] = "Информация не найдена!"
# print(formatted_data)
return formatted_data
def clean_data(self): def clean_data(self):
self.all_data = dict() self.data = dict()
with open(self.datapath, "wb") as datafile:
pickle.dump(self.all_data, datafile)
print("data cleaned")
from dataclasses import dataclass
@dataclass
class GameDescription:
title: str = 'Информация не найдена!'
desc: str = 'Информация не найдена!'
languages: str = 'Информация не найдена!'
reqs: str = 'Информация не найдена!'
image_location_path: str = ''
def as_dict(self):
formatted_data = dict()
formatted_data['title'] = self.title
formatted_data['desc'] = self.desc
formatted_data['languages'] = self.languages
formatted_data['reqs'] = self.reqs
formatted_data['image_location_path'] = self.image_location_path
return formatted_data
from dataclasses import dataclass
@dataclass
class GameEntry:
name: str = ''
exec: str = ''
icon: str = ''
import typing import typing
from dataclasses import dataclass, fields from dataclasses import fields
from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt, QByteArray from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt, QByteArray
from ingame.models.GameEntry import GameEntry
@dataclass
class Game:
name: str = ''
exec: str = ''
icon: str = ''
class GamesModel(QAbstractListModel): class GamesModel(QAbstractListModel):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._list = [] self.games_list = []
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> typing.Any: def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> typing.Any:
if 0 <= index.row() < self.rowCount(): if 0 <= index.row() < self.rowCount():
student = self._list[index.row()] student = self.games_list[index.row()]
name = self.roleNames().get(role) name = self.roleNames().get(role)
if name: if name:
return getattr(student, name.decode()) return getattr(student, name.decode())
def roleNames(self) -> dict[int, QByteArray]: def roleNames(self) -> dict[int, QByteArray]:
d = {} d = {}
for i, field in enumerate(fields(Game)): for i, field in enumerate(fields(GameEntry)):
d[Qt.DisplayRole + i] = field.name.encode() d[Qt.DisplayRole + i] = field.name.encode()
return d return d
def rowCount(self, index: QModelIndex = QModelIndex()) -> int: def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
return len(self._list) return len(self.games_list)
def add_game(self, game: Game) -> None: def add_game(self, game: GameEntry) -> None:
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._list.append(game) self.games_list.append(game)
self.endInsertRows() self.endInsertRows()
def clear(self) -> None: def clear(self) -> None:
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._list = [] self.games_list = []
self.endInsertRows() self.endInsertRows()
pass pass
...@@ -256,6 +256,22 @@ files = [ ...@@ -256,6 +256,22 @@ files = [
] ]
[[package]] [[package]]
name = "platformdirs"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pygame" name = "pygame"
version = "2.5.2" version = "2.5.2"
description = "Python Game Development" description = "Python Game Development"
...@@ -408,6 +424,19 @@ full = ["python-steam-api[all]"] ...@@ -408,6 +424,19 @@ full = ["python-steam-api[all]"]
style = ["codespell[toml] (>=2.2.4)", "isort", "ruff (>=0.1.8)", "toml-sort", "yamllint"] style = ["codespell[toml] (>=2.2.4)", "isort", "ruff (>=0.1.8)", "toml-sort", "yamllint"]
[[package]] [[package]]
name = "python-steamgriddb"
version = "1.0.5"
description = "A Python wrapper for SteamGridDB's API"
optional = false
python-versions = "*"
files = [
{file = "python-steamgriddb-1.0.5.tar.gz", hash = "sha256:036db7bb09865da73b40b68cf04fb9675cd18b4908275092d91f37bf16245069"},
]
[package.dependencies]
requests = "*"
[[package]]
name = "requests" name = "requests"
version = "2.31.0" version = "2.31.0"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
...@@ -498,4 +527,4 @@ zstd = ["zstandard (>=0.18.0)"] ...@@ -498,4 +527,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.11,<3.13" python-versions = ">=3.11,<3.13"
content-hash = "a19b74dab3d3d62bbdfc01b2ab1b7d011607b45cd0601e65f6a83f16603ce099" content-hash = "8acdd92388f970774431a6b4f12e8e5c0d5d01c0d88d748ae7a4a049b50a5005"
...@@ -16,6 +16,8 @@ requests = "^2.31.0" ...@@ -16,6 +16,8 @@ requests = "^2.31.0"
desktop-parser = "^0.1.1" desktop-parser = "^0.1.1"
pygame = "^2.5.2" pygame = "^2.5.2"
python-steam-api = "^2.0" python-steam-api = "^2.0"
python-steamgriddb = "^1.0.5"
platformdirs = "^4.2.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
mypy = "^1.9.0" mypy = "^1.9.0"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment