python-linkplay 0.0.18__tar.gz → 0.0.20__tar.gz

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.
Files changed (23) hide show
  1. {python_linkplay-0.0.18/src/python_linkplay.egg-info → python_linkplay-0.0.20}/PKG-INFO +1 -1
  2. python_linkplay-0.0.20/src/linkplay/__version__.py +1 -0
  3. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/bridge.py +62 -24
  4. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/consts.py +2 -0
  5. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/controller.py +24 -15
  6. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/discovery.py +20 -17
  7. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/exceptions.py +4 -0
  8. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/utils.py +23 -4
  9. {python_linkplay-0.0.18 → python_linkplay-0.0.20/src/python_linkplay.egg-info}/PKG-INFO +1 -1
  10. python_linkplay-0.0.18/src/linkplay/__version__.py +0 -1
  11. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/LICENSE +0 -0
  12. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/README.md +0 -0
  13. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/pyproject.toml +0 -0
  14. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/setup.cfg +0 -0
  15. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/setup.py +0 -0
  16. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/__init__.py +0 -0
  17. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/__main__.py +0 -0
  18. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/linkplay/endpoint.py +0 -0
  19. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/python_linkplay.egg-info/SOURCES.txt +0 -0
  20. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/python_linkplay.egg-info/dependency_links.txt +0 -0
  21. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/python_linkplay.egg-info/not-zip-safe +0 -0
  22. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/python_linkplay.egg-info/requires.txt +0 -0
  23. {python_linkplay-0.0.18 → python_linkplay-0.0.20}/src/python_linkplay.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python_linkplay
3
- Version: 0.0.18
3
+ Version: 0.0.20
4
4
  Summary: A Python Library for Seamless LinkPlay Device Control
5
5
  Author: Velleman Group nv
6
6
  License: MIT
@@ -0,0 +1 @@
1
+ __version__ = '0.0.20'
@@ -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
 
@@ -71,7 +73,22 @@ class LinkPlayDevice:
71
73
  def eth(self) -> str | None:
72
74
  """Returns the ethernet address."""
73
75
  eth2 = self.properties.get(DeviceAttribute.ETH2)
74
- return eth2 if eth2 else self.properties.get(DeviceAttribute.APCLI0)
76
+ eth0 = self.properties.get(DeviceAttribute.ETH0)
77
+ for eth in [eth2, eth0]:
78
+ if eth == "0.0.0.0":
79
+ eth = None
80
+ return (
81
+ eth2
82
+ if eth2
83
+ else eth0
84
+ if eth0
85
+ else self.properties.get(DeviceAttribute.APCLI0)
86
+ )
87
+
88
+ async def timesync(self) -> None:
89
+ """Sync the time."""
90
+ timestamp = time.strftime("%Y%m%d%H%M%S")
91
+ await self.bridge.request(LinkPlayCommand.TIMESYNC.format(timestamp))
75
92
 
76
93
 
77
94
  class LinkPlayPlayer:
@@ -172,12 +189,14 @@ class LinkPlayPlayer:
172
189
  )
173
190
  await self.bridge.request(LinkPlayCommand.PLAY_PRESET.format(preset_number))
174
191
 
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))
192
+ async def seek(self, position: int) -> None:
193
+ """Seek to a position."""
194
+ if (
195
+ self.total_length_in_seconds > 0
196
+ and position >= 0
197
+ and position <= self.total_length_in_seconds
198
+ ):
199
+ await self.bridge.request(LinkPlayCommand.SEEK.format(position))
181
200
 
182
201
  @property
183
202
  def muted(self) -> bool:
@@ -217,6 +236,16 @@ class LinkPlayPlayer:
217
236
  """Returns the total length of the track in milliseconds."""
218
237
  return int(self.properties.get(PlayerAttribute.TOTAL_LENGTH, 0))
219
238
 
239
+ @property
240
+ def current_position_in_seconds(self) -> int:
241
+ """Returns the current position of the track in seconds."""
242
+ return int(int(self.properties.get(PlayerAttribute.CURRENT_POSITION, 0)) / 1000)
243
+
244
+ @property
245
+ def total_length_in_seconds(self) -> int:
246
+ """Returns the total length of the track in seconds."""
247
+ return int(int(self.properties.get(PlayerAttribute.TOTAL_LENGTH, 0)) / 1000)
248
+
220
249
  @property
221
250
  def status(self) -> PlayingStatus:
222
251
  """Returns the current playing status."""
@@ -328,26 +357,31 @@ class LinkPlayMultiroom:
328
357
 
329
358
  async def update_status(self, bridges: list[LinkPlayBridge]) -> None:
330
359
  """Updates the multiroom status."""
331
- properties: dict[Any, Any] = await self.leader.json_request(
332
- LinkPlayCommand.MULTIROOM_LIST
333
- )
360
+ try:
361
+ properties: dict[Any, Any] = await self.leader.json_request(
362
+ LinkPlayCommand.MULTIROOM_LIST
363
+ )
334
364
 
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)
365
+ self.followers = []
366
+ if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
367
+ return
368
+
369
+ follower_uuids = [
370
+ follower[MultiroomAttribute.UUID]
371
+ for follower in properties[MultiroomAttribute.FOLLOWER_LIST]
372
+ ]
373
+ new_followers = [
374
+ bridge for bridge in bridges if bridge.device.uuid in follower_uuids
375
+ ]
376
+ self.followers.extend(new_followers)
377
+ except LinkPlayInvalidDataException as exc:
378
+ LOGGER.exception(exc)
347
379
 
348
380
  async def ungroup(self) -> None:
349
381
  """Ungroups the multiroom group."""
350
382
  await self.leader.request(LinkPlayCommand.MULTIROOM_UNGROUP)
383
+ for follewer in self.followers:
384
+ follewer.multiroom = None
351
385
  self.followers = []
352
386
 
353
387
  async def add_follower(self, follower: LinkPlayBridge) -> None:
@@ -355,14 +389,18 @@ class LinkPlayMultiroom:
355
389
  await follower.request(
356
390
  LinkPlayCommand.MULTIROOM_JOIN.format(self.leader.device.eth)
357
391
  ) # type: ignore[str-format]
358
- self.followers.append(follower)
392
+ if follower not in self.followers:
393
+ follower.multiroom = self
394
+ self.followers.append(follower)
359
395
 
360
396
  async def remove_follower(self, follower: LinkPlayBridge) -> None:
361
397
  """Removes a follower from the multiroom group."""
362
398
  await self.leader.request(
363
399
  LinkPlayCommand.MULTIROOM_KICK.format(follower.device.eth)
364
400
  ) # type: ignore[str-format]
365
- self.followers.remove(follower)
401
+ if follower in self.followers:
402
+ follower.multiroom = None
403
+ self.followers.remove(follower)
366
404
 
367
405
  async def set_volume(self, value: int) -> None:
368
406
  """Sets the volume for the multiroom group."""
@@ -145,6 +145,7 @@ class PlayingMode(StrEnum):
145
145
  HTTP_MAX = "29"
146
146
  ALARM = "30"
147
147
  SPOTIFY = "31"
148
+ TIDAL = "32"
148
149
  LINE_IN = "40"
149
150
  BLUETOOTH = "41"
150
151
  OPTICAL = "43"
@@ -179,6 +180,7 @@ PLAY_MODE_SEND_MAP: dict[PlayingMode, str] = { # case sensitive!
179
180
  PlayingMode.UDISK: "udisk",
180
181
  PlayingMode.ALARM: "Alarm",
181
182
  PlayingMode.SPOTIFY: "Spotify",
183
+ PlayingMode.TIDAL: "Tidal",
182
184
  PlayingMode.LINE_IN: "line-in",
183
185
  PlayingMode.BLUETOOTH: "bluetooth",
184
186
  PlayingMode.OPTICAL: "optical",
@@ -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]
@@ -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
@@ -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
@@ -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.20
4
4
  Summary: A Python Library for Seamless LinkPlay Device Control
5
5
  Author: Velleman Group nv
6
6
  License: MIT
@@ -1 +0,0 @@
1
- __version__ = '0.0.18'