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