python-linkplay 0.0.5__py3-none-any.whl → 0.0.7__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/__main__.py CHANGED
@@ -1,11 +1,11 @@
1
1
  import asyncio
2
- import aiohttp
3
2
 
4
3
  from linkplay.controller import LinkPlayController
4
+ from linkplay.utils import create_unverified_client_session
5
5
 
6
6
 
7
7
  async def main():
8
- async with aiohttp.ClientSession() as session:
8
+ async with create_unverified_client_session() as session:
9
9
  controller = LinkPlayController(session)
10
10
 
11
11
  await controller.discover_bridges()
@@ -16,5 +16,6 @@ async def main():
16
16
  for multiroom in controller.multirooms:
17
17
  print(multiroom.followers)
18
18
 
19
+
19
20
  if __name__ == "__main__":
20
21
  asyncio.run(main())
linkplay/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.0.5'
1
+ __version__ = '0.0.7'
linkplay/bridge.py CHANGED
@@ -1,32 +1,34 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any
3
- from aiohttp import ClientSession
4
4
 
5
5
  from linkplay.consts import (
6
+ INPUT_MODE_MAP,
7
+ PLAY_MODE_SEND_MAP,
6
8
  ChannelType,
7
- LinkPlayCommand,
8
9
  DeviceAttribute,
9
- PlayerAttribute,
10
- MuteMode,
11
10
  EqualizerMode,
11
+ InputMode,
12
+ LinkPlayCommand,
12
13
  LoopMode,
13
- PLAY_MODE_SEND_MAP,
14
+ MultiroomAttribute,
15
+ MuteMode,
16
+ PlayerAttribute,
17
+ PlayingMode,
14
18
  PlayingStatus,
15
- InputMode,
16
19
  SpeakerType,
17
- PlayingMode,
18
- INPUT_MODE_MAP,
19
- MultiroomAttribute
20
20
  )
21
-
22
- from linkplay.utils import session_call_api_json, session_call_api_ok, decode_hexstr
21
+ from linkplay.endpoint import LinkPlayEndpoint
22
+ from linkplay.utils import decode_hexstr
23
23
 
24
24
 
25
- class LinkPlayDevice():
25
+ class LinkPlayDevice:
26
26
  """Represents a LinkPlay device."""
27
27
 
28
28
  bridge: LinkPlayBridge
29
- properties: dict[DeviceAttribute, str] = dict.fromkeys(DeviceAttribute.__members__.values(), "")
29
+ properties: dict[DeviceAttribute, str] = dict.fromkeys(
30
+ DeviceAttribute.__members__.values(), ""
31
+ )
30
32
 
31
33
  def __init__(self, bridge: LinkPlayBridge):
32
34
  self.bridge = bridge
@@ -42,31 +44,39 @@ class LinkPlayDevice():
42
44
  @property
43
45
  def uuid(self) -> str:
44
46
  """The UUID of the device."""
45
- return self.properties[DeviceAttribute.UUID]
47
+ return self.properties.get(DeviceAttribute.UUID, "")
46
48
 
47
49
  @property
48
50
  def name(self) -> str:
49
51
  """The name of the device."""
50
- return self.properties[DeviceAttribute.DEVICE_NAME]
52
+ return self.properties.get(DeviceAttribute.DEVICE_NAME, "")
51
53
 
52
54
  @property
53
55
  def playmode_support(self) -> list[PlayingMode]:
54
56
  """Returns the player playmode support."""
55
57
 
56
- flags = InputMode(int(self.properties[DeviceAttribute.PLAYMODE_SUPPORT], base=16))
58
+ flags = InputMode(
59
+ int(self.properties[DeviceAttribute.PLAYMODE_SUPPORT], base=16)
60
+ )
57
61
  return [INPUT_MODE_MAP[flag] for flag in flags]
58
62
 
59
63
  @property
60
64
  def eth(self) -> str:
61
65
  """Returns the ethernet address."""
62
- return self.properties[DeviceAttribute.ETH_DHCP]
66
+ return (
67
+ self.properties[DeviceAttribute.ETH_DHCP]
68
+ if DeviceAttribute.ETH_DHCP in self.properties
69
+ else self.properties[DeviceAttribute.ETH0]
70
+ )
63
71
 
64
72
 
65
- class LinkPlayPlayer():
73
+ class LinkPlayPlayer:
66
74
  """Represents a LinkPlay player."""
67
75
 
68
76
  bridge: LinkPlayBridge
69
- properties: dict[PlayerAttribute, str] = dict.fromkeys(PlayerAttribute.__members__.values(), "")
77
+ properties: dict[PlayerAttribute, str] = dict.fromkeys(
78
+ PlayerAttribute.__members__.values(), ""
79
+ )
70
80
 
71
81
  def __init__(self, bridge: LinkPlayBridge):
72
82
  self.bridge = bridge
@@ -134,111 +144,104 @@ class LinkPlayPlayer():
134
144
 
135
145
  async def set_play_mode(self, mode: PlayingMode) -> None:
136
146
  """Set the play mode."""
137
- await self.bridge.request(LinkPlayCommand.SWITCH_MODE.format(PLAY_MODE_SEND_MAP[mode])) # type: ignore[str-format]
147
+ await self.bridge.request(
148
+ LinkPlayCommand.SWITCH_MODE.format(PLAY_MODE_SEND_MAP[mode])
149
+ ) # type: ignore[str-format]
138
150
 
139
151
  @property
140
152
  def muted(self) -> bool:
141
153
  """Returns if the player is muted."""
142
- return self.properties[PlayerAttribute.MUTED] == MuteMode.MUTED
154
+ return self.properties.get(PlayerAttribute.MUTED, MuteMode.UNMUTED) == MuteMode.MUTED
143
155
 
144
156
  @property
145
157
  def title(self) -> str:
146
158
  """Returns if the currently playing title of the track."""
147
- return self.properties[PlayerAttribute.TITLE]
159
+ return self.properties.get(PlayerAttribute.TITLE, "")
148
160
 
149
161
  @property
150
162
  def artist(self) -> str:
151
163
  """Returns if the currently playing artist."""
152
- return self.properties[PlayerAttribute.ARTIST]
164
+ return self.properties.get(PlayerAttribute.ARTIST, "")
153
165
 
154
166
  @property
155
167
  def album(self) -> str:
156
168
  """Returns if the currently playing album."""
157
- return self.properties[PlayerAttribute.ALBUM]
169
+ return self.properties.get(PlayerAttribute.ALBUM, "")
158
170
 
159
171
  @property
160
172
  def volume(self) -> int:
161
173
  """Returns the player volume, expressed in %."""
162
- return int(self.properties[PlayerAttribute.VOLUME])
174
+ return int(self.properties.get(PlayerAttribute.VOLUME, 0))
163
175
 
164
176
  @property
165
177
  def current_position(self) -> int:
166
178
  """Returns the current position of the track in milliseconds."""
167
- return int(self.properties[PlayerAttribute.CURRENT_POSITION])
179
+ return int(self.properties.get(PlayerAttribute.CURRENT_POSITION, 0))
168
180
 
169
181
  @property
170
182
  def total_length(self) -> int:
171
183
  """Returns the total length of the track in milliseconds."""
172
- return int(self.properties[PlayerAttribute.TOTAL_LENGTH])
184
+ return int(self.properties.get(PlayerAttribute.TOTAL_LENGTH, 0))
173
185
 
174
186
  @property
175
187
  def status(self) -> PlayingStatus:
176
188
  """Returns the current playing status."""
177
- return PlayingStatus(self.properties[PlayerAttribute.PLAYING_STATUS])
189
+ return PlayingStatus(self.properties.get(PlayerAttribute.PLAYING_STATUS, PlayingStatus.STOPPED))
178
190
 
179
191
  @property
180
192
  def equalizer_mode(self) -> EqualizerMode:
181
193
  """Returns the current equalizer mode."""
182
- return EqualizerMode(self.properties[PlayerAttribute.EQUALIZER_MODE])
194
+ return EqualizerMode(self.properties.get(PlayerAttribute.EQUALIZER_MODE, EqualizerMode.CLASSIC))
183
195
 
184
196
  @property
185
197
  def speaker_type(self) -> SpeakerType:
186
198
  """Returns the current speaker the player is playing on."""
187
- return SpeakerType(self.properties[PlayerAttribute.SPEAKER_TYPE])
199
+ return SpeakerType(self.properties.get(PlayerAttribute.SPEAKER_TYPE, SpeakerType.MAIN_SPEAKER))
188
200
 
189
201
  @property
190
202
  def channel_type(self) -> ChannelType:
191
203
  """Returns the channel the player is playing on."""
192
- return ChannelType(self.properties[PlayerAttribute.CHANNEL_TYPE])
204
+ return ChannelType(self.properties.get(PlayerAttribute.CHANNEL_TYPE, ChannelType.STEREO))
193
205
 
194
206
  @property
195
207
  def play_mode(self) -> PlayingMode:
196
208
  """Returns the current playing mode of the player."""
197
- return PlayingMode(self.properties[PlayerAttribute.PLAYBACK_MODE])
209
+ return PlayingMode(self.properties.get(PlayerAttribute.PLAYBACK_MODE, PlayingMode.IDLE))
198
210
 
199
211
  @property
200
212
  def loop_mode(self) -> LoopMode:
201
213
  """Returns the current playlist mode."""
202
- return LoopMode(self.properties[PlayerAttribute.PLAYLIST_MODE])
214
+ return LoopMode(self.properties.get(PlayerAttribute.PLAYLIST_MODE, LoopMode.CONTINUOUS_PLAYBACK))
203
215
 
204
216
 
205
- class LinkPlayBridge():
217
+ class LinkPlayBridge:
206
218
  """Represents a LinkPlay bridge to control the device and player attached to it."""
207
219
 
208
- protocol: str
209
- ip_address: str
210
- session: ClientSession
220
+ endpoint: LinkPlayEndpoint
211
221
  device: LinkPlayDevice
212
222
  player: LinkPlayPlayer
213
223
 
214
- def __init__(self, protocol: str, ip_address: str, session: ClientSession):
215
- self.protocol = protocol
216
- self.ip_address = ip_address
217
- self.session = session
224
+ def __init__(self, *, endpoint: LinkPlayEndpoint):
225
+ self.endpoint = endpoint
218
226
  self.device = LinkPlayDevice(self)
219
227
  self.player = LinkPlayPlayer(self)
220
228
 
221
- def __repr__(self) -> str:
229
+ def __str__(self) -> str:
222
230
  if self.device.name == "":
223
- return self.endpoint
231
+ return f"{self.endpoint}"
224
232
 
225
233
  return self.device.name
226
234
 
227
- @property
228
- def endpoint(self) -> str:
229
- """Returns the current player endpoint."""
230
- return f"{self.protocol}://{self.ip_address}"
231
-
232
235
  async def json_request(self, command: str) -> dict[str, str]:
233
236
  """Performs a GET request on the given command and returns the result as a JSON object."""
234
- return await session_call_api_json(self.endpoint, self.session, command)
237
+ return await self.endpoint.json_request(command)
235
238
 
236
239
  async def request(self, command: str) -> None:
237
240
  """Performs a GET request on the given command and verifies the result."""
238
- await session_call_api_ok(self.endpoint, self.session, command)
241
+ await self.endpoint.request(command)
239
242
 
240
243
 
241
- class LinkPlayMultiroom():
244
+ class LinkPlayMultiroom:
242
245
  """Represents a LinkPlay multiroom group. Contains a leader and a list of followers.
243
246
  The leader is the device that controls the group."""
244
247
 
@@ -251,14 +254,21 @@ class LinkPlayMultiroom():
251
254
 
252
255
  async def update_status(self, bridges: list[LinkPlayBridge]) -> None:
253
256
  """Updates the multiroom status."""
254
- properties: dict[Any, Any] = await self.leader.json_request(LinkPlayCommand.MULTIROOM_LIST)
257
+ properties: dict[Any, Any] = await self.leader.json_request(
258
+ LinkPlayCommand.MULTIROOM_LIST
259
+ )
255
260
 
256
261
  self.followers = []
257
262
  if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
258
263
  return
259
264
 
260
- follower_uuids = [follower[MultiroomAttribute.UUID] for follower in properties[MultiroomAttribute.FOLLOWER_LIST]]
261
- new_followers = [bridge for bridge in bridges if bridge.device.uuid in follower_uuids]
265
+ follower_uuids = [
266
+ follower[MultiroomAttribute.UUID]
267
+ for follower in properties[MultiroomAttribute.FOLLOWER_LIST]
268
+ ]
269
+ new_followers = [
270
+ bridge for bridge in bridges if bridge.device.uuid in follower_uuids
271
+ ]
262
272
  self.followers.extend(new_followers)
263
273
 
264
274
  async def ungroup(self) -> None:
@@ -268,12 +278,16 @@ class LinkPlayMultiroom():
268
278
 
269
279
  async def add_follower(self, follower: LinkPlayBridge) -> None:
270
280
  """Adds a follower to the multiroom group."""
271
- await follower.request(LinkPlayCommand.MULTIROOM_JOIN.format(self.leader.device.eth)) # type: ignore[str-format]
281
+ await follower.request(
282
+ LinkPlayCommand.MULTIROOM_JOIN.format(self.leader.device.eth)
283
+ ) # type: ignore[str-format]
272
284
  self.followers.append(follower)
273
285
 
274
286
  async def remove_follower(self, follower: LinkPlayBridge) -> None:
275
287
  """Removes a follower from the multiroom group."""
276
- await self.leader.request(LinkPlayCommand.MULTIROOM_KICK.format(follower.device.eth)) # type: ignore[str-format]
288
+ await self.leader.request(
289
+ LinkPlayCommand.MULTIROOM_KICK.format(follower.device.eth)
290
+ ) # type: ignore[str-format]
277
291
  self.followers.remove(follower)
278
292
 
279
293
  async def set_volume(self, value: int) -> None:
linkplay/consts.py CHANGED
@@ -1,18 +1,72 @@
1
- from enum import StrEnum, IntFlag
1
+ from enum import IntFlag, StrEnum
2
2
 
3
3
  API_ENDPOINT: str = "{}/httpapi.asp?command={}"
4
4
  API_TIMEOUT: int = 2
5
5
  UNKNOWN_TRACK_PLAYING: str = "Unknown"
6
- UPNP_DEVICE_TYPE = 'urn:schemas-upnp-org:device:MediaRenderer:1'
6
+ UPNP_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1"
7
+
8
+ MTLS_CERTIFICATE_CONTENTS = """
9
+ -----BEGIN PRIVATE KEY-----
10
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCk/u2tH0LwCOv8
11
+ JmqLvQdjNAdkxfSCPwrdHM7STlq5xaJGQe29yd8kjP7h0wERkeO/9JO62wUHBu0P
12
+ WIsS/jwLG+G3oAU+7BNfjmBhDXyHIRQLzAWEbtxbsSTke1losfvlQXGumXrMqf9X
13
+ LdIYvbA53mp8GImQbJkaCDvwnEdUFkuJ0W5Tofr58jJqfCt6OPwHmnP4oC6LpPtJ
14
+ YDy7r1Q9sLgCYEtDEw/Mhf+mKuC0pnst52e1qceVjvCuUBoeuhk6kpnEbpSdEKbD
15
+ bdE8cPoVRmrj1//PLFMVtNB7k2aPMb3CcoJ/dHxaCXwk9b3jIBs6CyWixN92CuaO
16
+ Q98Ug/YlAgMBAAECggEAHyCpHlwjeL12J9/nge1rk1+hdXWTJ29VUVm5+xslKp8K
17
+ ek6912xaWL7w5xGzxejMGs69gCcJz8WSu65srmygT0g3UTkzRCetj/2AWU7+C1BG
18
+ Q+N9tvpjQDkvSJusxn+tkhbCp7n03N/FeGEAngJLWN+JH1hRu5mBWNPs2vvgyRAO
19
+ Cv95G7uENavCUXcyYsKPoAfz3ebD/idwwWW2RKAd0ufYeafiFC0ImTLcpEjBvCTW
20
+ UoAniBSVx1PHK4IAUb3pMdPtIv1uBlIMotHS/GdEyHU6qOsX5ijHqncHHneaytmL
21
+ +wJukPqASEBl3F2UnzryBUgGqr1wyH9vtPGjklnngQKBgQDZv3oxZWul//2LV+jo
22
+ ZipbnP6nwG3J6pOWPDD3dHoZ6Q2DRyJXN5ty40PS393GVvrSJSdRGeD9+ox5sFoj
23
+ iUMgd6kHG4ME7Fre57zUkqy1Ln1K1fkP5tBUD0hviigHBWih2/Nyl2vrdvX5Wpxx
24
+ 5r42UQa9nOzrNB03DTOhDrUszQKBgQDB+xdMRNSFfCatQj+y2KehcH9kaANPvT0l
25
+ l9vgb72qks01h05GSPBZnT1qfndh/Myno9KuVPhJ0HrVwRAjZTd4T69fAH3imW+R
26
+ 7HP+RgDen4SRTxj6UTJh2KZ8fdPeCby1xTwxYNjq8HqpiO6FHZpE+l4FE8FalZK+
27
+ Z3GhE7DuuQKBgDq7b+0U6xVKWAwWuSa+L9yoGvQKblKRKB/Uumx0iV6lwtRPAo89
28
+ 23sAm9GsOnh+C4dVKCay8UHwK6XDEH0XT/jY7cmR/SP90IDhRsibi2QPVxIxZs2I
29
+ N1cFDEexnxxNtCw8VIzrFNvdKXmJnDsIvvONpWDNjAXg96RatjtR6UJdAoGBAIAx
30
+ HU5r1j54s16gf1QD1ZPcsnN6QWX622PynX4OmjsVVMPhLRtJrHysax/rf52j4OOQ
31
+ YfSPdp3hRqvoMHATvbqmfnC79HVBjPfUWTtaq8xzgro8mXcjHbaH5E41IUSFDs7Z
32
+ D1Raej+YuJc9RNN3orGe+29DhO4GFrn5xp/6UV0RAoGBAKUdRgryWzaN4auzWaRD
33
+ lxoMhlwQdCXzBI1YLH2QUL8elJOHMNfmja5G9iW07ZrhhvQBGNDXFbFrX4hI3c/0
34
+ JC3SPhaaedIjOe9Qd3tn5KgYxbBnWnCTt0kxgro+OM3ORgJseSWbKdRrjOkUxkab
35
+ /NDvel7IF63U4UEkrVVt1bYg
36
+ -----END PRIVATE KEY-----
37
+ -----BEGIN CERTIFICATE-----
38
+ MIIDmDCCAoACAQEwDQYJKoZIhvcNAQELBQAwgZExCzAJBgNVBAYTAkNOMREwDwYD
39
+ VQQIDAhTaGFuZ2hhaTERMA8GA1UEBwwIU2hhbmdoYWkxETAPBgNVBAoMCExpbmtw
40
+ bGF5MQwwCgYDVQQLDANpbmMxGTAXBgNVBAMMEHd3dy5saW5rcGxheS5jb20xIDAe
41
+ BgkqhkiG9w0BCQEWEW1haWxAbGlua3BsYXkuY29tMB4XDTE4MTExNTAzMzI1OVoX
42
+ DTQ2MDQwMTAzMzI1OVowgZExCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGFuZ2hh
43
+ aTERMA8GA1UEBwwIU2hhbmdoYWkxETAPBgNVBAoMCExpbmtwbGF5MQwwCgYDVQQL
44
+ DANpbmMxGTAXBgNVBAMMEHd3dy5saW5rcGxheS5jb20xIDAeBgkqhkiG9w0BCQEW
45
+ EW1haWxAbGlua3BsYXkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
46
+ AQEApP7trR9C8Ajr/CZqi70HYzQHZMX0gj8K3RzO0k5aucWiRkHtvcnfJIz+4dMB
47
+ EZHjv/STutsFBwbtD1iLEv48Cxvht6AFPuwTX45gYQ18hyEUC8wFhG7cW7Ek5HtZ
48
+ aLH75UFxrpl6zKn/Vy3SGL2wOd5qfBiJkGyZGgg78JxHVBZLidFuU6H6+fIyanwr
49
+ ejj8B5pz+KAui6T7SWA8u69UPbC4AmBLQxMPzIX/pirgtKZ7LedntanHlY7wrlAa
50
+ HroZOpKZxG6UnRCmw23RPHD6FUZq49f/zyxTFbTQe5NmjzG9wnKCf3R8Wgl8JPW9
51
+ 4yAbOgslosTfdgrmjkPfFIP2JQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQARmy6f
52
+ esrifhW5NM9i3xsEVp945iSXhqHgrtIROgrC7F1EIAyoIiBdaOvitZVtsYc7Ivys
53
+ QtyVmEGscyjuYTdfigvwTVVj2oCeFv1Xjf+t/kSuk6X3XYzaxPPnFG4nAe2VwghE
54
+ rbZG0K5l8iXM7Lm+ZdqQaAYVWsQDBG8lbczgkB9q5ed4zbDPf6Fsrsynxji/+xa4
55
+ 9ARfyHlkCDBThGNnnl+QITtfOWxm/+eReILUQjhwX+UwbY07q/nUxLlK6yrzyjnn
56
+ wi2B2GovofQ/4icVZ3ecTqYK3q9gEtJi72V+dVHM9kSA4Upy28Y0U1v56uoqeWQ6
57
+ uc2m8y8O/hXPSfKd
58
+ -----END CERTIFICATE-----
59
+ """
7
60
 
8
61
 
9
62
  class LinkPlayCommand(StrEnum):
10
63
  """Defines the LinkPlay commands."""
11
- DEVICE_STATUS = "getStatus"
64
+
65
+ DEVICE_STATUS = "getStatusEx"
12
66
  SYSLOG = "getsyslog"
13
67
  UPDATE_SERVER = "GetUpdateServer"
14
68
  REBOOT = "reboot"
15
- PLAYER_STATUS = "getPlayerStatus"
69
+ PLAYER_STATUS = "getPlayerStatusEx"
16
70
  NEXT = "setPlayerCmd:next"
17
71
  PREVIOUS = "setPlayerCmd:prev"
18
72
  UNMUTE = "setPlayerCmd:mute:0"
@@ -39,12 +93,14 @@ class LinkPlayCommand(StrEnum):
39
93
 
40
94
  class SpeakerType(StrEnum):
41
95
  """Defines the speaker type."""
96
+
42
97
  MAIN_SPEAKER = "0"
43
98
  SUB_SPEAKER = "1"
44
99
 
45
100
 
46
101
  class ChannelType(StrEnum):
47
102
  """Defines the channel type."""
103
+
48
104
  STEREO = "0"
49
105
  LEFT_CHANNEL = "1"
50
106
  RIGHT_CHANNEL = "2"
@@ -52,6 +108,8 @@ class ChannelType(StrEnum):
52
108
 
53
109
  class PlayingMode(StrEnum):
54
110
  """Defines a possible playing mode."""
111
+
112
+ DAB = "-96" # Unmapped
55
113
  IDLE = "-1"
56
114
  NONE = "0"
57
115
  AIRPLAY = "1"
@@ -71,7 +129,6 @@ class PlayingMode(StrEnum):
71
129
  SPOTIFY = "31"
72
130
  LINE_IN = "40"
73
131
  BLUETOOTH = "41"
74
- EXT_LOCAL = "42"
75
132
  OPTICAL = "43"
76
133
  RCA = "44"
77
134
  COAXIAL = "45"
@@ -79,57 +136,69 @@ class PlayingMode(StrEnum):
79
136
  LINE_IN_2 = "47"
80
137
  XLR = "48"
81
138
  HDMI = "49"
82
- MIRROR = "50"
139
+ CD = "50"
83
140
  USB_DAC = "51"
84
141
  TF_CARD_2 = "52"
142
+ EXTERN_BLUETOOTH = "53"
143
+ PHONO = "54"
85
144
  OPTICAL_2 = "56"
145
+ COAXIAL_2 = "57"
146
+ ARC = "58"
86
147
  TALK = "60"
87
- SLAVE = "99"
148
+ FOLLOWER = "99"
88
149
 
89
150
 
90
151
  # Map between a play mode and how to activate the play mode
91
- PLAY_MODE_SEND_MAP: dict[PlayingMode, str] = { # case sensitive!
92
- PlayingMode.NONE: 'Idle',
93
- PlayingMode.AIRPLAY: 'Airplay',
94
- PlayingMode.DLNA: 'DLNA',
95
- PlayingMode.QPLAY: 'QPlay',
96
- PlayingMode.NETWORK: 'wifi',
97
- PlayingMode.WIIMU_LOCAL: 'udisk',
98
- PlayingMode.TF_CARD_1: 'TFcard',
99
- PlayingMode.API: 'API',
100
- PlayingMode.UDISK: 'udisk',
101
- PlayingMode.ALARM: 'Alarm',
102
- PlayingMode.SPOTIFY: 'Spotify',
103
- PlayingMode.LINE_IN: 'line-in',
104
- PlayingMode.BLUETOOTH: 'bluetooth',
105
- PlayingMode.OPTICAL: 'optical',
106
- PlayingMode.RCA: 'RCA',
107
- PlayingMode.COAXIAL: 'co-axial',
108
- PlayingMode.FM: 'FM',
109
- PlayingMode.LINE_IN_2: 'line-in2',
110
- PlayingMode.XLR: 'XLR',
111
- PlayingMode.HDMI: 'HDMI',
112
- PlayingMode.MIRROR: 'cd',
113
- PlayingMode.USB_DAC: 'USB DAC',
114
- PlayingMode.TF_CARD_2: 'TFcard',
115
- PlayingMode.TALK: 'Talk',
116
- PlayingMode.SLAVE: 'Idle',
117
- PlayingMode.OPTICAL_2: 'optical2'
152
+ PLAY_MODE_SEND_MAP: dict[PlayingMode, str] = { # case sensitive!
153
+ PlayingMode.NONE: "Idle",
154
+ PlayingMode.AIRPLAY: "Airplay",
155
+ PlayingMode.DLNA: "DLNA",
156
+ PlayingMode.QPLAY: "QPlay",
157
+ PlayingMode.NETWORK: "wifi",
158
+ PlayingMode.WIIMU_LOCAL: "udisk",
159
+ PlayingMode.TF_CARD_1: "TFcard",
160
+ PlayingMode.API: "API",
161
+ PlayingMode.UDISK: "udisk",
162
+ PlayingMode.ALARM: "Alarm",
163
+ PlayingMode.SPOTIFY: "Spotify",
164
+ PlayingMode.LINE_IN: "line-in",
165
+ PlayingMode.BLUETOOTH: "bluetooth",
166
+ PlayingMode.OPTICAL: "optical",
167
+ PlayingMode.RCA: "RCA",
168
+ PlayingMode.COAXIAL: "co-axial",
169
+ PlayingMode.FM: "FM",
170
+ PlayingMode.LINE_IN_2: "line-in2",
171
+ PlayingMode.XLR: "XLR",
172
+ PlayingMode.HDMI: "HDMI",
173
+ PlayingMode.CD: "cd",
174
+ PlayingMode.USB_DAC: "PCUSB",
175
+ PlayingMode.TF_CARD_2: "TFcard",
176
+ PlayingMode.TALK: "Talk",
177
+ PlayingMode.FOLLOWER: "Idle",
178
+ PlayingMode.OPTICAL_2: "optical2",
179
+ PlayingMode.PHONO: "phono",
180
+ PlayingMode.COAXIAL_2: "co-axial2",
181
+ PlayingMode.ARC: "ARC",
182
+ PlayingMode.DAB: "DAB",
183
+ PlayingMode.EXTERN_BLUETOOTH: "extern_bluetooth",
118
184
  }
119
185
 
120
186
 
121
187
  class LoopMode(StrEnum):
122
188
  """Defines the loop mode."""
189
+
123
190
  CONTINOUS_PLAY_ONE_SONG = "-1"
124
191
  PLAY_IN_ORDER = "0"
125
192
  CONTINUOUS_PLAYBACK = "1"
126
193
  RANDOM_PLAYBACK = "2"
127
194
  LIST_CYCLE = "3"
128
- FOUR = "4"
195
+ SHUFF_DISABLED_REPEAT_DISABLED = "4"
196
+ SHUFF_ENABLED_REPEAT_ENABLED_LOOP_ONCE = "5"
129
197
 
130
198
 
131
199
  class EqualizerMode(StrEnum):
132
200
  """Defines the equalizer mode."""
201
+
133
202
  NONE = "0"
134
203
  CLASSIC = "1"
135
204
  POP = "2"
@@ -139,6 +208,7 @@ class EqualizerMode(StrEnum):
139
208
 
140
209
  class PlayingStatus(StrEnum):
141
210
  """Defines the playing status."""
211
+
142
212
  PLAYING = "play"
143
213
  LOADING = "load"
144
214
  STOPPED = "stop"
@@ -147,20 +217,33 @@ class PlayingStatus(StrEnum):
147
217
 
148
218
  class MuteMode(StrEnum):
149
219
  """Defines the mute mode."""
220
+
150
221
  UNMUTED = "0"
151
222
  MUTED = "1"
152
223
 
153
224
 
154
225
  class InputMode(IntFlag):
155
226
  """Defines which inputs the player supports."""
227
+
156
228
  LINE_IN = 2
157
229
  BLUETOOTH = 4
158
230
  USB = 8
159
231
  OPTICAL = 16
232
+ RCA = 32
160
233
  COAXIAL = 64
234
+ FM = 128
161
235
  LINE_IN_2 = 256
236
+ XLR = 512
237
+ HDMI = 1024
238
+ CD = 2048
239
+ TF_CARD_1 = 8192
240
+ EXTERN_BLUETOOTH = 16384
162
241
  USB_DAC = 32768
242
+ PHONO = 65536
163
243
  OPTICAL_2 = 262144
244
+ COAXIAL_2 = 524288
245
+ FOLLOWER = 2097152 # unknown: is capable to follow leader?
246
+ ARC = 4194304
164
247
 
165
248
 
166
249
  # Map between the input modes and the play mode
@@ -169,15 +252,27 @@ INPUT_MODE_MAP: dict[InputMode, PlayingMode] = {
169
252
  InputMode.BLUETOOTH: PlayingMode.BLUETOOTH,
170
253
  InputMode.USB: PlayingMode.UDISK,
171
254
  InputMode.OPTICAL: PlayingMode.OPTICAL,
255
+ InputMode.RCA: PlayingMode.RCA,
172
256
  InputMode.COAXIAL: PlayingMode.COAXIAL,
257
+ InputMode.FM: PlayingMode.FM,
173
258
  InputMode.LINE_IN_2: PlayingMode.LINE_IN_2,
259
+ InputMode.XLR: PlayingMode.XLR,
260
+ InputMode.HDMI: PlayingMode.HDMI,
261
+ InputMode.CD: PlayingMode.CD,
262
+ InputMode.TF_CARD_1: PlayingMode.TF_CARD_1,
263
+ InputMode.EXTERN_BLUETOOTH: PlayingMode.EXTERN_BLUETOOTH,
174
264
  InputMode.USB_DAC: PlayingMode.USB_DAC,
175
- InputMode.OPTICAL_2: PlayingMode.OPTICAL_2
265
+ InputMode.PHONO: PlayingMode.PHONO,
266
+ InputMode.OPTICAL_2: PlayingMode.OPTICAL_2,
267
+ InputMode.COAXIAL_2: PlayingMode.COAXIAL_2,
268
+ InputMode.FOLLOWER: PlayingMode.FOLLOWER,
269
+ InputMode.ARC: PlayingMode.ARC,
176
270
  }
177
271
 
178
272
 
179
273
  class PlayerAttribute(StrEnum):
180
274
  """Defines the player attributes."""
275
+
181
276
  SPEAKER_TYPE = "type"
182
277
  CHANNEL_TYPE = "ch"
183
278
  PLAYBACK_MODE = "mode"
@@ -199,6 +294,7 @@ class PlayerAttribute(StrEnum):
199
294
 
200
295
  class DeviceAttribute(StrEnum):
201
296
  """Defines the device attributes."""
297
+
202
298
  UUID = "uuid"
203
299
  DEVICE_NAME = "DeviceName"
204
300
  GROUP_NAME = "GroupName"
@@ -278,10 +374,35 @@ class DeviceAttribute(StrEnum):
278
374
  ETH_MAC_ADDRESS = "ETH_MAC"
279
375
  SECURITY = "security"
280
376
  SECURITY_VERSION = "security_version"
377
+ FW_RELEASE_VERSION = "FW_Release_version"
378
+ PCB_VERSION = "PCB_version"
379
+ EXPIRED = "expired"
380
+ BT_MAC = "BT_MAC"
381
+ AP_MAC = "AP_MAC"
382
+ ETH0 = "eth0"
383
+ UPDATE_CHECK_COUNT = "update_check_count"
384
+ BLE_REMOTE_UPDATE_CHECKED_COUNTER = "BleRemote_update_checked_counter"
385
+ ALEXA_VER = "alexa_ver"
386
+ ALEXA_BETA_ENABLE = "alexa_beta_enable"
387
+ ALEXA_FORCE_BETA_CFG = "alexa_force_beta_cfg"
388
+ VOLUME_CONTROL = "volume_control"
389
+ WLAN_SNR = "wlanSnr"
390
+ WLAN_NOISE = "wlanNoise"
391
+ WLAN_FREQ = "wlanFreq"
392
+ WLAN_DATA_RATE = "wlanDataRate"
393
+ OTA_INTERFACE_VER = "ota_interface_ver"
394
+ EQ_SUPPORT = "EQ_support"
395
+ AUDIO_CHANNEL_CONFIG = "audio_channel_config"
396
+ APP_TIMEZONE_ID = "app_timezone_id"
397
+ AVS_TIMEZONE_ID = "avs_timezone_id"
398
+ TZ_INFO_VER = "tz_info_ver"
399
+ POWER_MODE = "power_mode"
400
+ SECURITY_CAPABILITIES = "security_capabilities"
281
401
 
282
402
 
283
403
  class MultiroomAttribute(StrEnum):
284
404
  """Defines the player attributes."""
405
+
285
406
  NUM_FOLLOWERS = "slaves"
286
407
  FOLLOWER_LIST = "slave_list"
287
408
  UUID = "uuid"
linkplay/controller.py CHANGED
@@ -4,7 +4,7 @@ from linkplay.bridge import LinkPlayBridge, LinkPlayMultiroom
4
4
  from linkplay.discovery import discover_linkplay_bridges
5
5
 
6
6
 
7
- class LinkPlayController():
7
+ class LinkPlayController:
8
8
  """Represents a LinkPlay controller to manage the devices and multirooms."""
9
9
 
10
10
  session: ClientSession
@@ -22,7 +22,11 @@ class LinkPlayController():
22
22
  # Discover new bridges
23
23
  discovered_bridges = await discover_linkplay_bridges(self.session)
24
24
  current_bridges = [bridge.device.uuid for bridge in self.bridges]
25
- new_bridges = [discovered_bridge for discovered_bridge in discovered_bridges if discovered_bridge.device.uuid not in current_bridges]
25
+ new_bridges = [
26
+ discovered_bridge
27
+ for discovered_bridge in discovered_bridges
28
+ if discovered_bridge.device.uuid not in current_bridges
29
+ ]
26
30
  self.bridges.extend(new_bridges)
27
31
 
28
32
  async def discover_multirooms(self) -> None:
@@ -31,7 +35,9 @@ class LinkPlayController():
31
35
  # Create new multirooms from new bridges
32
36
  new_multirooms = []
33
37
  for bridge in self.bridges:
34
- has_multiroom = any(multiroom for multiroom in self.multirooms if multiroom.leader == bridge)
38
+ has_multiroom = any(
39
+ multiroom for multiroom in self.multirooms if multiroom.leader == bridge
40
+ )
35
41
 
36
42
  if has_multiroom:
37
43
  continue
@@ -46,7 +52,9 @@ class LinkPlayController():
46
52
  await multiroom.update_status(self.bridges)
47
53
 
48
54
  # Remove multirooms if they have no followers
49
- empty_multirooms = [multiroom for multiroom in self.multirooms if not multiroom.followers]
55
+ empty_multirooms = [
56
+ multiroom for multiroom in self.multirooms if not multiroom.followers
57
+ ]
50
58
  for empty_multiroom in empty_multirooms:
51
59
  self.multirooms.remove(empty_multiroom)
52
60
 
linkplay/discovery.py CHANGED
@@ -3,40 +3,83 @@ from typing import Any
3
3
  from aiohttp import ClientSession
4
4
  from async_upnp_client.search import async_search
5
5
  from async_upnp_client.utils import CaseInsensitiveDict
6
+ from deprecated import deprecated
6
7
 
7
- from linkplay.consts import UPNP_DEVICE_TYPE, LinkPlayCommand, MultiroomAttribute
8
8
  from linkplay.bridge import LinkPlayBridge
9
+ from linkplay.consts import UPNP_DEVICE_TYPE, LinkPlayCommand, MultiroomAttribute
10
+ from linkplay.endpoint import LinkPlayApiEndpoint, LinkPlayEndpoint
9
11
  from linkplay.exceptions import LinkPlayRequestException
10
12
 
11
13
 
12
- async def linkplay_factory_bridge(ip_address: str, session: ClientSession) -> LinkPlayBridge | None:
14
+ @deprecated(
15
+ reason="Use linkplay_factory_bridge_endpoint with a LinkPlayEndpoint or linkplay_factory_httpapi_bridge instead.",
16
+ version="0.0.7",
17
+ )
18
+ async def linkplay_factory_bridge(
19
+ ip_address: str, session: ClientSession
20
+ ) -> LinkPlayBridge | None:
13
21
  """Attempts to create a LinkPlayBridge from the given IP address.
14
22
  Returns None if the device is not an expected LinkPlay device."""
15
- bridge = LinkPlayBridge("http", ip_address, session)
23
+ endpoint: LinkPlayApiEndpoint = LinkPlayApiEndpoint(
24
+ protocol="http", endpoint=ip_address, session=session
25
+ )
16
26
  try:
17
- await bridge.device.update_status()
18
- await bridge.player.update_status()
27
+ return await linkplay_factory_bridge_endpoint(endpoint)
19
28
  except LinkPlayRequestException:
20
29
  return None
30
+
31
+
32
+ async def linkplay_factory_bridge_endpoint(
33
+ endpoint: LinkPlayEndpoint,
34
+ ) -> LinkPlayBridge:
35
+ """Attempts to create a LinkPlayBridge from given LinkPlayEndpoint.
36
+ Raises LinkPlayRequestException if the device is not an expected LinkPlay device."""
37
+
38
+ bridge: LinkPlayBridge = LinkPlayBridge(endpoint=endpoint)
39
+ await bridge.device.update_status()
40
+ await bridge.player.update_status()
21
41
  return bridge
22
42
 
23
43
 
24
- async def discover_linkplay_bridges(session: ClientSession, discovery_through_multiroom: bool = True) -> list[LinkPlayBridge]:
44
+ async def linkplay_factory_httpapi_bridge(
45
+ ip_address: str, session: ClientSession
46
+ ) -> LinkPlayBridge:
47
+ """Attempts to create a LinkPlayBridge from the given IP address.
48
+ Attempts to use HTTPS first, then falls back to HTTP.
49
+ Raises LinkPlayRequestException if the device is not an expected LinkPlay device."""
50
+
51
+ https_endpoint: LinkPlayApiEndpoint = LinkPlayApiEndpoint(
52
+ protocol="https", endpoint=ip_address, session=session
53
+ )
54
+ try:
55
+ return await linkplay_factory_bridge_endpoint(https_endpoint)
56
+ except LinkPlayRequestException:
57
+ http_endpoint: LinkPlayApiEndpoint = LinkPlayApiEndpoint(
58
+ protocol="http", endpoint=ip_address, session=session
59
+ )
60
+ return await linkplay_factory_bridge_endpoint(http_endpoint)
61
+
62
+
63
+ async def discover_linkplay_bridges(
64
+ session: ClientSession, discovery_through_multiroom: bool = True
65
+ ) -> list[LinkPlayBridge]:
25
66
  """Attempts to discover LinkPlay devices on the local network."""
26
67
  bridges: dict[str, LinkPlayBridge] = {}
27
68
 
28
69
  async def add_linkplay_device_to_list(upnp_device: CaseInsensitiveDict):
29
- ip_address: str | None = upnp_device.get('_host')
70
+ ip_address: str | None = upnp_device.get("_host")
30
71
 
31
72
  if not ip_address:
32
73
  return
33
74
 
34
- if bridge := await linkplay_factory_bridge(ip_address, session):
75
+ try:
76
+ bridge = await linkplay_factory_httpapi_bridge(ip_address, session)
35
77
  bridges[bridge.device.uuid] = bridge
78
+ except LinkPlayRequestException:
79
+ pass
36
80
 
37
81
  await async_search(
38
- search_target=UPNP_DEVICE_TYPE,
39
- async_callback=add_linkplay_device_to_list
82
+ search_target=UPNP_DEVICE_TYPE, async_callback=add_linkplay_device_to_list
40
83
  )
41
84
 
42
85
  # Discover additional bridges through grouped multirooms
@@ -51,17 +94,25 @@ async def discover_linkplay_bridges(session: ClientSession, discovery_through_mu
51
94
  return list(bridges.values())
52
95
 
53
96
 
54
- async def discover_bridges_through_multiroom(bridge: LinkPlayBridge,
55
- session: ClientSession) -> list[LinkPlayBridge]:
97
+ async def discover_bridges_through_multiroom(
98
+ bridge: LinkPlayBridge, session: ClientSession
99
+ ) -> list[LinkPlayBridge]:
56
100
  """Discovers bridges through the multiroom of the provided bridge."""
57
- properties: dict[Any, Any] = await bridge.json_request(LinkPlayCommand.MULTIROOM_LIST)
101
+ properties: dict[Any, Any] = await bridge.json_request(
102
+ LinkPlayCommand.MULTIROOM_LIST
103
+ )
58
104
 
59
105
  if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
60
106
  return []
61
107
 
62
108
  followers: list[LinkPlayBridge] = []
63
109
  for follower in properties[MultiroomAttribute.FOLLOWER_LIST]:
64
- if new_bridge := await linkplay_factory_bridge(follower[MultiroomAttribute.IP], session):
110
+ try:
111
+ new_bridge = await linkplay_factory_httpapi_bridge(
112
+ follower[MultiroomAttribute.IP], session
113
+ )
65
114
  followers.append(new_bridge)
115
+ except LinkPlayRequestException:
116
+ pass
66
117
 
67
118
  return followers
linkplay/endpoint.py ADDED
@@ -0,0 +1,40 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from aiohttp import ClientSession
4
+
5
+ from linkplay.utils import session_call_api_json, session_call_api_ok
6
+
7
+
8
+ class LinkPlayEndpoint(ABC):
9
+ """Represents an abstract LinkPlay endpoint."""
10
+
11
+ @abstractmethod
12
+ async def request(self, command: str) -> None:
13
+ """Performs a request on the given command and verifies the result."""
14
+
15
+ @abstractmethod
16
+ async def json_request(self, command: str) -> dict[str, str]:
17
+ """Performs a request on the given command and returns the result as a JSON object."""
18
+
19
+
20
+ class LinkPlayApiEndpoint(LinkPlayEndpoint):
21
+ """Represents a LinkPlay HTTP API endpoint."""
22
+
23
+ def __init__(self, *, protocol: str, endpoint: str, session: ClientSession):
24
+ assert protocol in [
25
+ "http",
26
+ "https",
27
+ ], "Protocol must be either 'http' or 'https'"
28
+ self._endpoint: str = f"{protocol}://{endpoint}"
29
+ self._session: ClientSession = session
30
+
31
+ async def request(self, command: str) -> None:
32
+ """Performs a GET request on the given command and verifies the result."""
33
+ await session_call_api_ok(self._endpoint, self._session, command)
34
+
35
+ async def json_request(self, command: str) -> dict[str, str]:
36
+ """Performs a GET request on the given command and returns the result as a JSON object."""
37
+ return await session_call_api_json(self._endpoint, self._session, command)
38
+
39
+ def __str__(self) -> str:
40
+ return self._endpoint
linkplay/utils.py CHANGED
@@ -1,12 +1,17 @@
1
1
  import asyncio
2
- from typing import Dict
2
+ import contextlib
3
3
  import json
4
+ import os
5
+ import socket
6
+ import ssl
4
7
  from http import HTTPStatus
8
+ from typing import Dict
5
9
 
6
10
  import async_timeout
7
- from aiohttp import ClientSession, ClientError
11
+ from aiohttp import ClientError, ClientSession, TCPConnector
12
+ from appdirs import AppDirs
8
13
 
9
- from linkplay.consts import API_ENDPOINT, API_TIMEOUT
14
+ from linkplay.consts import API_ENDPOINT, API_TIMEOUT, MTLS_CERTIFICATE_CONTENTS
10
15
  from linkplay.exceptions import LinkPlayRequestException
11
16
 
12
17
 
@@ -28,25 +33,32 @@ async def session_call_api(endpoint: str, session: ClientSession, command: str)
28
33
 
29
34
  try:
30
35
  async with async_timeout.timeout(API_TIMEOUT):
31
- response = await session.get(url, ssl=False)
36
+ response = await session.get(url)
32
37
 
33
38
  except (asyncio.TimeoutError, ClientError, asyncio.CancelledError) as error:
34
- raise LinkPlayRequestException(f"Error requesting data from '{url}'") from error
39
+ raise LinkPlayRequestException(
40
+ f"{error} error requesting data from '{url}'"
41
+ ) from error
35
42
 
36
43
  if response.status != HTTPStatus.OK:
37
- raise LinkPlayRequestException(f"Unexpected HTTPStatus {response.status} received from '{url}'")
44
+ raise LinkPlayRequestException(
45
+ f"Unexpected HTTPStatus {response.status} received from '{url}'"
46
+ )
38
47
 
39
48
  return await response.text()
40
49
 
41
50
 
42
- async def session_call_api_json(endpoint: str, session: ClientSession,
43
- command: str) -> Dict[str, str]:
51
+ async def session_call_api_json(
52
+ endpoint: str, session: ClientSession, command: str
53
+ ) -> Dict[str, str]:
44
54
  """Calls the LinkPlay API and returns the result as a JSON object."""
45
55
  result = await session_call_api(endpoint, session, command)
46
56
  return json.loads(result) # type: ignore
47
57
 
48
58
 
49
- async def session_call_api_ok(endpoint: str, session: ClientSession, command: str) -> None:
59
+ async def session_call_api_ok(
60
+ endpoint: str, session: ClientSession, command: str
61
+ ) -> None:
50
62
  """Calls the LinkPlay API and checks if the response is OK. Throws exception if not."""
51
63
  result = await session_call_api(endpoint, session, command)
52
64
 
@@ -60,3 +72,35 @@ def decode_hexstr(hexstr: str) -> str:
60
72
  return bytes.fromhex(hexstr).decode("utf-8")
61
73
  except ValueError:
62
74
  return hexstr
75
+
76
+
77
+ def create_unverified_context() -> ssl.SSLContext:
78
+ """Creates an unverified SSL context with the default mTLS certificate."""
79
+ dirs = AppDirs("python-linkplay")
80
+ mtls_certificate_path = os.path.join(dirs.user_data_dir, "linkplay.pem")
81
+
82
+ if not os.path.isdir(dirs.user_data_dir):
83
+ os.makedirs(dirs.user_data_dir, exist_ok=True)
84
+
85
+ if not os.path.isfile(mtls_certificate_path):
86
+ with open(mtls_certificate_path, "w", encoding="utf-8") as file:
87
+ file.write(MTLS_CERTIFICATE_CONTENTS)
88
+
89
+ sslcontext: ssl.SSLContext = ssl.create_default_context(
90
+ purpose=ssl.Purpose.SERVER_AUTH
91
+ )
92
+ sslcontext.load_cert_chain(certfile=mtls_certificate_path)
93
+ sslcontext.check_hostname = False
94
+ sslcontext.verify_mode = ssl.CERT_NONE
95
+ with contextlib.suppress(AttributeError):
96
+ # This only works for OpenSSL >= 1.0.0
97
+ sslcontext.options |= ssl.OP_NO_COMPRESSION
98
+ sslcontext.set_default_verify_paths()
99
+ return sslcontext
100
+
101
+
102
+ def create_unverified_client_session() -> ClientSession:
103
+ """Creates a ClientSession using the default unverified SSL context"""
104
+ context: ssl.SSLContext = create_unverified_context()
105
+ connector: TCPConnector = TCPConnector(family=socket.AF_UNSPEC, ssl=context)
106
+ return ClientSession(connector=connector)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python_linkplay
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: A Python Library for Seamless LinkPlay Device Control
5
5
  Author: Velleman Group nv
6
6
  License: MIT
@@ -8,18 +8,21 @@ Classifier: Programming Language :: Python :: 3
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: async-timeout >=4.0.3
12
- Requires-Dist: aiohttp >=3.8.5
13
- Requires-Dist: async-upnp-client >=0.36.2
11
+ Requires-Dist: async-timeout>=4.0.3
12
+ Requires-Dist: aiohttp>=3.8.5
13
+ Requires-Dist: appdirs>=1.4.4
14
+ Requires-Dist: async-upnp-client>=0.36.2
15
+ Requires-Dist: deprecated>=1.2.14
14
16
  Provides-Extra: testing
15
- Requires-Dist: pytest >=7.3.1 ; extra == 'testing'
16
- Requires-Dist: pytest-cov >=4.1.0 ; extra == 'testing'
17
- Requires-Dist: pytest-mock >=3.10.0 ; extra == 'testing'
18
- Requires-Dist: pytest-asyncio >=0.23.3 ; extra == 'testing'
19
- Requires-Dist: mypy >=1.3.0 ; extra == 'testing'
20
- Requires-Dist: flake8 >=6.0.0 ; extra == 'testing'
21
- Requires-Dist: tox >=4.6.0 ; extra == 'testing'
22
- Requires-Dist: typing-extensions >=4.6.3 ; extra == 'testing'
17
+ Requires-Dist: pytest>=7.3.1; extra == "testing"
18
+ Requires-Dist: pytest-cov>=4.1.0; extra == "testing"
19
+ Requires-Dist: pytest-mock>=3.10.0; extra == "testing"
20
+ Requires-Dist: pytest-asyncio>=0.23.3; extra == "testing"
21
+ Requires-Dist: mypy>=1.3.0; extra == "testing"
22
+ Requires-Dist: ruff>=0.5.4; extra == "testing"
23
+ Requires-Dist: tox>=4.6.0; extra == "testing"
24
+ Requires-Dist: typing-extensions>=4.6.3; extra == "testing"
25
+ Requires-Dist: pre-commit>=3.8.0; extra == "testing"
23
26
 
24
27
 
25
28
  [![PyPI package](https://badge.fury.io/py/python-linkplay.svg)](https://pypi.org/project/python-linkplay/)
@@ -0,0 +1,15 @@
1
+ linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
2
+ linkplay/__main__.py,sha256=rH3diy9W57N1mw16XeTYkymImeSTIWIaBeB2AGzlL28,559
3
+ linkplay/__version__.py,sha256=7EgDv-timUu06OY6bkABnaoin_WSpRfqEKwS3w4SJZ4,22
4
+ linkplay/bridge.py,sha256=KGD-gwJRhDIdE9jAOUyVlcpfGDb1diDUkggTDvVkf-M,11164
5
+ linkplay/consts.py,sha256=wz1lVRz-9hkymc9ucV_LHldcu-msYvimI0tjr2Ncgoc,12734
6
+ linkplay/controller.py,sha256=JIQAKPs3EK7ZwzoyzSy0HBl21gH9Cc9RrLXIGOMzkCM,2146
7
+ linkplay/discovery.py,sha256=aEzN_94pKLmHKYIL7DxSW0FYRsaF2ruZe2bwXz0zf5U,4299
8
+ linkplay/endpoint.py,sha256=qbB977_KltNRZlWlm-3JiByPZiie84Hn2TL523IfqGs,1486
9
+ linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
10
+ linkplay/utils.py,sha256=IdUtceKTA3vdY_HNzKUFZzPGXkQFW8E4yUNe5K9TNbo,3583
11
+ python_linkplay-0.0.7.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
12
+ python_linkplay-0.0.7.dist-info/METADATA,sha256=VCLgUHUz00tc0tLzAiOpMkPDeIbrHDfkYrItjH8VZXA,2955
13
+ python_linkplay-0.0.7.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
14
+ python_linkplay-0.0.7.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
15
+ python_linkplay-0.0.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,14 +0,0 @@
1
- linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
2
- linkplay/__main__.py,sha256=eBEvuF8rDs0vuexX3Mcd5O02GRPQtOOOAVNEWqZhhdg,502
3
- linkplay/__version__.py,sha256=eB5tEg_BNHHsJgQexgmpZJr4c2VbfooQZMXAWuDHoIg,22
4
- linkplay/bridge.py,sha256=NX02ZBcbaz7yZv1EeFvQ9CAUh5jVcuSABtpKIY4aj74,10948
5
- linkplay/consts.py,sha256=KIBGrRQqxd1B4kRO0Vl0e5-UzbMLzJGC_ECohzkSRwQ,7750
6
- linkplay/controller.py,sha256=f0-fxURcts4j_p5prW-EW5IkAcdsR5Zl8nT1aL_587U,2050
7
- linkplay/discovery.py,sha256=NIUC3FVPH-FcHvMmojJw0s0w2Q-WCuulGrQA-PDLcsw,2642
8
- linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
9
- linkplay/utils.py,sha256=E_SjIyeK76ishhwuU24m28y1FDAMEj1QbRNt-aHIMdA,2137
10
- python_linkplay-0.0.5.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
11
- python_linkplay-0.0.5.dist-info/METADATA,sha256=hF4ShFim1culKemftnbGIf_VZlxrwa6RPCBIEZSm830,2859
12
- python_linkplay-0.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
13
- python_linkplay-0.0.5.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
14
- python_linkplay-0.0.5.dist-info/RECORD,,