Skip to content

Commit

Permalink
version 0.36
Browse files Browse the repository at this point in the history
  • Loading branch information
FriendsOfGalaxy committed Jun 26, 2020
1 parent ae392cf commit 892f1b3
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 187 deletions.
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-r app.txt
invoke==1.2.0
pytest==5.2.0
pytest==5.4.1
pytest-asyncio==0.10.0
pytest-mock==1.10.3
pytest-flakes==4.0.0
Expand Down
69 changes: 3 additions & 66 deletions src/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import time
import random
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, List, NewType, Optional, Tuple
from typing import Dict, List, NewType, Optional

import aiohttp
from galaxy.api.errors import (
Expand All @@ -21,13 +20,6 @@
Timestamp = NewType("Timestamp", int)


@dataclass
class ProductInfo:
offer_id: OfferId
display_name: str
master_title_id: MasterTitleId
achievement_set: Optional[AchievementSet] = None


class CookieJar(aiohttp.CookieJar):
def __init__(self):
Expand Down Expand Up @@ -330,61 +322,6 @@ async def get_friends(self, user_id):
logging.exception("Can not parse backend response: %s", await response.text())
raise UnknownBackendResponse()

async def get_owned_games(self, user_id) -> Dict[OfferId, ProductInfo]:
response = await self._http_client.get("{base_api}/atom/users/{user_id}/other/{other_user_id}/games".format(
base_api=self._get_api_host(),
user_id=user_id,
other_user_id=user_id
))

'''
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<productInfoList>
<productInfo>
<productId>OFB-EAST:109552153</productId>
<displayProductName>Battlefield 4™ (Trial)</displayProductName>
<cdnAssetRoot>http://static.cdn.ea.com/ebisu/u/f/products/1015365</cdnAssetRoot>
<imageServer>https://Eaassets-a.akamaihd.net/origin-com-store-final-assets-prod</imageServer>
<packArtSmall>/76889/63.0x89.0/1007968_SB_63x89_en_US_^_2013-11-13-18-04-11_e8670.jpg</packArtSmall>
<packArtMedium>/76889/142.0x200.0/1007968_MB_142x200_en_US_^_2013-11-13-18-04-08_2ff.jpg</packArtMedium>
<packArtLarge>/76889/231.0x326.0/1007968_LB_231x326_en_US_^_2013-11-13-18-04-04_18173.jpg</packArtLarge>
<softwareList>
<software softwarePlatform="PCWIN">
<achievementSetOverride>51302_76889_50844</achievementSetOverride>
</software>
</softwareList>
<masterTitleId>76889</masterTitleId>
<gameDistributionSubType>Limited Trial</gameDistributionSubType>
</productInfo>
</productInfoList>
'''
try:
def parse_product(product_info_xml) -> Tuple[OfferId, ProductInfo]:
def get_tag(tag_name) -> str:
return product_info_xml.find(tag_name).text

def parse_achievement_set():
set_xml = product_info_xml.find(".//softwareList/*/achievementSetOverride")
if set_xml is None:
return None
return set_xml.text

return OfferId(get_tag("productId")), ProductInfo(
offer_id=OfferId(get_tag("productId")),
display_name=get_tag("displayProductName"),
master_title_id=MasterTitleId(get_tag("masterTitleId")),
achievement_set=parse_achievement_set()
)

content = await response.text()
return dict(
parse_product(product_info_xml)
for product_info_xml in ET.ElementTree(ET.fromstring(content)).iter("productInfo")
)
except (ET.ParseError, AttributeError, ValueError):
logging.exception("Can not parse backend response: %s", await response.text())
raise UnknownBackendResponse()

async def get_lastplayed_games(self, user_id) -> Dict[MasterTitleId, Timestamp]:
response = await self._http_client.get("{base_api}/atom/users/{user_id}/games/lastplayed".format(
base_api=self._get_api_host(),
Expand Down Expand Up @@ -534,9 +471,9 @@ async def get_subscriptions(self, user_id) -> List[Subscription]:
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()
raise UnknownBackendResponse()
else:
logging.debug(f'no subscription active')
logging.debug('no subscription active')
return [subs['standard'], subs['premium']]

async def get_games_in_subscription(self, tier):
Expand Down
41 changes: 36 additions & 5 deletions src/local_games.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import glob
import re
import functools
import logging
import os
import platform
Expand All @@ -13,7 +15,7 @@
import psutil

from dataclasses import dataclass
from enum import Enum, auto
from enum import Enum, auto, Flag
from typing import Iterator, Tuple

from galaxy.api.errors import FailedParsingManifest
Expand Down Expand Up @@ -43,7 +45,7 @@ class _State(Enum):
kDecrypting = auto()
kReadyToInstall = auto()
kPreInstall = auto()
kInstalling = auto()
kInstalling = auto() # This status is used for games which are installing or updating
kPostInstall = auto()
kFetchLicense = auto()
kCompleted = auto()
Expand All @@ -56,6 +58,13 @@ class _Manifest:
prev_state: _State
ddinstallalreadycompleted: str
dipinstallpath: str
ddinitialdownload: str


class OriginGameState(Flag):
None_ = 0
Installed = 1
Playable = 2


def _parse_msft_file(filepath):
Expand All @@ -68,8 +77,9 @@ def _parse_msft_file(filepath):
prev_state = _State[parsed_data.get("previousstate", _State.kInvalid.name)]
ddinstallalreadycompleted = parsed_data.get("ddinstallalreadycompleted", "0")
dipinstallpath = parsed_data.get("dipinstallpath", "")
ddinitialdownload = parsed_data.get("ddinitialdownload", "0")

return _Manifest(game_id, state, prev_state, ddinstallalreadycompleted, dipinstallpath)
return _Manifest(game_id, state, prev_state, ddinstallalreadycompleted, dipinstallpath, ddinitialdownload)


def get_local_games_manifests(manifests_stats):
Expand All @@ -84,6 +94,14 @@ def get_local_games_manifests(manifests_stats):
return manifests


def parse_map_crc_for_total_size(filepath) -> int:
with open(filepath, 'r', encoding='utf-16-le') as f:
content = f.read()
pattern = r'size=(\d+)'
sizes = re.findall(pattern, content)
return functools.reduce(lambda a, b : a + int(b), sizes, 0)


if platform.system() == "Windows":
def get_process_info(pid) -> Tuple[int, Optional[str]]:
_MAX_PATH = 260
Expand Down Expand Up @@ -154,6 +172,18 @@ def process_iter() -> Iterator[Tuple[int, str]]:
logging.exception("Failed to get information for PID=%s" % pid)


def read_state(manifest : _Manifest) -> OriginGameState:
game_state = OriginGameState.None_
if manifest.state == _State.kReadyToStart and manifest.prev_state == _State.kCompleted:
game_state |= OriginGameState.Installed
game_state |= OriginGameState.Playable
if manifest.ddinstallalreadycompleted == "1" and manifest.state != _State.kPostInstall:
game_state |= OriginGameState.Playable
if manifest.state in (_State.kInstalling, _State.kInitializing, _State.kTransferring, _State.kEnqueued, _State.kPostInstall) and manifest.ddinitialdownload == "0":
game_state |= OriginGameState.Installed
return game_state


def get_local_games_from_manifests(manifests):
local_games = []

Expand All @@ -169,8 +199,9 @@ def is_game_running(game_folder_name):

state = LocalGameState.None_

if ((manifest.state == _State.kReadyToStart and manifest.prev_state == _State.kCompleted)
or manifest.ddinstallalreadycompleted == "1"):
game_state = read_state(manifest)
if OriginGameState.Installed in game_state \
or OriginGameState.Playable in game_state:
state |= LocalGameState.Installed

if manifest.dipinstallpath and is_game_running(manifest.dipinstallpath):
Expand Down
47 changes: 37 additions & 10 deletions src/plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import pathlib
import json
import logging
import platform
Expand All @@ -15,10 +16,13 @@
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, Subscription, SubscriptionGame
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
from backend import AuthenticatedHttpClient, MasterTitleId, OfferId, OriginBackendClient, Timestamp, AchievementSet
from local_games import get_local_content_path, LocalGames, parse_map_crc_for_total_size
from uri_scheme_handler import is_uri_handler_installed
from version import __version__
import re
Expand Down Expand Up @@ -52,7 +56,6 @@ def regex_pattern(regex):


class OriginPlugin(Plugin):
# pylint: disable=abstract-method
def __init__(self, reader, writer, token):
super().__init__(Platform.Origin, __version__, reader, writer, token)
self._user_id = None
Expand Down Expand Up @@ -131,18 +134,30 @@ async def get_owned_games(self):

return games

@staticmethod
def _get_achievement_set_override(offer) -> Optional[AchievementSet]:
potential_achievement_set = None
for achievement_set in offer["platforms"]:
potential_achievement_set = achievement_set["achievementSetOverride"]
if achievement_set["platform"] == "PCWIN":
return potential_achievement_set
return potential_achievement_set

async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
self._check_authenticated()

owned_offers = await self._get_owned_offers()
achievement_sets: Dict[OfferId, AchievementSet] = dict()
for offer in owned_offers:
achievement_sets[offer["offerId"]] = self._get_achievement_set_override(offer)
return AchievementsImportContext(
owned_games=await self._backend_client.get_owned_games(self._user_id),
owned_games=achievement_sets,
achievements=await self._backend_client.get_achievements(self._persona_id)
)

async def get_unlocked_achievements(self, game_id: str, context: AchievementsImportContext) -> List[Achievement]:
try:
achievements_set = context.owned_games[game_id].achievement_set
except (KeyError, AttributeError):
achievements_set = context.owned_games[game_id]
except KeyError:
logging.exception("Game '{}' not found amongst owned".format(game_id))
raise UnknownBackendResponse()

Expand Down Expand Up @@ -219,12 +234,12 @@ async def prepare_subscription_games_context(self, subscription_names: List[str]
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!")
logging.error("Assertion: 'Galaxy passed unknown subscription name %s. This should not happen!", sub_name)
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]:
async def get_subscription_games(self, subscription_name: str, context: Any) -> AsyncGenerator[List[SubscriptionGame], None]:
if context and subscription_name:
yield context[subscription_name]

Expand Down Expand Up @@ -267,6 +282,18 @@ async def notify_local_games_changed():
loop = asyncio.get_running_loop()
asyncio.create_task(notify_local_games_changed())

async def prepare_local_size_context(self, game_ids) -> Dict[str, pathlib.PurePath]:
game_id_crc_map = {}
for filepath, manifest in zip(self._local_games._manifests_stats.keys(), self._local_games._manifests):
game_id_crc_map[manifest.game_id] = pathlib.PurePath(filepath).parent / 'map.crc'
return game_id_crc_map

async def get_local_size(self, game_id, context: Dict[str, pathlib.PurePath]) -> Optional[int]:
try:
return parse_map_crc_for_total_size(context[game_id])
except (KeyError, FileNotFoundError) as e:
raise UnknownError(f"Manifest for game {game_id} is not found: {repr(e)} | context: {context}")

@staticmethod
def _get_multiplayer_id(offer) -> Optional[MultiplayerId]:
for game_platform in offer["platforms"]:
Expand Down
10 changes: 8 additions & 2 deletions src/version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
__version__ = "0.35"
__version__ = "0.36"

__changelog__ = {
"0.36":
"""
- better handle installation status of games
- fix error on retrieving achievements for some games
- added support for local sizes
""",
"0.35":
"""
- added support for subscriptions
Expand All @@ -14,4 +20,4 @@
- fix rare bug while parsing game times (#16)
- fix handling status 400 with "login_error": go to "Credentials Lost" instead of "Offline. Retry"
"""
}
}
1 change: 1 addition & 0 deletions tests/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def test_integration():
"ShutdownPlatformClient",
"ImportFriends",
"ImportGameTime",
"ImportLocalSize"
}
}
}
Expand Down
Loading

0 comments on commit 892f1b3

Please sign in to comment.