Commit 194a9181 authored by Yankovskiy Georgiy's avatar Yankovskiy Georgiy

Merge resolve

parents a01be939 a2989022
......@@ -131,8 +131,4 @@ pyvenv.cfg
venv
pip-selfcheck.json
# End of https://www.toptal.com/developers/gitignore/api/pycharm,venv
### Extra
.agent-data
\ No newline at end of file
# End of https://www.toptal.com/developers/gitignore/api/pycharm,venv
\ No newline at end of file
......@@ -6,31 +6,26 @@ from time import sleep
from pathlib import Path
from typing import AnyStr, Union
import requests
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
from steamgrid import SteamGridDB
class GameShortcut:
def __init__(self, filename, product_name, icon):
self.filename = filename
self.product_name = product_name
self.icon = icon
class App(QtCore.QObject):
app_name = "ingame"
app_author = "foss"
game_list_details_retrieving_progress = Signal(float, name="gameListDetailsRetrievingProgress")
class App(QtCore.QObject):
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")
......@@ -40,9 +35,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
......@@ -54,94 +52,94 @@ 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}')
self.games_model.clear()
files = glob.glob(f"{self.portproton_location}/*.desktop")
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
url_img = find_image(_name)
_icon = (url_img
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: fix: progress=1.0 not emitted if details already cached/downloaded
def retrieve_games_details(self):
def retrieve_games_details_thread(t):
t.game_list_details_retrieving_progress.emit(0.0)
all_count: int = len(self.games_model.games_list)
game_entry: GameEntry
i: int = 0
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
t.game_list_details_retrieving_progress.emit(float(i) / all_count)
i += 1
t.game_list_details_retrieving_progress.emit(1.0)
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)
def get_game_data_thread(t, 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 = threading.Thread(target=get_game_data_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):
......@@ -154,40 +152,13 @@ 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):
return self.games_model
def find_image(game_name):
steamgriddb = SteamGridDB('66827eabea66de47d036777ed2be87b2')
save_path = f"{Path(__file__).resolve().parent}/../../qml/images/{game_name}.png"
if os.path.exists(save_path):
print("FOUND!")
return save_path
result = steamgriddb.search_game(game_name)
grids = steamgriddb.get_grids_by_gameid(list([result[0].id]))
for grid in grids:
if grid.height == 900 and grid.width == 600:
url_img = grid.url
response = requests.get(url_img)
print(save_path)
with open(save_path, 'wb') as file:
file.write(response.content)
break
return url_img
\ No newline at end of file
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
import time
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()
''' DATABASE '''
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 ''
def __add_game_description(self, search_results, game_name):
game_description = GameDescription()
game_description.locked = True
self.data[game_name] = game_description
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()
# 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 search_game(self, game_name):
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']
self.get_all_data()
# Steam Grid DB image retrieving
game_description.image_location_path = self.__steam_grid_db_retrieve_image(game_name)
game_description.locked = False
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:
locked: bool = False
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"
......@@ -511,4 +527,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.11,<3.13"
content-hash = "25bf5edd372b37ccd07dd1f5dd40b16fe884c5ab918957b9a2f5c282b0a7dfd4"
content-hash = "8acdd92388f970774431a6b4f12e8e5c0d5d01c0d88d748ae7a4a049b50a5005"
......@@ -17,6 +17,7 @@ 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"
......
......@@ -312,6 +312,7 @@ Rectangle {
// Повторитель
Repeater {
id: gamesGridRepeater
model: core_app.games
// Карточка игры
Game {
......@@ -365,6 +366,7 @@ Rectangle {
// LOGIC
property int focusedItems: 0;
property int focusedTabs: 0;
......@@ -412,28 +414,33 @@ Rectangle {
// c[tabs.focusedItems].clicked();
}
function onGamepadClickedLB(done){
if(window.scene !== S.homeScene) return;
/* SIGNALS */
function onGamepadClickedLB(args){
tabs.applyTabsFocus(-1)
}
function onGamepadClickedRB(done){
if(window.scene !== S.homeScene) return;
function onGamepadClickedRB(args){
tabs.applyTabsFocus(1)
}
function onGamepadAxisLeft(done){
if(window.scene !== S.homeScene) return;
function onGamepadAxisLeft(args){
tabs.applyItemsFocus(-1)
}
function onGamepadAxisRight(done){
if(window.scene !== S.homeScene) return;
function onGamepadAxisRight(args){
tabs.applyItemsFocus(1)
}
function onGamepadClickedApply(done){
function onGamepadClickedApply(args){
if(window.scene !== S.homeScene) return;
// console.log("onGamepadClickedApply");
let c = gamesGrid.children;
c[tabs.focusedItems].press();
}
function onGameListDetailsRetrievingProgress(args) {
let progress = args[0];
console.log(progress);
if(progress === 1.0){
gamesGridRepeater.model = [];
gamesGridRepeater.model = core_app.games;
}
}
}
......
......@@ -16,7 +16,7 @@ Window {
}
FontLoader {
id: globalFont;
source: "./fonts/OpenSans-VariableFont_wdth.ttf"
source: "./fonts/OpenSans-VariableFont.ttf"
}
......@@ -52,6 +52,9 @@ Window {
function onGamepadClickedBack(done){
window._trigger("onGamepadClickedBack", done);
}
function onGameListDetailsRetrievingProgress(progress){
homeScene.onGameListDetailsRetrievingProgress([progress]);
}
}
function _trigger(_method, ...args){
......
......@@ -41,20 +41,23 @@ Rectangle {
anchors.rightMargin: 0
}
function onGamepadClickedLB(done){
tabs.onGamepadClickedLB(done)
function onGamepadClickedLB(args){
tabs.onGamepadClickedLB(args)
}
function onGamepadClickedRB(done){
tabs.onGamepadClickedRB(done)
function onGamepadClickedRB(args){
tabs.onGamepadClickedRB(args)
}
function onGamepadAxisLeft(done){
tabs.onGamepadAxisLeft(done)
function onGamepadAxisLeft(args){
tabs.onGamepadAxisLeft(args)
}
function onGamepadAxisRight(done){
tabs.onGamepadAxisRight(done)
function onGamepadAxisRight(args){
tabs.onGamepadAxisRight(args)
}
function onGamepadClickedApply(done){
tabs.onGamepadClickedApply(done)
function onGamepadClickedApply(args){
tabs.onGamepadClickedApply(args)
}
function onGameListDetailsRetrievingProgress(args){
tabs.onGameListDetailsRetrievingProgress(args)
}
}
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