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 +1 -1
- linkplay/bridge.py +43 -21
- linkplay/controller.py +24 -15
- linkplay/discovery.py +20 -17
- linkplay/exceptions.py +4 -0
- linkplay/utils.py +23 -4
- {python_linkplay-0.0.18.dist-info → python_linkplay-0.0.19.dist-info}/METADATA +1 -1
- python_linkplay-0.0.19.dist-info/RECORD +15 -0
- python_linkplay-0.0.18.dist-info/RECORD +0 -15
- {python_linkplay-0.0.18.dist-info → python_linkplay-0.0.19.dist-info}/LICENSE +0 -0
- {python_linkplay-0.0.18.dist-info → python_linkplay-0.0.19.dist-info}/WHEEL +0 -0
- {python_linkplay-0.0.18.dist-info → python_linkplay-0.0.19.dist-info}/top_level.txt +0 -0
linkplay/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = '0.0.
|
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
|
176
|
-
"""
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
350
|
+
try:
|
351
|
+
properties: dict[Any, Any] = await self.leader.json_request(
|
352
|
+
LinkPlayCommand.MULTIROOM_LIST
|
353
|
+
)
|
334
354
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
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 =
|
58
|
-
|
59
|
-
multiroom.
|
60
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
follower
|
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
|
-
|
102
|
-
|
103
|
-
|
101
|
+
try:
|
102
|
+
properties: dict[Any, Any] = await bridge.json_request(
|
103
|
+
LinkPlayCommand.MULTIROOM_LIST
|
104
|
+
)
|
104
105
|
|
105
|
-
|
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
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
|
-
|
66
|
-
|
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(
|
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|