From 892f1b302bda31194efc281091a288201e48839c Mon Sep 17 00:00:00 2001 From: FriendsOfGalaxy Date: Fri, 26 Jun 2020 15:02:34 +0200 Subject: [PATCH] version 0.36 --- requirements/dev.txt | 2 +- src/backend.py | 69 +-------------- src/local_games.py | 41 +++++++-- src/plugin.py | 47 +++++++--- src/version.py | 10 ++- tests/integration_test.py | 1 + tests/test_achievements.py | 129 +++++++--------------------- tests/test_game_library_settings.py | 5 +- tests/test_local_games.py | 4 +- tests/test_local_size.py | 63 ++++++++++++++ 10 files changed, 184 insertions(+), 187 deletions(-) create mode 100644 tests/test_local_size.py diff --git a/requirements/dev.txt b/requirements/dev.txt index f19937e..22b370f 100755 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -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 diff --git a/src/backend.py b/src/backend.py index 70819ef..a3d1bdf 100755 --- a/src/backend.py +++ b/src/backend.py @@ -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 ( @@ -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): @@ -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 - )) - - ''' - - - - OFB-EAST:109552153 - Battlefield 4™ (Trial) - http://static.cdn.ea.com/ebisu/u/f/products/1015365 - https://Eaassets-a.akamaihd.net/origin-com-store-final-assets-prod - /76889/63.0x89.0/1007968_SB_63x89_en_US_^_2013-11-13-18-04-11_e8670.jpg - /76889/142.0x200.0/1007968_MB_142x200_en_US_^_2013-11-13-18-04-08_2ff.jpg - /76889/231.0x326.0/1007968_LB_231x326_en_US_^_2013-11-13-18-04-04_18173.jpg - - - 51302_76889_50844 - - - 76889 - Limited Trial - - - ''' - 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(), @@ -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): diff --git a/src/local_games.py b/src/local_games.py index 98a255a..30497eb 100755 --- a/src/local_games.py +++ b/src/local_games.py @@ -1,4 +1,6 @@ import glob +import re +import functools import logging import os import platform @@ -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 @@ -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() @@ -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): @@ -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): @@ -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 @@ -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 = [] @@ -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): diff --git a/src/plugin.py b/src/plugin.py index bbad4a5..263c8ad 100755 --- a/src/plugin.py +++ b/src/plugin.py @@ -1,4 +1,5 @@ import asyncio +import pathlib import json import logging import platform @@ -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 @@ -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 @@ -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() @@ -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] @@ -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"]: diff --git a/src/version.py b/src/version.py index 53a95ae..fb8d255 100755 --- a/src/version.py +++ b/src/version.py @@ -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 @@ -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" """ -} \ No newline at end of file +} diff --git a/tests/integration_test.py b/tests/integration_test.py index ca2a0d9..760fe77 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -64,6 +64,7 @@ def test_integration(): "ShutdownPlatformClient", "ImportFriends", "ImportGameTime", + "ImportLocalSize" } } } diff --git a/tests/test_achievements.py b/tests/test_achievements.py index cc87cbf..8b94ff4 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -2,21 +2,27 @@ from galaxy.api.errors import AuthenticationRequired from galaxy.api.types import Achievement -from backend import OriginBackendClient, ProductInfo +from backend import OriginBackendClient from plugin import AchievementsImportContext -OWNED_GAMES_SIMPLE_SIMPLE_ACHIEVEMENTS = { - "DR:119971300": ProductInfo("DR:119971300", "Need For Speed™ Shift", "54856", None), - "OFB-EAST:109552409": ProductInfo("OFB-EAST:109552409", "The Sims™ 4", "55482", None), - "OFB-EAST:48217": ProductInfo("OFB-EAST:48217", "Plants vs. Zombies™ Game of the Year Edition", "180975", None), - "OFB-EAST:50885": ProductInfo("OFB-EAST:50885", "Dead Space™ 3", "52657", "50563_52657_50844"), - "Origin.OFR.50.0001672": ProductInfo("Origin.OFR.50.0001672", "THE WITCHER® 3: WILD HUNT", "192492", "50318_194188_50844"), - "Origin.OFR.50.0001452": ProductInfo("Origin.OFR.50.0001452", "Titanfall® 2", "192492", "193634_192492_50844") +from tests.async_mock import AsyncMock + +SIMPLE_ACHIEVEMENTS_SETS ={ + "DR:119971300": None, + "OFB-EAST:109552409": None, + "OFB-EAST:48217": None, + "OFB-EAST:50885": "50563_52657_50844", + "Origin.OFR.50.0001672": "50318_194188_50844", + "Origin.OFR.50.0001452": "193634_192492_50844" +} + +SPECIAL_ACHIEVEMENTS_SETS = { + "DR:225064100": "BF_BF3_PC" } -OWNED_GAME_SPECIAL_ACHIEVEMENTS = {"DR:225064100": ProductInfo("DR:225064100", "Battlefield 3™", "50182", "BF_BF3_PC")} +ACHIEVEMENT_SETS = {**SIMPLE_ACHIEVEMENTS_SETS, **SPECIAL_ACHIEVEMENTS_SETS} + -OWNED_GAMES = {**OWNED_GAMES_SIMPLE_SIMPLE_ACHIEVEMENTS, **OWNED_GAME_SPECIAL_ACHIEVEMENTS} ACHIEVEMENTS = { "DR:119971300": [], @@ -124,6 +130,11 @@ "193634_192492_50844": ACHIEVEMENTS["Origin.OFR.50.0001452"] } +SINGLE_ACHIEVEMENTS_SET_BACKEND_PARSED = { + "BF_BF3_PC": ACHIEVEMENTS["DR:225064100"] +} + + SINGLE_ACHIEVEMENTS_SET_BACKEND_RESPONSE = { "XP2ACH02_00": { "complete": True, @@ -147,16 +158,12 @@ } } -SINGLE_ACHIEVEMENTS_SET_BACKEND_PARSED = { - "BF_BF3_PC": ACHIEVEMENTS["DR:225064100"] -} - @pytest.mark.asyncio async def test_not_authenticated(plugin, http_client): http_client.is_authenticated.return_value = False with pytest.raises(AuthenticationRequired): - await plugin.prepare_achievements_context(OWNED_GAMES.keys()) + await plugin.prepare_achievements_context(None) @pytest.mark.asyncio @@ -166,9 +173,12 @@ async def test_achievements_context_preparation( persona_id, backend_client ): - await authenticated_plugin.prepare_achievements_context(OWNED_GAMES.keys()) + authenticated_plugin._get_owned_offers = AsyncMock() + authenticated_plugin._get_owned_offers.return_value = [] + await authenticated_plugin.prepare_achievements_context(None) + - backend_client.get_owned_games.assert_called_once_with(user_id) + authenticated_plugin._get_owned_offers.assert_called_once_with() backend_client.get_achievements.assert_called_once_with(persona_id) @@ -178,11 +188,11 @@ async def test_get_unlocked_achievements_simple( backend_client, user_id ): - for game_id in OWNED_GAMES_SIMPLE_SIMPLE_ACHIEVEMENTS.keys(): + for game_id in SIMPLE_ACHIEVEMENTS_SETS.keys(): assert ACHIEVEMENTS[game_id] == await authenticated_plugin.get_unlocked_achievements( game_id, context=AchievementsImportContext( - owned_games=OWNED_GAMES_SIMPLE_SIMPLE_ACHIEVEMENTS, + owned_games=SIMPLE_ACHIEVEMENTS_SETS, achievements=MULTIPLE_ACHIEVEMENTS_SETS_BACKEND_PARSED ) ) @@ -198,94 +208,17 @@ async def test_get_unlocked_achievements_explicit_call( ): backend_client.get_achievements.return_value = SINGLE_ACHIEVEMENTS_SET_BACKEND_PARSED - for game_id in OWNED_GAMES.keys(): + for game_id in ACHIEVEMENT_SETS.keys(): assert ACHIEVEMENTS[game_id] == await authenticated_plugin.get_unlocked_achievements( game_id, context=AchievementsImportContext( - owned_games=OWNED_GAMES, + owned_games=ACHIEVEMENT_SETS, achievements=MULTIPLE_ACHIEVEMENTS_SETS_BACKEND_PARSED ) ) backend_client.get_achievements.assert_called_once_with(persona_id, "BF_BF3_PC") - -BACKEND_GAMES_RESPONSE = """ - - - OFB-EAST:48217 - Plants vs. Zombies™ Game of the Year Edition - 180975 - Normal Game - - - DR:119971300 - Need For Speed™ Shift - 54856 - - - OFB-EAST:109552409 - The Sims™ 4 - 55482 - Normal Game - - - DR:225064100 - Battlefield 3™ - - - BF_BF3_PC - - - 50182 - Normal Game - - - OFB-EAST:50885 - Dead Space™ 3 - - - 50563_52657_50844 - - - 52657 - Normal Game - - - Origin.OFR.50.0001452 - Titanfall® 2 - - - 193634_192492_50844 - - - 192492 - Normal Game - - - Origin.OFR.50.0001672 - THE WITCHER® 3: WILD HUNT - - - 50318_194188_50844 - - - 192492 - Normal Game - - -""" - - -@pytest.mark.asyncio -async def test_owned_games_parsing(persona_id, http_client, create_xml_response): - http_client.get.return_value = create_xml_response(BACKEND_GAMES_RESPONSE) - - assert OWNED_GAMES == await OriginBackendClient(http_client).get_owned_games(persona_id) - - http_client.get.assert_called_once() - - @pytest.mark.asyncio @pytest.mark.parametrize("backend_response, parsed, explicit_set", [ ({}, {}, None), diff --git a/tests/test_game_library_settings.py b/tests/test_game_library_settings.py index 3fcc7fc..c87ca54 100644 --- a/tests/test_game_library_settings.py +++ b/tests/test_game_library_settings.py @@ -1,7 +1,6 @@ import pytest from galaxy.api.errors import AuthenticationRequired from galaxy.api.types import GameLibrarySettings -from galaxy.unittest.mock import async_return_value GAME_IDS = ['DR:119971300', 'OFB-EAST:48217', 'OFB-EAST:109552409', 'Origin.OFR.50.0002694'] @@ -81,7 +80,7 @@ async def test_get_favorite_games( user_id, http_client, ): - http_client.get.return_value = async_return_value(BACKEND_FAVORITES_RESPONSE) + http_client.get.return_value = BACKEND_FAVORITES_RESPONSE assert FAVORITE_GAMES == await backend_client.get_favorite_games(user_id) @@ -92,7 +91,7 @@ async def test_get_hidden_games( user_id, http_client, ): - http_client.get.return_value = async_return_value(BACKEND_HIDDEN_RESPONSE) + http_client.get.return_value = BACKEND_HIDDEN_RESPONSE assert HIDDEN_GAMES == await backend_client.get_hidden_games(user_id) diff --git a/tests/test_local_games.py b/tests/test_local_games.py index e09bcae..b9f8711 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -39,7 +39,7 @@ def test_bad_manifest_format(local_games_object, tmpdir): def test_installing(local_games_object, tmpdir): mfst_file = tmpdir.mkdir("GameName").join("gameid.mfst") - mfst_file.write("?currentstate=kInstalling&id=OFB-EAST%3a48217&previousstate=kPostTransfer") + mfst_file.write("?currentstate=kInstalling&id=OFB-EAST%3a48217&previousstate=kPostTransfer&ddinitialdownload=1") expected = [LocalGame("OFB-EAST:48217", LocalGameState.None_)] @@ -101,7 +101,7 @@ def test_notify_removed(local_games_object, tmpdir): def test_notify_changed(local_games_object, tmpdir): mfst_file = tmpdir.mkdir("GameName1").join("gameid.mfst") - mfst_file.write("?currentstate=kInstalling&id=OFB-EAST%3a48217&previousstate=kPostTransfer") + mfst_file.write("?currentstate=kInstalling&id=OFB-EAST%3a48217&previousstate=kPostTransfer&ddinitialdownload=1") local_games_object.update() mfst_file.write("?currentstate=kReadyToStart&id=OFB-EAST%3a48217&previousstate=kCompleted") diff --git a/tests/test_local_size.py b/tests/test_local_size.py new file mode 100644 index 0000000..a59e5dd --- /dev/null +++ b/tests/test_local_size.py @@ -0,0 +1,63 @@ +import pytest + +from galaxy.api.errors import UnknownError + +from local_games import parse_map_crc_for_total_size + + +def test_parse_map_crc_for_total_size_no_file(): + with pytest.raises(FileNotFoundError): + parse_map_crc_for_total_size('nonexisting/path') + + +@pytest.mark.parametrize('content, expected', [ + ( + '', 0 + ), + ( + '?crc=3156175984&file=game.exe&id=aeae3e851c66&jobid=%7b1b69c729-23b7-415f-a252-8f9cd9387475%7d&size=270071' + , 270071 + ), + ( + '?crc=3156175984&file=game.exe&id=aeae3e851c66&jobid=%7b1b69c729-23b7-415f-a252-8f9cd9387475%7d&size=10000000\n' + '?crc=3156175984&file=game_launcher.exe&id=aeae3e851c66&jobid=%7b1b69c729-23b7-415f-a252-8f9cd9387475%7d&size=1000000' + , 11000000 + ), + ( + '?crc=1827740508&file=vpk%2ffrontend.bsp.pak000_dir.vpk&id=c72a35f78b00&jobid=%7bd287f304-bdb3-49ba-9ff9-c4c1a60217ee%7d&size=1000\n' + '?crc=4211709804&file=vpk%2fmp_common.bsp.pak000_dir.vpk&id=c72a35f78b00&jobid=%7bd287f304-bdb3-49ba-9ff9-c4c1a60217ee%7d&size=10\n' + '?crc=254416060&file=__installer%2fdlc%2favatars%2fsupport%2fmnfst.txt&id=f9eef5faa86c&jobid=%7bf1a367f9-c088-40bc-893a-c5350e04debd%7d&size=5\n' + , 1015 + ) +]) +def test_parse_map_crc_for_total_size(content, expected, tmp_path): + crc_file = tmp_path / 'map.crc' + crc_file.write_text(content, encoding='utf-16-le') + assert expected == parse_map_crc_for_total_size(crc_file) + + +@pytest.mark.asyncio +async def test_plugin_local_size_game_not_installed(plugin): + game_id = 'gameId' + context = await plugin.prepare_local_size_context([game_id]) + with pytest.raises(UnknownError): + await plugin.get_local_size(game_id, context) + + +@pytest.mark.asyncio +async def test_plugin_local_size_game_installed(tmpdir, plugin): + expected_size = 142342 + game_id = 'OFB-EAST:48217' + local_content_dir = tmpdir.mkdir("GameName1") + mfst_file = local_content_dir.join("Origin.gameId.mfst") + mfst_file.write("?currentstate=kReadyToStart&id=OFB-EAST%3a48217&previousstate=kCompleted") + crc_file = local_content_dir.join("map.crc") + crc_file.write_text( + f'?crc=3156175984&file=game.exe&id=aeae3e851c66&jobid=%7b1b69c729-23b7-415f-a252-8f9cd9387475%7d&size={expected_size}', + encoding='utf-16-le' + ) + + await plugin.get_local_games() # need to prepare local client cache + context = await plugin.prepare_local_size_context([game_id]) + result = await plugin.get_local_size(game_id, context) + assert result == expected_size