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
from PySide6 import QtCore
from os.path import expanduser
from desktop_parser import DesktopFile
from platformdirs import user_cache_dir, user_config_dir
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 PySide6.QtCore import Property, Signal, Slot, QObject, Qt
......@@ -24,11 +26,12 @@ class GameShortcut:
class App(QtCore.QObject):
app_name = "ingame"
app_author = "foss"
game_started = Signal(bool, name="gameStarted")
game_ended = Signal(bool, name="gameEnded")
data_found = Signal(dict, name="gotGameData")
gamepad_clicked_LB = Signal(bool, name="gamepadClickedLB")
gamepad_clicked_RB = Signal(bool, name="gamepadClickedRB")
gamepad_clicked_apply = Signal(bool, name="gamepadClickedApply")
......@@ -38,9 +41,12 @@ class App(QtCore.QObject):
def __init__(self):
super().__init__()
self.games_model: GamesModel = GamesModel()
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.running_game_process: Union[subprocess.Popen, None] = None
......@@ -52,13 +58,13 @@ class App(QtCore.QObject):
self.gamepad.r_clicked = lambda: self.gamepad_axis_right.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()
def setup(self):
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()
print(f'Current PortProton location: {self.portproton_location}')
......@@ -67,79 +73,73 @@ class App(QtCore.QObject):
for val in files:
desktop_file = DesktopFile.from_file(val)
data = desktop_file.data
entry = data['Desktop Entry']
desktop_file_data = desktop_file.data
desktop_entry = desktop_file_data['Desktop Entry']
_name = entry['Name'] or 'generic'
_exec = 'Exec' in entry and entry['Exec'] or ''
_icon = entry['Icon']
entry_name = desktop_entry['Name'] or 'generic'
entry_exec = 'Exec' in desktop_entry and desktop_entry['Exec'] or ''
entry_icon = desktop_entry['Icon']
assert (isinstance(_name, str)
and isinstance(_exec, str)
and isinstance(_icon, str))
assert (isinstance(entry_name, str)
and isinstance(entry_exec, str)
and isinstance(entry_icon, str))
exec_split = _exec.split(' ')
entry_exec_split = entry_exec.split(' ')
# Ignore extra non-related desktop entries
if (len(exec_split) <= 1 or
('data/scripts/start.sh' not in exec_split[1] or '%F' in exec_split[-1])):
if (len(entry_exec_split) <= 1 or
('data/scripts/start.sh' not in entry_exec_split[1] or '%F' in entry_exec_split[-1])):
continue
# TODO parse product name
_icon = (os.path.isfile(_icon) and _icon
or os.path.realpath(f"{Path(__file__).resolve().parent}../../../qml/images/PUBG.png"))
# Автозапуск игры:
# PW_GUI_DISABLED_CS=1
# START_FROM_STEAM=1
entry_icon = (os.path.isfile(entry_icon) and entry_icon) or ''
# Remove extra env in the beginning
_exec = _exec[4:len(_exec)]
_exec = f"env START_FROM_STEAM=1 {_exec}"
entry_exec = f"env START_FROM_STEAM=1 {entry_exec[4:len(entry_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.retrieve_games_details()
except FileNotFoundError:
print('File not found')
except Exception as e:
print('An error occurred', e)
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):
# do stuff
# if can_exit:
self.gamepad.terminate()
# event.accept() # let the window close
# else:
# event.ignore()
self.agent.clean_data()
# self.agent.save_db()
### SLOTS ###
''' SLOTS '''
@Slot(str, result=dict)
def get_game_data(self, game_name):
#print(game_name)
def search_thread(t, name):
search_result = t.agent.search_game(name)
search_result = t.agent.retrieve_game_description(name)
t.data_found.emit(search_result)
return
thread = threading.Thread(target=search_thread, args=(self, game_name))
thread.start()
pass
@Slot(str)
def start_game(self, exec):
def start_game(self, _exec):
self.game_started.emit(True)
def run_in_thread(t, _exec):
......@@ -152,18 +152,12 @@ class App(QtCore.QObject):
)
t.running_game_process.wait()
t.game_ended.emit(True)
# output = self.running_game_process.stdout.read()
# self.running_game_process.stdout.close()
return
thread = threading.Thread(target=run_in_thread, args=(self, exec))
thread = threading.Thread(target=run_in_thread, args=(self, _exec))
thread.start()
pass
### PROPERTIES ###
''' PROPERTIES '''
@Property(QObject, constant=True)
def games(self):
......
import os
import pickle
import requests
from steam_web_api import Steam
# TODO:
# [?] Определиться, используется ли Lutris. Если да, вместо этого будет обращение к нему. Если нет,
# продумать "рыбу" более логично.
# [?] Починить отображение системных требований (точнее, разобраться, что именно возвращает API.
# [done 1/2] Додумать форматированные данные, что именно мы видим на странице игры?
from steamgrid import SteamGridDB
from ingame.models.GameDescription import GameDescription
class GameAgent:
generic_name = "Risk of rain 2"
datapath = ".agent-data"
all_data = dict()
scenario = 0
# generic_name = "Risk of rain 2"
# scenario = 0
# db_storage_path = ".agent-data"
# data = dict()
def __init__(self):
def __init__(self, config_path, cache_path):
super().__init__()
agent_key = "SOME_KEY_HERE_I_GUESS"
self.steam_process = Steam(agent_key)
self.get_all_data()
# TODO: move API tokens to GUI settings tab / environmental variables
self.steam_grid_db_client = SteamGridDB('66827eabea66de47d036777ed2be87b2')
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):
if not data['apps']:
self.all_data[name] = 0
else:
game_id = data['apps'][0]['id']
data = self.steam_process.apps.get_app_details(game_id)
self.all_data[name] = data[str(game_id[0])]['data']
with open(self.datapath, "wb+") as datafile:
pickle.dump(self.all_data, datafile)
self.get_all_data()
game_description.title = app_data['name']
game_description.desc = app_data['short_description']
game_description.reqs = ((app_data['linux_requirements']
and (
app_data['linux_requirements']['minimum'] or
app_data['linux_requirements']['recommended']
))
or (app_data['pc_requirements']
and (
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:
print("ITS HERE!")
else:
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 save_db(self):
with open(self.db_storage_path, "wb+") as datafile:
pickle.dump(self.data, datafile)
def get_all_data(self):
def load_db(self):
try:
with open(self.datapath, "rb") as datafile:
self.all_data = pickle.load(datafile)
with open(self.db_storage_path, "rb") as datafile:
self.data = pickle.load(datafile)
except FileNotFoundError:
self.all_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
self.data = dict()
def clean_data(self):
self.all_data = dict()
with open(self.datapath, "wb") as datafile:
pickle.dump(self.all_data, datafile)
print("data cleaned")
self.data = dict()
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
from dataclasses import dataclass, fields
from dataclasses import fields
from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt, QByteArray
@dataclass
class Game:
name: str = ''
exec: str = ''
icon: str = ''
from ingame.models.GameEntry import GameEntry
class GamesModel(QAbstractListModel):
def __init__(self):
super().__init__()
self._list = []
self.games_list = []
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> typing.Any:
if 0 <= index.row() < self.rowCount():
student = self._list[index.row()]
student = self.games_list[index.row()]
name = self.roleNames().get(role)
if name:
return getattr(student, name.decode())
def roleNames(self) -> dict[int, QByteArray]:
d = {}
for i, field in enumerate(fields(Game)):
for i, field in enumerate(fields(GameEntry)):
d[Qt.DisplayRole + i] = field.name.encode()
return d
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._list.append(game)
self.games_list.append(game)
self.endInsertRows()
def clear(self) -> None:
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._list = []
self.games_list = []
self.endInsertRows()
pass
......@@ -256,6 +256,22 @@ files = [
]
[[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"
version = "2.5.2"
description = "Python Game Development"
......@@ -408,6 +424,19 @@ full = ["python-steam-api[all]"]
style = ["codespell[toml] (>=2.2.4)", "isort", "ruff (>=0.1.8)", "toml-sort", "yamllint"]
[[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"
version = "2.31.0"
description = "Python HTTP for Humans."
......@@ -498,4 +527,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.11,<3.13"
content-hash = "a19b74dab3d3d62bbdfc01b2ab1b7d011607b45cd0601e65f6a83f16603ce099"
content-hash = "8acdd92388f970774431a6b4f12e8e5c0d5d01c0d88d748ae7a4a049b50a5005"
......@@ -16,6 +16,8 @@ requests = "^2.31.0"
desktop-parser = "^0.1.1"
pygame = "^2.5.2"
python-steam-api = "^2.0"
python-steamgriddb = "^1.0.5"
platformdirs = "^4.2.2"
[tool.poetry.group.dev.dependencies]
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