Skip to content

Commit f57e03d

Browse files
committed
Add game library settings feature
1 parent 66085e2 commit f57e03d

6 files changed

Lines changed: 291 additions & 3 deletions

File tree

src/galaxy/api/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class Feature(Enum):
110110
ImportFriends = "ImportFriends"
111111
ShutdownPlatformClient = "ShutdownPlatformClient"
112112
LaunchPlatformClient = "LaunchPlatformClient"
113+
ImportGameLibrarySettings = "ImportGameLibrarySettings"
113114

114115

115116
class LicenseType(Enum):

src/galaxy/api/plugin.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from galaxy.api.consts import Feature
1111
from galaxy.api.errors import ImportInProgress, UnknownError
1212
from galaxy.api.jsonrpc import ApplicationError, NotificationClient, Server
13-
from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep
13+
from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep, GameLibrarySettings
1414
from galaxy.task_manager import TaskManager
1515

1616
class JSONEncoder(json.JSONEncoder):
@@ -46,6 +46,7 @@ def __init__(self, platform, version, reader, writer, handshake_token):
4646

4747
self._achievements_import_in_progress = False
4848
self._game_times_import_in_progress = False
49+
self._game_library_settings_import_in_progress = False
4950

5051
self._persistent_cache = dict()
5152

@@ -109,6 +110,9 @@ def __init__(self, platform, version, reader, writer, handshake_token):
109110
self._register_method("start_game_times_import", self._start_game_times_import)
110111
self._detect_feature(Feature.ImportGameTime, ["get_game_time"])
111112

113+
self._register_method("start_game_library_settings_import", self._start_game_library_settings_import)
114+
self._detect_feature(Feature.ImportGameLibrarySettings, ["get_game_library_settings"])
115+
112116
async def __aenter__(self):
113117
return self
114118

@@ -402,6 +406,20 @@ def _game_time_import_failure(self, game_id: str, error: ApplicationError) -> No
402406
def _game_times_import_finished(self) -> None:
403407
self._notification_client.notify("game_times_import_finished", None)
404408

409+
def _game_library_settings_import_success(self, game_library_settings: GameLibrarySettings) -> None:
410+
params = {"game_library_settings": game_library_settings}
411+
self._notification_client.notify("game_library_settings_import_success", params)
412+
413+
def _game_library_settings_import_failure(self, game_id: str, error: ApplicationError) -> None:
414+
params = {
415+
"game_id": game_id,
416+
"error": error.json()
417+
}
418+
self._notification_client.notify("game_library_settings_import_failure", params)
419+
420+
def _game_library_settings_import_finished(self) -> None:
421+
self._notification_client.notify("game_library_settings_import_finished", None)
422+
405423
def lost_authentication(self) -> None:
406424
"""Notify the client that integration has lost authentication for the
407425
current user and is unable to perform actions which would require it.
@@ -750,6 +768,63 @@ def game_times_import_complete(self) -> None:
750768
(like updating cache).
751769
"""
752770

771+
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
772+
if self._game_library_settings_import_in_progress:
773+
raise ImportInProgress()
774+
775+
context = await self.prepare_game_library_settings_context(game_ids)
776+
777+
async def import_game_library_settings(game_id, context_):
778+
try:
779+
game_library_settings = await self.get_game_library_settings(game_id, context_)
780+
self._game_library_settings_import_success(game_library_settings)
781+
except ApplicationError as error:
782+
self._game_library_settings_import_failure(game_id, error)
783+
except Exception:
784+
logging.exception("Unexpected exception raised in import_game_library_settings")
785+
self._game_library_settings_import_failure(game_id, UnknownError())
786+
787+
async def import_game_library_settings_set(game_ids_, context_):
788+
try:
789+
imports = [import_game_library_settings(game_id, context_) for game_id in game_ids_]
790+
await asyncio.gather(*imports)
791+
finally:
792+
self._game_library_settings_import_finished()
793+
self._game_library_settings_import_in_progress = False
794+
self.game_library_settings_import_complete()
795+
796+
self._external_task_manager.create_task(
797+
import_game_library_settings_set(game_ids, context),
798+
"game library settings import",
799+
handle_exceptions=False
800+
)
801+
self._game_library_settings_import_in_progress = True
802+
803+
async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any:
804+
"""Override this method to prepare context for get_game_library_settings.
805+
This allows for optimizations like batch requests to platform API.
806+
Default implementation returns None.
807+
808+
:param game_ids: the ids of the games for which game time are imported
809+
:return: context
810+
"""
811+
return None
812+
813+
async def get_game_library_settings(self, game_id: str, context: Any) -> GameLibrarySettings:
814+
"""Override this method to return the game library settings for the game
815+
identified by the provided game_id.
816+
This method is called by import task initialized by GOG Galaxy Client.
817+
818+
:param game_id: the id of the game for which the game time is returned
819+
:param context: the value returned from :meth:`prepare_game_library_settings_context`
820+
:return: GameLibrarySettings object
821+
"""
822+
raise NotImplementedError()
823+
824+
def game_library_settings_import_complete(self) -> None:
825+
"""Override this method to handle operations after game times import is finished
826+
(like updating cache).
827+
"""
753828

754829
def create_and_run_plugin(plugin_class, argv):
755830
"""Call this method as an entry point for the implemented integration.

src/galaxy/api/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,15 @@ class GameTime():
151151
game_id: str
152152
time_played: Optional[int]
153153
last_played_time: Optional[int]
154+
155+
@dataclass
156+
class GameLibrarySettings():
157+
"""Library settings of a game, defines assigned tags and visibility flag.
158+
159+
:param game_id: id of the related game
160+
:param tags: collection of tags assigned to the game
161+
:param hidden: indicates if the game should be hidden in GOG Galaxy application
162+
"""
163+
game_id: str
164+
tags: Optional[List[str]]
165+
hidden: Optional[bool]

tests/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ async def plugin(reader, writer):
4949
"game_times_import_complete",
5050
"shutdown_platform_client",
5151
"shutdown",
52-
"tick"
52+
"tick",
53+
"get_game_library_settings",
54+
"prepare_game_library_settings_context",
55+
"game_library_settings_import_complete",
5356
)
5457

5558
with ExitStack() as stack:

tests/test_features.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ def test_base_class():
1414
Feature.ImportGameTime,
1515
Feature.ImportFriends,
1616
Feature.ShutdownPlatformClient,
17-
Feature.LaunchPlatformClient
17+
Feature.LaunchPlatformClient,
18+
Feature.ImportGameLibrarySettings
1819
}
1920

2021

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
from unittest.mock import call
2+
3+
import pytest
4+
from galaxy.api.types import GameLibrarySettings
5+
from galaxy.api.errors import BackendError
6+
from galaxy.unittest.mock import async_return_value
7+
8+
from tests import create_message, get_messages
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_get_game_time_success(plugin, read, write):
13+
plugin.prepare_game_library_settings_context.return_value = async_return_value("abc")
14+
request = {
15+
"jsonrpc": "2.0",
16+
"id": "3",
17+
"method": "start_game_library_settings_import",
18+
"params": {
19+
"game_ids": ["3", "5", "7"]
20+
}
21+
}
22+
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
23+
plugin.get_game_library_settings.side_effect = [
24+
async_return_value(GameLibrarySettings("3", None, True)),
25+
async_return_value(GameLibrarySettings("5", [], False)),
26+
async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)),
27+
]
28+
await plugin.run()
29+
plugin.get_game_library_settings.assert_has_calls([
30+
call("3", "abc"),
31+
call("5", "abc"),
32+
call("7", "abc"),
33+
])
34+
plugin.game_library_settings_import_complete.assert_called_once_with()
35+
36+
assert get_messages(write) == [
37+
{
38+
"jsonrpc": "2.0",
39+
"id": "3",
40+
"result": None
41+
},
42+
{
43+
"jsonrpc": "2.0",
44+
"method": "game_library_settings_import_success",
45+
"params": {
46+
"game_library_settings": {
47+
"game_id": "3",
48+
"hidden": True
49+
}
50+
}
51+
},
52+
{
53+
"jsonrpc": "2.0",
54+
"method": "game_library_settings_import_success",
55+
"params": {
56+
"game_library_settings": {
57+
"game_id": "5",
58+
"tags": [],
59+
"hidden": False
60+
}
61+
}
62+
},
63+
{
64+
"jsonrpc": "2.0",
65+
"method": "game_library_settings_import_success",
66+
"params": {
67+
"game_library_settings": {
68+
"game_id": "7",
69+
"tags": ["tag1", "tag2", "tag3"]
70+
}
71+
}
72+
},
73+
{
74+
"jsonrpc": "2.0",
75+
"method": "game_library_settings_import_finished",
76+
"params": None
77+
}
78+
]
79+
80+
81+
@pytest.mark.asyncio
82+
@pytest.mark.parametrize("exception,code,message", [
83+
(BackendError, 4, "Backend error"),
84+
(KeyError, 0, "Unknown error")
85+
])
86+
async def test_get_game_library_settings_error(exception, code, message, plugin, read, write):
87+
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
88+
request = {
89+
"jsonrpc": "2.0",
90+
"id": "3",
91+
"method": "start_game_library_settings_import",
92+
"params": {
93+
"game_ids": ["6"]
94+
}
95+
}
96+
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
97+
plugin.get_game_library_settings.side_effect = exception
98+
await plugin.run()
99+
plugin.get_game_library_settings.assert_called()
100+
plugin.game_library_settings_import_complete.assert_called_once_with()
101+
102+
assert get_messages(write) == [
103+
{
104+
"jsonrpc": "2.0",
105+
"id": "3",
106+
"result": None
107+
},
108+
{
109+
"jsonrpc": "2.0",
110+
"method": "game_library_settings_import_failure",
111+
"params": {
112+
"game_id": "6",
113+
"error": {
114+
"code": code,
115+
"message": message
116+
}
117+
}
118+
},
119+
{
120+
"jsonrpc": "2.0",
121+
"method": "game_library_settings_import_finished",
122+
"params": None
123+
}
124+
]
125+
126+
127+
@pytest.mark.asyncio
128+
async def test_prepare_get_game_library_settings_context_error(plugin, read, write):
129+
plugin.prepare_game_library_settings_context.side_effect = BackendError()
130+
request = {
131+
"jsonrpc": "2.0",
132+
"id": "3",
133+
"method": "start_game_library_settings_import",
134+
"params": {
135+
"game_ids": ["6"]
136+
}
137+
}
138+
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
139+
await plugin.run()
140+
141+
assert get_messages(write) == [
142+
{
143+
"jsonrpc": "2.0",
144+
"id": "3",
145+
"error": {
146+
"code": 4,
147+
"message": "Backend error"
148+
}
149+
}
150+
]
151+
152+
153+
@pytest.mark.asyncio
154+
async def test_import_in_progress(plugin, read, write):
155+
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
156+
requests = [
157+
{
158+
"jsonrpc": "2.0",
159+
"id": "3",
160+
"method": "start_game_library_settings_import",
161+
"params": {
162+
"game_ids": ["6"]
163+
}
164+
},
165+
{
166+
"jsonrpc": "2.0",
167+
"id": "4",
168+
"method": "start_game_library_settings_import",
169+
"params": {
170+
"game_ids": ["7"]
171+
}
172+
}
173+
]
174+
read.side_effect = [
175+
async_return_value(create_message(requests[0])),
176+
async_return_value(create_message(requests[1])),
177+
async_return_value(b"", 10)
178+
]
179+
180+
await plugin.run()
181+
182+
messages = get_messages(write)
183+
assert {
184+
"jsonrpc": "2.0",
185+
"id": "3",
186+
"result": None
187+
} in messages
188+
assert {
189+
"jsonrpc": "2.0",
190+
"id": "4",
191+
"error": {
192+
"code": 600,
193+
"message": "Import already in progress"
194+
}
195+
} in messages
196+

0 commit comments

Comments
 (0)