import json import logging import os import sys import webbrowser from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union from urllib import parse from galaxy.api.consts import LocalGameState, OSCompatibility, Platform from galaxy.api.errors import InvalidCredentials from galaxy.api.plugin import create_and_run_plugin, Plugin from galaxy.api.types import Authentication, Game, LicenseInfo, LicenseType, LocalGame, NextStep from galaxy.proc_tools import process_iter from twitch_db_client import db_select, get_cookie from twitch_launcher_client import TwitchLauncherClient def is_windows() -> bool: return sys.platform == "win32" T = TypeVar("T") def os_specific(unknown, win: Optional[T] = None, mac: Optional[T] = None) -> Optional[T]: return {"win32": win, "darwin": mac}.get(sys.platform, unknown) @dataclass class InstalledGame(LocalGame): install_path: str class TwitchPlugin(Plugin): @staticmethod def _read_manifest() -> str: with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "manifest.json")) as manifest: return json.load(manifest) @property def _db_owned_games(self) -> str: return str(os_specific( win=os.path.join(os.path.expandvars("%APPDATA%"), "Twitch", "Games", "Sql", "GameProductInfo.sqlite") , unknown="" )) @property def _db_installed_games(self) -> str: return str(os_specific( win=os.path.join(os.path.expandvars("%PROGRAMDATA%"), "Twitch", "Games", "Sql", "GameInstallInfo.sqlite") , unknown="" )) def _get_user_info(self) -> Optional[Dict[str, str]]: cookies_db_path = self._launcher_client.cookies_db_path if not cookies_db_path: logging.warning("Cookies db not found") return None user_info_cookie = get_cookie(cookies_db_path, "twilight-user.desklight") if not user_info_cookie: return {} user_info = json.loads(parse.unquote(user_info_cookie)) if not user_info: return {} return user_info def _get_owned_games(self) -> Dict[str, Game]: try: return { row["ProductIdStr"]: Game( game_id=row["ProductIdStr"] , game_title=row["ProductTitle"] , dlcs=None , license_info=LicenseInfo(LicenseType.SinglePurchase) ) for row in db_select( db_path=self._db_owned_games , query="select ProductIdStr, ProductTitle from DbSet" ) } except Exception: logging.exception("Failed to get owned games") return {} def _update_owned_games(self) -> None: owned_games = self._get_owned_games() for game_id in self._owned_games_cache.keys() - owned_games.keys(): self.remove_game(game_id) for game_id in (owned_games.keys() - self._owned_games_cache.keys()): self.add_game(owned_games[game_id]) self._owned_games_cache = owned_games def _get_installed_games(self) -> Dict[str, InstalledGame]: try: return { row["Id"]: InstalledGame( game_id=row["Id"] , local_game_state=LocalGameState.Installed , install_path=row["InstallDirectory"] ) for row in db_select( db_path=self._db_installed_games , query="select Id, Installed, InstallDirectory from DbSet" ) if row.get("Installed") and os.path.exists(row.get("InstallDirectory", "")) } except Exception: logging.exception("Failed to get local games") return {} def _get_local_games(self) -> Dict[str, InstalledGame]: installed_games = self._get_installed_games() if not installed_games: return installed_games running_processes = [ proc_info.binary_path for proc_info in process_iter() if proc_info and proc_info.binary_path ] def is_game_running(game_install_path) -> bool: for process_path in running_processes: if process_path.startswith(game_install_path): return True return False for installed_game in installed_games.values(): if is_game_running(installed_game.install_path): installed_game.local_game_state |= LocalGameState.Running return installed_games def _update_local_games_state(self) -> None: local_games = self._get_local_games() for game_id in self._local_games_cache.keys() - local_games.keys(): self.update_local_game_status(LocalGame(game_id, LocalGameState.None_)) for game_id, local_game in local_games.items(): old_game = self._local_games_cache.get(game_id) if old_game is None or old_game.local_game_state != local_game.local_game_state: self.update_local_game_status(LocalGame(game_id, local_game.local_game_state)) self._local_games_cache = local_games def __init__(self, reader, writer, token): self._manifest = self._read_manifest() self._launcher_client = TwitchLauncherClient() self._owned_games_cache: Dict[str, Game] = {} self._local_games_cache: Dict[str, InstalledGame] = {} super().__init__(Platform(self._manifest["platform"]), self._manifest["version"], reader, writer, token) def handshake_complete(self) -> None: self._launcher_client.update_install_path() self._owned_games_cache = self._get_owned_games() self._local_games_cache = self._get_local_games() def tick(self) -> None: self._launcher_client.update_install_path() self._update_owned_games() self._update_local_games_state() async def authenticate(self, stored_credentials: Optional[Dict] = None) -> Union[NextStep, Authentication]: if not self._launcher_client.is_installed: webbrowser.open_new_tab("https://www.twitch.tv/downloads") raise InvalidCredentials def get_auth_info() -> Optional[Tuple[str, str]]: user_info = self._get_user_info() if not user_info: logging.warning("No user info") return None user_id = user_info.get("id") user_name = user_info.get("displayName") if not user_id or not user_name: logging.warning("No user id/name") return None return user_id, user_name auth_info = get_auth_info() if not auth_info: await self._launcher_client.start_launcher() raise InvalidCredentials self.store_credentials({"external-credentials": "force-reconnect-on-startup"}) return Authentication(user_id=auth_info[0], user_name=auth_info[1]) async def get_owned_games(self) -> List[Game]: return list(self._owned_games_cache.values()) async def get_local_games(self) -> List[LocalGame]: return [ LocalGame(game_id=game.game_id, local_game_state=game.local_game_state) for game in self._local_games_cache.values() ] async def install_game(self, game_id: str) -> None: return await self._launcher_client.launch_game(game_id) async def launch_game(self, game_id: str) -> None: return await self._launcher_client.launch_game(game_id) async def uninstall_game(self, game_id: str) -> None: return self._launcher_client.uninstall_game(game_id) if is_windows(): async def launch_platform_client(self) -> None: return await self._launcher_client.start_launcher() async def shutdown_platform_client(self) -> None: return self._launcher_client.quit_launcher() async def get_os_compatibility(self, game_id: str, context: Any) -> Optional[OSCompatibility]: return OSCompatibility.Windows def main(): create_and_run_plugin(TwitchPlugin, sys.argv) if __name__ == "__main__": main()