Ejemplo n.º 1
0
class EpicPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Epic, __version__, reader, writer, token)
        self._http_client = AuthenticatedHttpClient(store_credentials_callback=self.store_credentials)
        self._epic_client = EpicClient(self._http_client)
        self._local_provider = LocalGamesProvider()
        self._games_cache = {}
        self._refresh_owned_task = None

    async def _do_auth(self):
        user_info = await self._epic_client.get_users_info([self._http_client.account_id])
        display_name = self._epic_client.get_display_name(user_info)

        self._http_client.set_auth_lost_callback(self.lost_authentication)

        return Authentication(self._http_client.account_id, display_name)

    async def authenticate(self, stored_credentials=None):
        if not stored_credentials:
            return NextStep("web_session", AUTH_PARAMS, js=AUTH_JS)

        refresh_token = stored_credentials["refresh_token"]
        try:
            await self._http_client.authenticate_with_refresh_token(refresh_token)
        except Exception:
            # TODO: distinguish between login-related and all other (networking, server, e.t.c.) errors
            raise InvalidCredentials()

        return await self._do_auth()

    async def pass_login_credentials(self, step, credentials, cookies):
        try:
            await self._http_client.authenticate_with_exchage_code(
                credentials["end_uri"].split(AUTH_REDIRECT_URL, 1)[1]
            )
        except Exception:
            # TODO: distinguish between login-related and all other (networking, server, e.t.c.) errors
            raise InvalidCredentials()

        return await self._do_auth()

    async def _get_title_sanitized(self, app_name):
        if app_name in self._games_cache:
            return self._games_cache[app_name].game_title.replace(" ", "-").lower()
        log.debug('Nothing found, fallback to epic client')
        assets = await self._epic_client.get_assets()
        for asset in assets:
            if asset.app_name == app_name:
                details = await self._epic_client.get_catalog_items(asset.namespace, asset.catalog_id)
                return details.title.replace(" ", "-").lower()
        log.warning(f'Game {app_name} was not found in assets')
        raise UnknownBackendResponse()

    async def _get_owned_games(self):
        requests = []
        assets = await self._epic_client.get_assets()
        for namespace, _, catalog_id in assets:
            requests.append(self._epic_client.get_catalog_items(namespace, catalog_id))
        items = await asyncio.gather(*requests)
        games = []

        for i, item in enumerate(items):
            if "games" not in item.categories:
                continue
            game = Game(assets[i].app_name, item.title, None, LicenseInfo(LicenseType.SinglePurchase))
            games.append(game)
        return games

    async def get_owned_games(self):
        games = await self._get_owned_games()
        for game in games:
            self._games_cache[game.game_id] = game
        self._refresh_owned_task = asyncio.create_task(self._check_for_new_games())
        return games

    async def get_local_games(self):
        if self._local_provider.first_run:
            self._local_provider.setup()
        return [
            LocalGame(app_name, state)
            for app_name, state in self._local_provider.games.items()
        ]

    async def open_epic_browser(self, game_id):
        try:
            title = await self._get_title_sanitized(game_id)
            title = title.replace(" ", "-").lower()
        except UnknownBackendResponse:
            url = "https://www.epicgames.com/"
        else:
            url = f"https://www.epicgames.com/store/product/{title}/home"
        log.info(f"Opening Epic website {url}")
        webbrowser.open(url)

    @property
    def _open(self):
        if SYSTEM == System.WINDOWS:
            return "start"
        elif SYSTEM == System.MACOS:
            return "open"

    async def launch_game(self, game_id):
        if not self._local_provider.is_launcher_installed:
            await self.open_epic_browser(game_id)
            return
        if self._local_provider.is_game_running(game_id):
            log.info('Game already running.')
            return
        cmd = f"{self._open} com.epicgames.launcher://apps/{game_id}?action=launch^&silent=true"
        log.info(f"Launching game {game_id}")
        subprocess.Popen(cmd, shell=True)
        await self._local_provider.search_process(game_id, timeout=30)

    async def uninstall_game(self, game_id):
        if not self._local_provider.is_launcher_installed:
            await self.open_epic_browser(game_id)
            return
        title = await self._get_title_sanitized(game_id)
        cmd = f"{self._open} com.epicgames.launcher://store/product/{title}/home"
        log.info(f"Uninstalling game {title}")
        subprocess.Popen(cmd, shell=True)

    async def install_game(self, game_id):
        if not self._local_provider.is_launcher_installed:
            await self.open_epic_browser(game_id)
            return
        title = await self._get_title_sanitized(game_id)
        cmd = f"{self._open} com.epicgames.launcher://store/product/{title}/home"
        log.info(f"Installing game {title}")
        subprocess.Popen(cmd, shell=True)

    async def get_friends(self):
        ids = await self._epic_client.get_friends_list()
        account_ids = []
        friends = []
        prev_slice = 0
        log.debug(ids)
        for index, entry in enumerate(ids):
            account_ids.append(entry["accountId"])
            ''' Send request for friends information in batches of 50 so the request isn't too large,
            50 is an arbitrary number, to be tailored if need be '''
            if index + 1 % 50 == 0 or index == len(ids) - 1:
                friends.extend(await self._epic_client.get_users_info(account_ids[prev_slice:]))
                prev_slice = index

        return[
            FriendInfo(user_id=friend["id"], user_name=friend["displayName"])
            for friend in friends
        ]

    def _update_local_game_statuses(self):
        updated = self._local_provider.consume_updated_games()
        for id_ in updated:
            new_state = self._local_provider.games[id_]
            log.debug(f'Updating game {id_} state to {new_state}')
            self.update_local_game_status(LocalGame(id_, new_state))

    async def _check_for_new_games(self):
        await asyncio.sleep(60)  # interval

        log.info("Checking for new games")
        assets = await self._epic_client.get_assets()

        for namespace, app_name, catalog_id in assets:
            if app_name not in self._games_cache and namespace != "ue":
                details = await self._epic_client.get_catalog_items(namespace, catalog_id)
                if "games" not in details.categories:
                    continue
                game = Game(app_name, details.title, None, LicenseInfo(LicenseType.SinglePurchase))
                log.info(f"Found new game, {game}")
                self.add_game(game)
                self._games_cache[game.game_id] = game

    def tick(self):
        if not self._local_provider.first_run:
            self._update_local_game_statuses()

        if self._refresh_owned_task and self._refresh_owned_task.done():
            self._refresh_owned_task = asyncio.create_task(self._check_for_new_games())

    def shutdown(self):
        self._local_provider._status_updater.cancel()
        asyncio.create_task(self._http_client.close())
Ejemplo n.º 2
0
class BethesdaPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Bethesda, __version__, reader, writer, token)
        self._http_client = AuthenticatedHttpClient(self.store_credentials)
        self.bethesda_client = BethesdaClient(self._http_client)
        self.local_client = LocalClient()
        self.products_cache = product_cache
        self.owned_games_cache = None

        self._asked_for_local = False

        self.update_game_running_status_task = None
        self.update_game_installation_status_task = None
        self.check_for_new_games_task = None
        self.running_games = {}
        self.launching_lock = None

    async def authenticate(self, stored_credentials=None):
        if not stored_credentials:
            return NextStep(
                "web_session",
                AUTH_PARAMS,
                cookies=[Cookie("passedICO", "true", ".bethesda.net")])
        try:
            log.info("Got stored credentials")
            cookies = pickle.loads(
                bytes.fromhex(stored_credentials['cookie_jar']))
            cookies_parsed = []
            for cookie in cookies:
                if cookie.key in cookies_parsed and cookie.domain:
                    self._http_client.update_cookies(
                        {cookie.key: cookie.value})
                elif cookie.key not in cookies_parsed:
                    self._http_client.update_cookies(
                        {cookie.key: cookie.value})
                cookies_parsed.append(cookie.key)

            log.info("Finished parsing stored credentials, authenticating")
            user = await self._http_client.authenticate()

            return Authentication(user_id=user['user_id'],
                                  user_name=user['display_name'])
        except Exception as e:
            log.error(
                f"Couldn't authenticate with stored credentials {repr(e)}")
            raise InvalidCredentials()

    async def pass_login_credentials(self, step, credentials, cookies):
        cookiez = {}
        for cookie in cookies:
            cookiez[cookie['name']] = cookie['value']
        self._http_client.update_cookies(cookiez)

        try:
            user = await self._http_client.authenticate()
        except Exception as e:
            log.error(repr(e))
            raise InvalidCredentials()

        return Authentication(user_id=user['user_id'],
                              user_name=user['display_name'])

    async def get_owned_games(self):
        owned_ids = []
        matched_ids = []
        games_to_send = []
        pre_orders = []
        try:
            owned_ids = await self.bethesda_client.get_owned_ids()
        except UnknownError as e:
            log.warning(f"No owned games detected {repr(e)}")

        log.info(f"Owned Ids: {owned_ids}")

        if owned_ids:
            for entitlement_id in owned_ids:
                for product in self.products_cache:
                    if 'reference_id' in self.products_cache[product]:
                        for reference_id in self.products_cache[product][
                                'reference_id']:
                            if entitlement_id in reference_id:
                                self.products_cache[product]['owned'] = True
                                matched_ids.append(entitlement_id)
            pre_orders = set(owned_ids) - set(matched_ids)

        for pre_order in pre_orders:
            pre_order_details = await self.bethesda_client.get_game_details(
                pre_order)
            if pre_order_details and 'Entry' in pre_order_details:
                for entry in pre_order_details['Entry']:
                    if 'fields' in entry and 'productName' in entry['fields']:
                        if entry['fields'][
                                'productName'] in self.products_cache:
                            self.products_cache[
                                entry['fields']['productName']]['owned'] = True
                        else:
                            games_to_send.append(
                                Game(
                                    pre_order, entry['fields']['productName'] +
                                    " (Pre Order)", None,
                                    LicenseInfo(LicenseType.SinglePurchase)))
                        break

        for product in self.products_cache:
            if self.products_cache[product]["owned"] and self.products_cache[
                    product]["free_to_play"]:
                games_to_send.append(
                    Game(self.products_cache[product]['local_id'], product,
                         None, LicenseInfo(LicenseType.FreeToPlay)))
            elif self.products_cache[product]["owned"]:
                games_to_send.append(
                    Game(self.products_cache[product]['local_id'], product,
                         None, LicenseInfo(LicenseType.SinglePurchase)))

        log.info(f"Games to send (with free games): {games_to_send}")
        self.owned_games_cache = games_to_send
        return games_to_send

    async def get_local_games(self):
        local_games = []
        installed_products = self.local_client.get_installed_games(
            self.products_cache)
        log.info(f"Installed products {installed_products}")
        for product in self.products_cache:
            for installed_product in installed_products:
                if installed_products[
                        installed_product] == self.products_cache[product][
                            'local_id']:
                    self.products_cache[product]['installed'] = True
                    local_games.append(
                        LocalGame(installed_products[installed_product],
                                  LocalGameState.Installed))

        self._asked_for_local = True
        log.info(f"Returning local games {local_games}")
        return local_games

    async def install_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return

        if not self.local_client.is_installed:
            await self._open_betty_browser()
            return

        if self.local_client.is_running:
            self.local_client.focus_client_window()
            await self.launch_game(game_id)
        else:
            uuid = None
            for product in self.products_cache:

                if self.products_cache[product]['local_id'] == game_id:
                    if self.products_cache[product]['installed']:
                        log.warning(
                            "Got install on already installed game, launching")
                        return await self.launch_game(game_id)
                    uuid = "\"" + self.products_cache[product]['uuid'] + "\""
            cmd = "\"" + self.local_client.client_exe_path + "\"" + f" --installproduct={uuid}"
            log.info(f"Calling install game with command {cmd}")
            subprocess.Popen(cmd, shell=True)

    async def launch_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return
        if not self.local_client.is_installed:
            await self._open_betty_browser()
            return

        for product in self.products_cache:
            if self.products_cache[product]['local_id'] == game_id:
                if not self.products_cache[product]['installed']:
                    if not self.local_client.is_running:
                        log.warning(
                            "Got launch on a not installed game, installing")
                        return await self.install_game(game_id)
                else:
                    if not self.local_client.is_running:
                        self.launching_lock = time.time() + 45
                    else:
                        self.launching_lock = time.time() + 30
                    self.running_games[game_id] = None
                    self.update_local_game_status(
                        LocalGame(
                            game_id,
                            LocalGameState.Installed | LocalGameState.Running))
                    self.update_game_running_status_task.cancel()

        log.info(f"Calling launch command for id {game_id}")
        cmd = f"start bethesdanet://run/{game_id}"
        subprocess.Popen(cmd, shell=True)

    async def uninstall_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return

        if not self.local_client.is_installed:
            await self._open_betty_browser()
            return

        for product in self.products_cache:
            if self.products_cache[product]['local_id'] == game_id:
                if not self.products_cache[product]['installed']:
                    return

        log.info(f"Calling uninstall command for id {game_id}")
        cmd = f"start bethesdanet://uninstall/{game_id}"

        subprocess.Popen(cmd, shell=True)

        if self.local_client.is_running:
            await asyncio.sleep(
                2)  # QOL, bethesda slowly reacts to uninstall command,
            self.local_client.focus_client_window()

    async def _open_betty_browser(self):
        url = "https://bethesda.net/game/bethesda-launcher"
        log.info(f"Opening Bethesda website on url {url}")
        webbrowser.open(url)

    async def _heavy_installation_status_check(self):
        installed_products = self.local_client.get_installed_games(
            self.products_cache)
        products_cache_installed_products = {}

        for product in self.products_cache:
            if self.products_cache[product]['installed']:
                products_cache_installed_products[
                    product] = self.products_cache[product]['local_id']

        for installed_product in installed_products:
            if installed_product not in products_cache_installed_products:
                self.products_cache[installed_product]["installed"] = True
                self.update_local_game_status(
                    LocalGame(installed_products[installed_product],
                              LocalGameState.Installed))

        for installed_product in products_cache_installed_products:
            if installed_product not in installed_products:
                self.products_cache[installed_product]["installed"] = False
                self.update_local_game_status(
                    LocalGame(
                        products_cache_installed_products[installed_product],
                        LocalGameState.None_))

    def _light_installation_status_check(self):
        for local_game in self.local_client.local_games_cache:
            local_game_installed = self.local_client.is_local_game_installed(
                self.local_client.local_games_cache[local_game])
            if local_game_installed and not self.products_cache[local_game][
                    "installed"]:
                self.products_cache[local_game]["installed"] = True
                self.update_local_game_status(
                    LocalGame(
                        self.local_client.local_games_cache[local_game]
                        ['local_id'], LocalGameState.Installed))
            elif not local_game_installed and self.products_cache[local_game][
                    "installed"]:
                self.products_cache[local_game]["installed"] = False
                self.update_local_game_status(
                    LocalGame(
                        self.local_client.local_games_cache[local_game]
                        ['local_id'], LocalGameState.None_))

    async def update_game_installation_status(self):

        if self.local_client.clientgame_changed():
            await asyncio.sleep(1)
            await self._heavy_installation_status_check()
        else:
            self._light_installation_status_check()

    async def update_game_running_status(self):

        process_iter_interval = 0.10
        dont_downgrade_status = False

        if self.launching_lock and self.launching_lock >= time.time():
            dont_downgrade_status = True
            process_iter_interval = 0.01

        for running_game in self.running_games.copy():
            if not self.running_games[running_game] and dont_downgrade_status:
                log.info(f"Found 'just launched' game {running_game}")
                continue
            elif not self.running_games[running_game]:
                log.info(
                    f"Found 'just launched' game but its still without pid and its time run out {running_game}"
                )
                self.running_games.pop(running_game)
                self.update_local_game_status(
                    LocalGame(running_game, LocalGameState.Installed))
                continue

            if self.running_games[running_game].is_running():
                return
            self.running_games.pop(running_game)
            self.update_local_game_status(
                LocalGame(running_game, LocalGameState.Installed))

        for process in psutil.process_iter(attrs=['name'], ad_value=''):
            await asyncio.sleep(process_iter_interval)
            for local_game in self.local_client.local_games_cache:
                try:
                    if process.name().lower(
                    ) in self.local_client.local_games_cache[local_game][
                            'execs']:
                        log.info(f"Found a running game! {local_game}")
                        local_id = self.local_client.local_games_cache[
                            local_game]['local_id']
                        if local_id not in self.running_games:
                            self.update_local_game_status(
                                LocalGame(
                                    local_id, LocalGameState.Installed
                                    | LocalGameState.Running))
                        self.running_games[local_id] = process
                        return
                except (psutil.AccessDenied, psutil.NoSuchProcess):
                    break

        await asyncio.sleep(3)

    async def check_for_new_games(self):
        owned_games = await self.get_owned_games()
        for owned_game in owned_games:
            if owned_game not in self.owned_games_cache:
                self.add_game(owned_game)
        self.owned_games_cache = owned_games
        await asyncio.sleep(60)

    def tick(self):

        if self._asked_for_local and (
                not self.update_game_installation_status_task
                or self.update_game_installation_status_task.done()):
            self.update_game_installation_status_task = asyncio.create_task(
                self.update_game_installation_status())

        if self._asked_for_local and (
                not self.update_game_running_status_task
                or self.update_game_running_status_task.done()):
            self.update_game_running_status_task = asyncio.create_task(
                self.update_game_running_status())

        if self.owned_games_cache and (not self.check_for_new_games_task or
                                       self.check_for_new_games_task.done()):
            self.check_for_new_games_task = asyncio.create_task(
                self.check_for_new_games())

    def shutdown(self):
        asyncio.create_task(self._http_client.close())