diff --git a/requirements/app.txt b/requirements/app.txt index cb8f327..9c55541 100644 --- a/requirements/app.txt +++ b/requirements/app.txt @@ -1,4 +1,4 @@ -galaxy.plugin.api==0.53 +galaxy.plugin.api==0.55 python-dateutil==2.8.0 requests==2.21.0 psutil==5.6.1 diff --git a/src/backend.py b/src/backend.py index c976a69..3e91f86 100644 --- a/src/backend.py +++ b/src/backend.py @@ -1,24 +1,9 @@ import logging as log -import asyncio class ParadoxClient: def __init__(self, http_client): self.http_client = http_client - self.paradox_launcher_skus = None - - async def prepare_sku(self): - data = {'token': self.http_client.token} - log.info('Starting skus retrieve') - response = await self.http_client.do_request('GET', 'https://accounts.paradoxplaza.com/api/skus', headers=data) - response = await response.json() - log.info('Finished skus retrieve') - paradox_launcher_skus = set() - for sku in response: - await asyncio.sleep(0.01) - if 'paradoxLauncher' in response[sku]['platform']: - paradox_launcher_skus.add(sku) - self.paradox_launcher_skus = paradox_launcher_skus async def get_account_id(self): data = {'Authorization': f'{{"session":{{"token":"{self.http_client.token}"}}}}', @@ -34,13 +19,14 @@ async def get_owned_games(self): headers=data) response = await response.json() + owned_products = [] if 'products' in response: for platforms in response['products']: for platform in platforms: for game in platforms[platform]: log.info(game) - if game['sku'] and game['title'] and game['product_type']: + if game['sku'] and game['title'] and game['product_type'] and "paradox builds" in game['platforms']: owned_products.append({'sku': game['sku'], 'title': game['title'], 'type': game['product_type']}) diff --git a/src/consts.py b/src/consts.py index da5b41d..6483cc8 100644 --- a/src/consts.py +++ b/src/consts.py @@ -1,5 +1,6 @@ - import re +import sys +from enum import EnumMeta AUTH_URL = r"https://accounts.paradoxplaza.com/login" @@ -9,9 +10,22 @@ PARADOX_LAUNCHER_EXE = "Paradox Launcher.exe" +class System(EnumMeta): + WINDOWS = 1 + MACOS = 2 + LINUX = 3 + + +if sys.platform == 'win32': + SYSTEM = System.WINDOWS +elif sys.platform == 'darwin': + SYSTEM = System.MACOS + + def regex_pattern(regex): return ".*" + re.escape(regex) + ".*" + AUTH_PARAMS = { "window_title": "Login to Paradox\u2122", "window_width": 700, diff --git a/src/plugin.py b/src/plugin.py index 60a0f0d..7d7f5be 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -1,4 +1,3 @@ - import logging as log import sys @@ -7,9 +6,9 @@ from galaxy.api.types import NextStep, Authentication, Game, LicenseInfo, LicenseType, LocalGame, LocalGameState from version import __version__ -from consts import AUTH_PARAMS from backend import ParadoxClient from http_client import AuthenticatedHttpClient +from consts import AUTH_PARAMS, System, SYSTEM import pickle import asyncio @@ -19,8 +18,6 @@ import webbrowser from typing import Any, List, Optional - - from local import LocalClient @@ -30,7 +27,6 @@ def __init__(self, reader, writer, token): self._http_client = AuthenticatedHttpClient(self.store_credentials) self.paradox_client = ParadoxClient(self._http_client) self.local_client = LocalClient() - self.prepare_sku = None self.owned_games_cache = None self.local_games_cache = {} @@ -39,9 +35,13 @@ def __init__(self, reader, writer, token): self.tick_counter = 0 self.local_games_called = None + self.owned_games_called = None self.update_installed_games_task = None self.update_running_games_task = None + self.update_owned_games_task = None + + async def authenticate(self, stored_credentials=None): if stored_credentials: @@ -49,7 +49,6 @@ async def authenticate(self, stored_credentials=None): self._http_client.authenticate_with_cookies(stored_cookies) self._http_client.set_auth_lost_callback(self.lost_authentication) acc_id = await self.paradox_client.get_account_id() - self.prepare_sku = asyncio.create_task(self.paradox_client.prepare_sku()) return Authentication(str(acc_id), 'Paradox') if not stored_credentials: return NextStep("web_session", AUTH_PARAMS) @@ -58,76 +57,85 @@ async def pass_login_credentials(self, step, credentials, cookies): self._http_client.authenticate_with_cookies(cookies) self._http_client.set_auth_lost_callback(self.lost_authentication) acc_id = await self.paradox_client.get_account_id() - self.prepare_sku = asyncio.create_task(self.paradox_client.prepare_sku()) return Authentication(str(acc_id), 'Paradox') async def get_owned_games(self): - owned_games = await self.paradox_client.get_owned_games() - await self.prepare_sku - games_to_send = [] - sent_titles = set() - for game in owned_games: - log.info(game) - if game['sku'] in self.paradox_client.paradox_launcher_skus and 'game' in game['type']: - title = game['title'].replace(' (Paradox)', '') - title = title.split(':')[0] - if title in sent_titles: - continue - sent_titles.add(title) - games_to_send.append(Game(title.lower().replace(' ', '_'), title, None, LicenseInfo(LicenseType.SinglePurchase))) - self.owned_games_cache = games_to_send - return games_to_send - - async def get_local_games(self): - games_path = self.local_client.games_path - if not games_path: - return [] - local_games = os.listdir(games_path) - games_to_send = [] - local_games_cache = {} - for local_game in local_games: - game_folder = os.path.join(games_path, local_game) - game_cpatch = os.path.join(game_folder, '.cpatch', local_game) - try: - with open(os.path.join(game_cpatch, 'version'))as game_cp: - version = game_cp.readline() - with open(os.path.join(game_cpatch, 'repository.json'), 'r') as js: - game_repository = json.load(js) - exe_path = game_repository['content']['versions'][version]['exePath'] - except FileNotFoundError: - continue - except Exception as e: - log.error(f"Unable to parse local game {local_game} {repr(e)}") - continue - - local_games_cache[local_game] = os.path.join(game_folder, exe_path) - games_to_send.append(LocalGame(local_game, LocalGameState.Installed)) - self.local_games_cache = local_games_cache - self.local_games_called = True + try: + owned_games = await self.paradox_client.get_owned_games() + sent_titles = set() + for game in owned_games: + log.info(game) + if 'game' in game['type']: + title = game['title'].replace(' (Paradox)', '') + title = title.split(':')[0] + if title in sent_titles: + continue + sent_titles.add(title) + games_to_send.append(Game(title.lower().replace(' ', '_'), title, None, LicenseInfo(LicenseType.SinglePurchase))) + self.owned_games_cache = games_to_send + self.owned_games_called = True + except Exception as e: + log.error(f"Encountered exception while retriving owned games {repr(e)}") + self.owned_games_called = True + raise e return games_to_send - async def launch_game(self, game_id): - exe_path = self.local_games_cache.get(game_id) - log.info(f"Launching {exe_path}") - game_dir = os.path.join(self.local_client.games_path, game_id) - subprocess.Popen(exe_path,cwd=game_dir) + if SYSTEM == System.WINDOWS: + async def get_local_games(self): + games_path = self.local_client.games_path + if not games_path: + self.local_games_called = True + return [] + local_games = os.listdir(games_path) + + games_to_send = [] + local_games_cache = {} + for local_game in local_games: + game_folder = os.path.join(games_path, local_game) + game_cpatch = os.path.join(game_folder, '.cpatch', local_game) + try: + with open(os.path.join(game_cpatch, 'version'))as game_cp: + version = game_cp.readline() + with open(os.path.join(game_cpatch, 'repository.json'), 'r') as js: + game_repository = json.load(js) + exe_path = game_repository['content']['versions'][version]['exePath'] + except FileNotFoundError: + continue + except Exception as e: + log.error(f"Unable to parse local game {local_game} {repr(e)}") + continue - async def install_game(self, game_id): - bootstraper_exe = self.local_client.bootstraper_exe - if bootstraper_exe: - subprocess.Popen(bootstraper_exe) - return - log.info("Local client not installed") - webbrowser.open('https://play.paradoxplaza.com') - - async def uninstall_game(self, game_id): - bootstraper_exe = self.local_client.bootstraper_exe - if bootstraper_exe: - subprocess.call(bootstraper_exe) - return - log.info("Local client not installed") - webbrowser.open('https://play.paradoxplaza.com') + local_games_cache[local_game] = os.path.join(game_folder, exe_path) + games_to_send.append(LocalGame(local_game, LocalGameState.Installed)) + self.local_games_cache = local_games_cache + self.local_games_called = True + return games_to_send + + if SYSTEM == System.WINDOWS: + async def launch_game(self, game_id): + exe_path = self.local_games_cache.get(game_id) + log.info(f"Launching {exe_path}") + game_dir = os.path.join(self.local_client.games_path, game_id) + subprocess.Popen(exe_path,cwd=game_dir) + + if SYSTEM == System.WINDOWS: + async def install_game(self, game_id): + bootstraper_exe = self.local_client.bootstraper_exe + if bootstraper_exe: + subprocess.Popen(bootstraper_exe) + return + log.info("Local client not installed") + webbrowser.open('https://play.paradoxplaza.com') + + if SYSTEM == System.WINDOWS: + async def uninstall_game(self, game_id): + bootstraper_exe = self.local_client.bootstraper_exe + if bootstraper_exe: + subprocess.call(bootstraper_exe) + return + log.info("Local client not installed") + webbrowser.open('https://play.paradoxplaza.com') async def update_installed_games(self): games_path = self.local_client.games_path @@ -165,18 +173,36 @@ async def update_running_games(self): self.running_game = running_game + async def update_owned_games(self): + owned_games_cache = self.owned_games_cache + owned_games = await self.get_owned_games() + log.info("Looking for new games") + for game in owned_games: + if game not in owned_games_cache: + log.info(f"Adding game {game}") + self.add_game(game) + + def tick(self): - if not self.local_games_called or sys.platform != 'win32': - return self.tick_counter += 1 + if not self.owned_games_called or (sys.platform == 'win32' and not self.local_games_called): + return + + if self.tick_counter % 60 == 0: + if not self.update_owned_games_task or self.update_owned_games_task.done(): + self.update_owned_games_task = asyncio.create_task(self.update_owned_games()) + + if sys.platform != 'win32': + return + if not self.update_installed_games_task or self.update_installed_games_task.done(): self.update_installed_games_task = asyncio.create_task(self.update_installed_games()) if not self.update_running_games_task or self.update_running_games_task.done(): self.update_running_games_task = asyncio.create_task(self.update_running_games()) - def shutdown(self): - asyncio.create_task(self._http_client.close()) + async def shutdown(self): + await self._http_client.close() async def prepare_os_compatibility_context(self, game_ids: List[str]) -> Any: return None diff --git a/src/version.py b/src/version.py index 6a35e85..896a370 100644 --- a/src/version.py +++ b/src/version.py @@ -1 +1 @@ -__version__ = "0.3" +__version__ = "0.4"