From a0499a0f144b995a90d5b4133193b20f6a6ca1fb Mon Sep 17 00:00:00 2001 From: FriendsOfGalaxy Date: Fri, 3 Apr 2020 10:54:16 +0200 Subject: [PATCH] version 0.35 --- requirements/app.txt | 2 +- src/backend.py | 65 ++++++++++++++++++++++++++++++- src/plugin.py | 28 +++++++++++++- src/version.py | 6 ++- tests/conftest.py | 3 +- tests/integration_test.py | 2 + tests/test_subscriptions.py | 77 +++++++++++++++++++++++++++++++++++++ 7 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 tests/test_subscriptions.py diff --git a/requirements/app.txt b/requirements/app.txt index 6bd5f13..3f9416c 100755 --- a/requirements/app.txt +++ b/requirements/app.txt @@ -1,2 +1,2 @@ -galaxy.plugin.api==0.63 +galaxy.plugin.api==0.65 pyobjc-framework-CoreServices==5.1.2; sys_platform == 'darwin' diff --git a/src/backend.py b/src/backend.py index 85d923c..70819ef 100755 --- a/src/backend.py +++ b/src/backend.py @@ -11,7 +11,7 @@ AccessDenied, AuthenticationRequired, BackendError, BackendNotAvailable, BackendTimeout, NetworkError, UnknownBackendResponse ) -from galaxy.api.types import Achievement +from galaxy.api.types import Achievement, SubscriptionGame, Subscription from galaxy.http import HttpClient from yarl import URL @@ -110,7 +110,6 @@ async def _get_access_token(self): try: data = await response.json(content_type=None) self._access_token = data["access_token"] - self._log_session_details() except (TypeError, ValueError, KeyError) as e: self._log_session_details() try: @@ -490,3 +489,65 @@ async def get_hidden_games(self, user_id): except (ET.ParseError, AttributeError, ValueError): logging.exception("Can not parse backend response: %s", await response.text()) raise UnknownBackendResponse() + + async def _get_subscription_status(self, subscription_uri): + def parse_timestamp(timestamp: str) -> Timestamp: + return Timestamp( + int((datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S") - datetime(1970, 1, 1)).total_seconds())) + + response = await self._http_client.get(subscription_uri) + try: + data = await response.json() + if data and data['Subscription']['status'].lower() == 'enabled': + return {'tier': data['Subscription']['subscriptionLevel'].lower(), + 'end_time': parse_timestamp(data['Subscription']['nextBillingDate'])} + else: + return None + except (ValueError, KeyError) as e: + logging.exception("Can not parse backend response while getting subs details: %s, error %s", await response.text(), repr(e)) + raise UnknownBackendResponse() + + async def _get_subscription_uri(self, user_id): + url = f"https://gateway.ea.com/proxy/subscription/pids/{user_id}/subscriptionsv2/groups/Origin Membership" + response = await self._http_client.get(url) + try: + data = await response.json() + if 'subscriptionUri' in data: + return f"https://gateway.ea.com/proxy/subscription/pids/{user_id}{data['subscriptionUri'][0]}" + else: + return None + except (ValueError, KeyError) as e: + logging.exception("Can not parse backend response while getting subs uri: %s, error %s", await response.text(), repr(e)) + raise UnknownBackendResponse() + + async def get_subscriptions(self, user_id) -> List[Subscription]: + subs = {'standard': Subscription(subscription_name='Origin Access Basic', owned=False), + 'premium': Subscription(subscription_name='Origin Access Premier', owned=False)} + + subscription_uri = await self._get_subscription_uri(user_id) + if subscription_uri: + sub_status = await self._get_subscription_status(subscription_uri) + logging.debug(f'sub_status: {sub_status}') + try: + if sub_status: + subs[sub_status['tier']].owned = True + subs[sub_status['tier']].end_time = sub_status['end_time'] + except (ValueError, KeyError) as e: + logging.exception("Unknown subscription tier, error %s", repr(e)) + raise UnknownBackendResponse() + else: + logging.debug(f'no subscription active') + return [subs['standard'], subs['premium']] + + async def get_games_in_subscription(self, tier): + url = f"https://api3.origin.com/ecommerce2/vaultInfo/Origin Membership/tiers/{tier}" + headers = { + "Accept": "application/vnd.origin.v3+json; x-cache/force-write" + } + response = await self._http_client.get(url, headers=headers) + try: + games = await response.json() + return [SubscriptionGame(game_title=game['displayName'], game_id=game['offerId']) for game in games['game']] + except (ValueError, KeyError) as e: + logging.exception("Can not parse backend response while getting subs games: %s, error %s", await response.text(), repr(e)) + raise UnknownBackendResponse() diff --git a/src/plugin.py b/src/plugin.py index 50a3052..bbad4a5 100755 --- a/src/plugin.py +++ b/src/plugin.py @@ -8,14 +8,14 @@ import webbrowser from collections import namedtuple from functools import partial -from typing import Any, Callable, Dict, List, NewType, Optional +from typing import Any, Callable, Dict, List, NewType, Optional, AsyncGenerator from galaxy.api.consts import LicenseType, Platform from galaxy.api.errors import ( AccessDenied, AuthenticationRequired, InvalidCredentials, UnknownBackendResponse, UnknownError ) from galaxy.api.plugin import create_and_run_plugin, Plugin -from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LicenseInfo, NextStep, GameLibrarySettings +from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LicenseInfo, NextStep, GameLibrarySettings, Subscription, SubscriptionGame from backend import AuthenticatedHttpClient, MasterTitleId, OfferId, OriginBackendClient, Timestamp from local_games import get_local_content_path, LocalGames @@ -204,6 +204,30 @@ async def _get_owned_offers(self): offer_ids = [entitlement["offerId"] for entitlement in entitlements] return await self._get_offers(offer_ids) + async def get_subscriptions(self) -> List[Subscription]: + self._check_authenticated() + return await self._backend_client.get_subscriptions(user_id=self._user_id) + + async def prepare_subscription_games_context(self, subscription_names: List[str]) -> Any: + self._check_authenticated() + subscription_name_to_tier = { + 'Origin Access Basic': 'standard', + 'Origin Access Premier': 'premium' + } + subscriptions = {} + for sub_name in subscription_names: + try: + tier = subscription_name_to_tier[sub_name] + except KeyError: + logging.error(f"Assertion: 'Galaxy passed unknown subscription name {sub_name}. This should not happen!") + raise UnknownError(f'Unknown subscription name {sub_name}!') + subscriptions[sub_name] = await self._backend_client.get_games_in_subscription(tier) + return subscriptions + + async def get_subscription_games(self, subscription_name: str, context: Any) -> AsyncGenerator[List[SubscriptionGame],None]: + if context and subscription_name: + yield context[subscription_name] + async def get_local_games(self): if self._local_games_update_in_progress: logging.debug("LocalGames.update in progress, returning cached values") diff --git a/src/version.py b/src/version.py index c78e067..53a95ae 100755 --- a/src/version.py +++ b/src/version.py @@ -1,6 +1,10 @@ -__version__ = "0.34.1" +__version__ = "0.35" __changelog__ = { + "0.35": + """ + - added support for subscriptions + """, "0.34.1": """ - add extended logging to find session expiration time mechanism diff --git a/tests/conftest.py b/tests/conftest.py index 1e6eefa..a45996d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -72,7 +72,8 @@ def backend_client(): mock.get_lastplayed_games = MagicMock() mock.get_hidden_games = AsyncMock() mock.get_favorite_games = AsyncMock() - + mock.get_games_in_subscription = AsyncMock() + mock.get_subscriptions = AsyncMock() return mock diff --git a/tests/integration_test.py b/tests/integration_test.py index 2513619..ca2a0d9 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -56,6 +56,8 @@ def test_integration(): "ImportOwnedGames", "ImportAchievements", "ImportInstalledGames", + "ImportSubscriptions", + "ImportSubscriptionGames", "ImportGameLibrarySettings", "LaunchGame", "InstallGame", diff --git a/tests/test_subscriptions.py b/tests/test_subscriptions.py new file mode 100644 index 0000000..b05bfb5 --- /dev/null +++ b/tests/test_subscriptions.py @@ -0,0 +1,77 @@ +import pytest +from galaxy.api.types import SubscriptionGame, Subscription +from galaxy.api.errors import BackendError + + +SUBSCRIPTION_OWNED_ID = 'Origin Access Premier' +SUBSCRIPTIONS_NOT_OWNED = [Subscription(subscription_name='Origin Access Basic', owned=False, end_time=None), + Subscription(subscription_name='Origin Access Premier', owned=False, end_time=None)] +SUBSCRIPTIONS_OWNED = [Subscription(subscription_name='Origin Access Basic', owned=False, end_time=None), + Subscription(subscription_name='Origin Access Premier', owned=True, end_time=1581331712)] + +SUBSCRIPTIONS_CONTEXT = {'Origin Access Premier': [ + SubscriptionGame(game_title='Mass Effect 3 N7 Digital Deluxe Edition - PC - WW (Origin/3PDD)', + game_id='DR:230773600', start_time=None, end_time=None), + SubscriptionGame(game_title='LEGRAND LEGACY: Tale of the Fatebounds - PC - WW - (Origin)', + game_id='Origin.OFR.50.0003727', start_time=None, end_time=None), + SubscriptionGame(game_title='Mable & the Wood - PC - WW - (Origin)', game_id='Origin.OFR.50.0003777', + start_time=None, end_time=None), + SubscriptionGame(game_title='Worms W.M.D - PC - WW - (Origin)', game_id='Origin.OFR.50.0003802', start_time=None, + end_time=None)]} + +SUBSCRIPTION_GAMES = [ + SubscriptionGame(game_title='Mass Effect 3 N7 Digital Deluxe Edition - PC - WW (Origin/3PDD)', + game_id='DR:230773600', start_time=None, end_time=None), + SubscriptionGame(game_title='LEGRAND LEGACY: Tale of the Fatebounds - PC - WW - (Origin)', + game_id='Origin.OFR.50.0003727', start_time=None, end_time=None), + SubscriptionGame(game_title='Mable & the Wood - PC - WW - (Origin)', game_id='Origin.OFR.50.0003777', + start_time=None, end_time=None), + SubscriptionGame(game_title='Worms W.M.D - PC - WW - (Origin)', game_id='Origin.OFR.50.0003802', start_time=None, + end_time=None)] + + +@pytest.mark.asyncio +async def test_subscription_not_owned( + authenticated_plugin, + backend_client, +): + backend_client.get_subscriptions.return_value = SUBSCRIPTIONS_NOT_OWNED + assert SUBSCRIPTIONS_NOT_OWNED == await authenticated_plugin.get_subscriptions() + + +@pytest.mark.asyncio +async def test_subscription_owned( + authenticated_plugin, + backend_client, +): + backend_client.get_subscriptions.return_value = SUBSCRIPTIONS_OWNED + assert SUBSCRIPTIONS_OWNED == await authenticated_plugin.get_subscriptions() + + +@pytest.mark.asyncio +async def test_prepare_subscription_games_context( + authenticated_plugin, + backend_client, +): + backend_client.get_games_in_subscription.return_value = SUBSCRIPTION_GAMES + assert SUBSCRIPTIONS_CONTEXT == await authenticated_plugin.prepare_subscription_games_context([SUBSCRIPTION_OWNED_ID]) + + +@pytest.mark.asyncio +async def test_prepare_subscription_games_context_error( + authenticated_plugin, + backend_client, +): + backend_client.get_games_in_subscription.side_effect = BackendError() + with pytest.raises(BackendError): + await authenticated_plugin.prepare_subscription_games_context([SUBSCRIPTION_OWNED_ID]) + + +@pytest.mark.asyncio +async def test_subscription_games( + authenticated_plugin, +): + async for sub_games in authenticated_plugin.get_subscription_games(SUBSCRIPTION_OWNED_ID, SUBSCRIPTIONS_CONTEXT): + assert sub_games == SUBSCRIPTION_GAMES + +