python-linkplay 0.0.18__py3-none-any.whl → 0.0.19__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
linkplay/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.0.18'
1
+ __version__ = '0.0.19'
linkplay/bridge.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import time
3
4
  from typing import Any
4
5
 
5
6
  from linkplay.consts import (
@@ -20,6 +21,7 @@ from linkplay.consts import (
20
21
  SpeakerType,
21
22
  )
22
23
  from linkplay.endpoint import LinkPlayEndpoint
24
+ from linkplay.exceptions import LinkPlayInvalidDataException
23
25
  from linkplay.utils import fixup_player_properties
24
26
 
25
27
 
@@ -73,6 +75,11 @@ class LinkPlayDevice:
73
75
  eth2 = self.properties.get(DeviceAttribute.ETH2)
74
76
  return eth2 if eth2 else self.properties.get(DeviceAttribute.APCLI0)
75
77
 
78
+ async def timesync(self) -> None:
79
+ """Sync the time."""
80
+ timestamp = time.strftime("%Y%m%d%H%M%S")
81
+ await self.bridge.request(LinkPlayCommand.TIMESYNC.format(timestamp))
82
+
76
83
 
77
84
  class LinkPlayPlayer:
78
85
  """Represents a LinkPlay player."""
@@ -172,12 +179,14 @@ class LinkPlayPlayer:
172
179
  )
173
180
  await self.bridge.request(LinkPlayCommand.PLAY_PRESET.format(preset_number))
174
181
 
175
- async def timesync(self) -> None:
176
- """Sync the time."""
177
- import time
178
-
179
- timestamp = time.strftime("%Y%m%d%H%M%S")
180
- await self.bridge.request(LinkPlayCommand.TIMESYNC.format(timestamp))
182
+ async def seek(self, position: int) -> None:
183
+ """Seek to a position."""
184
+ if (
185
+ self.total_length_in_seconds > 0
186
+ and position >= 0
187
+ and position <= self.total_length_in_seconds
188
+ ):
189
+ await self.bridge.request(LinkPlayCommand.SEEK.format(position))
181
190
 
182
191
  @property
183
192
  def muted(self) -> bool:
@@ -217,6 +226,16 @@ class LinkPlayPlayer:
217
226
  """Returns the total length of the track in milliseconds."""
218
227
  return int(self.properties.get(PlayerAttribute.TOTAL_LENGTH, 0))
219
228
 
229
+ @property
230
+ def current_position_in_seconds(self) -> int:
231
+ """Returns the current position of the track in seconds."""
232
+ return int(int(self.properties.get(PlayerAttribute.CURRENT_POSITION, 0)) / 1000)
233
+
234
+ @property
235
+ def total_length_in_seconds(self) -> int:
236
+ """Returns the total length of the track in seconds."""
237
+ return int(int(self.properties.get(PlayerAttribute.TOTAL_LENGTH, 0)) / 1000)
238
+
220
239
  @property
221
240
  def status(self) -> PlayingStatus:
222
241
  """Returns the current playing status."""
@@ -328,22 +347,25 @@ class LinkPlayMultiroom:
328
347
 
329
348
  async def update_status(self, bridges: list[LinkPlayBridge]) -> None:
330
349
  """Updates the multiroom status."""
331
- properties: dict[Any, Any] = await self.leader.json_request(
332
- LinkPlayCommand.MULTIROOM_LIST
333
- )
350
+ try:
351
+ properties: dict[Any, Any] = await self.leader.json_request(
352
+ LinkPlayCommand.MULTIROOM_LIST
353
+ )
334
354
 
335
- self.followers = []
336
- if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
337
- return
338
-
339
- follower_uuids = [
340
- follower[MultiroomAttribute.UUID]
341
- for follower in properties[MultiroomAttribute.FOLLOWER_LIST]
342
- ]
343
- new_followers = [
344
- bridge for bridge in bridges if bridge.device.uuid in follower_uuids
345
- ]
346
- self.followers.extend(new_followers)
355
+ self.followers = []
356
+ if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
357
+ return
358
+
359
+ follower_uuids = [
360
+ follower[MultiroomAttribute.UUID]
361
+ for follower in properties[MultiroomAttribute.FOLLOWER_LIST]
362
+ ]
363
+ new_followers = [
364
+ bridge for bridge in bridges if bridge.device.uuid in follower_uuids
365
+ ]
366
+ self.followers.extend(new_followers)
367
+ except LinkPlayInvalidDataException as exc:
368
+ LOGGER.exception(exc)
347
369
 
348
370
  async def ungroup(self) -> None:
349
371
  """Ungroups the multiroom group."""
linkplay/controller.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from aiohttp import ClientSession
2
2
 
3
3
  from linkplay.bridge import LinkPlayBridge, LinkPlayMultiroom
4
+ from linkplay.consts import LOGGER
4
5
  from linkplay.discovery import discover_linkplay_bridges
6
+ from linkplay.exceptions import LinkPlayInvalidDataException
5
7
 
6
8
 
7
9
  class LinkPlayController:
@@ -49,28 +51,35 @@ class LinkPlayController:
49
51
 
50
52
  removed_multirooms = []
51
53
  for multiroom in multirooms:
52
- for follower in multiroom.followers:
53
- follower.multiroom = None
54
- await multiroom.update_status(self.bridges)
55
- if len(multiroom.followers) > 0:
54
+ try:
56
55
  for follower in multiroom.followers:
57
- follower.multiroom = multiroom
58
- else:
59
- multiroom.leader.multiroom = None
60
- removed_multirooms.append(multiroom)
56
+ follower.multiroom = None
57
+
58
+ await multiroom.update_status(self.bridges)
59
+ if len(multiroom.followers) > 0:
60
+ for follower in multiroom.followers:
61
+ follower.multiroom = multiroom
62
+ else:
63
+ multiroom.leader.multiroom = None
64
+ removed_multirooms.append(multiroom)
65
+ except LinkPlayInvalidDataException as exc:
66
+ LOGGER.exception(exc)
61
67
 
62
68
  # Create new multirooms from new bridges
63
69
  for bridge in self.bridges:
64
70
  if bridge.multiroom:
65
71
  continue
66
72
 
67
- multiroom = LinkPlayMultiroom(bridge)
68
- await multiroom.update_status(self.bridges)
69
- if len(multiroom.followers) > 0:
70
- multirooms.append(multiroom)
71
- bridge.multiroom = multiroom
72
- for follower in multiroom.followers:
73
- follower.multiroom = multiroom
73
+ try:
74
+ multiroom = LinkPlayMultiroom(bridge)
75
+ await multiroom.update_status(self.bridges)
76
+ if len(multiroom.followers) > 0:
77
+ multirooms.append(multiroom)
78
+ bridge.multiroom = multiroom
79
+ for follower in multiroom.followers:
80
+ follower.multiroom = multiroom
81
+ except LinkPlayInvalidDataException as exc:
82
+ LOGGER.exception(exc)
74
83
 
75
84
  # Remove multirooms with no followers
76
85
  multirooms = [item for item in multirooms if item not in removed_multirooms]
linkplay/discovery.py CHANGED
@@ -8,7 +8,7 @@ from deprecated import deprecated
8
8
  from linkplay.bridge import LinkPlayBridge
9
9
  from linkplay.consts import UPNP_DEVICE_TYPE, LinkPlayCommand, MultiroomAttribute
10
10
  from linkplay.endpoint import LinkPlayApiEndpoint, LinkPlayEndpoint
11
- from linkplay.exceptions import LinkPlayRequestException
11
+ from linkplay.exceptions import LinkPlayInvalidDataException, LinkPlayRequestException
12
12
 
13
13
 
14
14
  @deprecated(
@@ -98,21 +98,24 @@ async def discover_bridges_through_multiroom(
98
98
  bridge: LinkPlayBridge, session: ClientSession
99
99
  ) -> list[LinkPlayBridge]:
100
100
  """Discovers bridges through the multiroom of the provided bridge."""
101
- properties: dict[Any, Any] = await bridge.json_request(
102
- LinkPlayCommand.MULTIROOM_LIST
103
- )
101
+ try:
102
+ properties: dict[Any, Any] = await bridge.json_request(
103
+ LinkPlayCommand.MULTIROOM_LIST
104
+ )
104
105
 
105
- if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
106
+ if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
107
+ return []
108
+
109
+ followers: list[LinkPlayBridge] = []
110
+ for follower in properties[MultiroomAttribute.FOLLOWER_LIST]:
111
+ try:
112
+ new_bridge = await linkplay_factory_httpapi_bridge(
113
+ follower[MultiroomAttribute.IP], session
114
+ )
115
+ followers.append(new_bridge)
116
+ except LinkPlayRequestException:
117
+ pass
118
+
119
+ return followers
120
+ except LinkPlayInvalidDataException:
106
121
  return []
107
-
108
- followers: list[LinkPlayBridge] = []
109
- for follower in properties[MultiroomAttribute.FOLLOWER_LIST]:
110
- try:
111
- new_bridge = await linkplay_factory_httpapi_bridge(
112
- follower[MultiroomAttribute.IP], session
113
- )
114
- followers.append(new_bridge)
115
- except LinkPlayRequestException:
116
- pass
117
-
118
- return followers
linkplay/exceptions.py CHANGED
@@ -4,3 +4,7 @@ class LinkPlayException(Exception):
4
4
 
5
5
  class LinkPlayRequestException(LinkPlayException):
6
6
  pass
7
+
8
+
9
+ class LinkPlayInvalidDataException(LinkPlayException):
10
+ pass
linkplay/utils.py CHANGED
@@ -22,7 +22,7 @@ from linkplay.consts import (
22
22
  PlayerAttribute,
23
23
  PlayingStatus,
24
24
  )
25
- from linkplay.exceptions import LinkPlayRequestException
25
+ from linkplay.exceptions import LinkPlayInvalidDataException, LinkPlayRequestException
26
26
 
27
27
 
28
28
  async def session_call_api(endpoint: str, session: ClientSession, command: str) -> str:
@@ -61,9 +61,28 @@ async def session_call_api(endpoint: str, session: ClientSession, command: str)
61
61
  async def session_call_api_json(
62
62
  endpoint: str, session: ClientSession, command: str
63
63
  ) -> dict[str, str]:
64
- """Calls the LinkPlay API and returns the result as a JSON object."""
65
- result = await session_call_api(endpoint, session, command)
66
- return json.loads(result) # type: ignore
64
+ """Calls the LinkPlay API and returns the result as a JSON object
65
+
66
+ Args:
67
+ endpoint (str): The endpoint to use.
68
+ session (ClientSession): The client session to use.
69
+ command (str): The command to use.
70
+
71
+ Raises:
72
+ LinkPlayRequestException: Thrown when the request fails (timeout, error http status).
73
+ LinkPlayInvalidDataException: Thrown when the request has succeeded with invalid json.
74
+
75
+ Returns:
76
+ str: The response of the API call.
77
+ """
78
+ try:
79
+ result = await session_call_api(endpoint, session, command)
80
+ return json.loads(result) # type: ignore
81
+ except json.JSONDecodeError as jsonexc:
82
+ url = API_ENDPOINT.format(endpoint, command)
83
+ raise LinkPlayInvalidDataException(
84
+ f"Unexpected JSON ({result}) received from '{url}'"
85
+ ) from jsonexc
67
86
 
68
87
 
69
88
  async def session_call_api_ok(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python_linkplay
3
- Version: 0.0.18
3
+ Version: 0.0.19
4
4
  Summary: A Python Library for Seamless LinkPlay Device Control
5
5
  Author: Velleman Group nv
6
6
  License: MIT
@@ -0,0 +1,15 @@
1
+ linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
2
+ linkplay/__main__.py,sha256=Wcza80QaWfOaHjyJEfQYhB9kiPLE0NOqIj4zVWv2Nqs,577
3
+ linkplay/__version__.py,sha256=Wg0q75xR03Wgtr26_qt1wcsGMmTO6dHkia7eXfDFFKY,23
4
+ linkplay/bridge.py,sha256=e9ObGLhBw7BJp2hUdSXj3qvHh15rC5sSC-Q7vBnEvrQ,14575
5
+ linkplay/consts.py,sha256=5JQ-5CgmbPMBhrxhTb_0APnt1EnYhiNwsHn-68yDiTI,13486
6
+ linkplay/controller.py,sha256=IBFhnt-VhdIZnI_3OLU6hA8oQa9IZWY7-8cN0uCh3-w,3277
7
+ linkplay/discovery.py,sha256=XgzzsOkxtUPu5f7V1KTIqQWP6_UCQncpbWGaVN1lULU,4457
8
+ linkplay/endpoint.py,sha256=5Ybr54aroFVEZ6fnFYP41QAuSP7-J9qHYAzLod4S3KY,2459
9
+ linkplay/exceptions.py,sha256=Kow13uJPSL4y6rXMnkcl_Yp9wH1weOyKw_knd0p-Exc,173
10
+ linkplay/utils.py,sha256=Kmbzw8zC9mV89ZOC5-GNtbiLkgUkuvAEUcsJdRkSY-w,8485
11
+ python_linkplay-0.0.19.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
12
+ python_linkplay-0.0.19.dist-info/METADATA,sha256=P00mdBXZWR0fccBk_aVAHGDuv2pQfyMDeCeTIJLo5p4,2988
13
+ python_linkplay-0.0.19.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
14
+ python_linkplay-0.0.19.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
15
+ python_linkplay-0.0.19.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
2
- linkplay/__main__.py,sha256=Wcza80QaWfOaHjyJEfQYhB9kiPLE0NOqIj4zVWv2Nqs,577
3
- linkplay/__version__.py,sha256=Te1DfrvgEn6zd58FwD1NO8Ms7gZvtmQkfEmQw_u7h7k,23
4
- linkplay/bridge.py,sha256=dJ0WfqkSEiFhp7vydqUCzBRt_Nxhylfg8pYZfs2e_N0,13619
5
- linkplay/consts.py,sha256=5JQ-5CgmbPMBhrxhTb_0APnt1EnYhiNwsHn-68yDiTI,13486
6
- linkplay/controller.py,sha256=i3eLlaZ5pWoGgRT27I564Z9Bmi_aiY9g6lUocXE2qmk,2894
7
- linkplay/discovery.py,sha256=aEzN_94pKLmHKYIL7DxSW0FYRsaF2ruZe2bwXz0zf5U,4299
8
- linkplay/endpoint.py,sha256=5Ybr54aroFVEZ6fnFYP41QAuSP7-J9qHYAzLod4S3KY,2459
9
- linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
10
- linkplay/utils.py,sha256=aU_9TL39ngQaEMhFWMtlwB3POba2GjGlfNBshLFFS90,7788
11
- python_linkplay-0.0.18.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
12
- python_linkplay-0.0.18.dist-info/METADATA,sha256=V4CmIWB_LMwkchCc3Ya0L3ao5BVM13CxcYtNq2VOet8,2988
13
- python_linkplay-0.0.18.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
14
- python_linkplay-0.0.18.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
15
- python_linkplay-0.0.18.dist-info/RECORD,,