python-linkplay 0.0.8__py3-none-any.whl → 0.0.10__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 +39 -13
- linkplay/consts.py +13 -0
- linkplay/controller.py +8 -0
- linkplay/endpoint.py +25 -1
- linkplay/utils.py +105 -6
- {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/METADATA +2 -2
- python_linkplay-0.0.10.dist-info/RECORD +15 -0
- {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/WHEEL +1 -1
- python_linkplay-0.0.8.dist-info/RECORD +0 -15
- {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/LICENSE +0 -0
- {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/top_level.txt +0 -0
linkplay/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = '0.0.
|
1
|
+
__version__ = '0.0.10'
|
linkplay/bridge.py
CHANGED
@@ -19,7 +19,7 @@ from linkplay.consts import (
|
|
19
19
|
SpeakerType,
|
20
20
|
)
|
21
21
|
from linkplay.endpoint import LinkPlayEndpoint
|
22
|
-
from linkplay.utils import
|
22
|
+
from linkplay.utils import fixup_player_properties
|
23
23
|
|
24
24
|
|
25
25
|
class LinkPlayDevice:
|
@@ -83,10 +83,11 @@ class LinkPlayPlayer:
|
|
83
83
|
|
84
84
|
async def update_status(self) -> None:
|
85
85
|
"""Update the player status."""
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
properties: dict[PlayerAttribute, str] = await self.bridge.json_request(
|
87
|
+
LinkPlayCommand.PLAYER_STATUS
|
88
|
+
) # type: ignore[assignment]
|
89
|
+
|
90
|
+
self.properties = fixup_player_properties(properties)
|
90
91
|
|
91
92
|
async def next(self) -> None:
|
92
93
|
"""Play the next song in the playlist."""
|
@@ -148,10 +149,19 @@ class LinkPlayPlayer:
|
|
148
149
|
LinkPlayCommand.SWITCH_MODE.format(PLAY_MODE_SEND_MAP[mode])
|
149
150
|
) # type: ignore[str-format]
|
150
151
|
|
152
|
+
async def play_preset(self, preset_number: int) -> None:
|
153
|
+
"""Play a preset."""
|
154
|
+
if not 0 < preset_number <= 10:
|
155
|
+
raise ValueError("Preset must be between 1 and 10.")
|
156
|
+
await self.bridge.request(LinkPlayCommand.PLAY_PRESET.format(preset_number))
|
157
|
+
|
151
158
|
@property
|
152
159
|
def muted(self) -> bool:
|
153
160
|
"""Returns if the player is muted."""
|
154
|
-
return
|
161
|
+
return (
|
162
|
+
self.properties.get(PlayerAttribute.MUTED, MuteMode.UNMUTED)
|
163
|
+
== MuteMode.MUTED
|
164
|
+
)
|
155
165
|
|
156
166
|
@property
|
157
167
|
def title(self) -> str:
|
@@ -186,32 +196,46 @@ class LinkPlayPlayer:
|
|
186
196
|
@property
|
187
197
|
def status(self) -> PlayingStatus:
|
188
198
|
"""Returns the current playing status."""
|
189
|
-
return PlayingStatus(
|
199
|
+
return PlayingStatus(
|
200
|
+
self.properties.get(PlayerAttribute.PLAYING_STATUS, PlayingStatus.STOPPED)
|
201
|
+
)
|
190
202
|
|
191
203
|
@property
|
192
204
|
def equalizer_mode(self) -> EqualizerMode:
|
193
205
|
"""Returns the current equalizer mode."""
|
194
|
-
return EqualizerMode(
|
206
|
+
return EqualizerMode(
|
207
|
+
self.properties.get(PlayerAttribute.EQUALIZER_MODE, EqualizerMode.CLASSIC)
|
208
|
+
)
|
195
209
|
|
196
210
|
@property
|
197
211
|
def speaker_type(self) -> SpeakerType:
|
198
212
|
"""Returns the current speaker the player is playing on."""
|
199
|
-
return SpeakerType(
|
213
|
+
return SpeakerType(
|
214
|
+
self.properties.get(PlayerAttribute.SPEAKER_TYPE, SpeakerType.MAIN_SPEAKER)
|
215
|
+
)
|
200
216
|
|
201
217
|
@property
|
202
218
|
def channel_type(self) -> ChannelType:
|
203
219
|
"""Returns the channel the player is playing on."""
|
204
|
-
return ChannelType(
|
220
|
+
return ChannelType(
|
221
|
+
self.properties.get(PlayerAttribute.CHANNEL_TYPE, ChannelType.STEREO)
|
222
|
+
)
|
205
223
|
|
206
224
|
@property
|
207
225
|
def play_mode(self) -> PlayingMode:
|
208
226
|
"""Returns the current playing mode of the player."""
|
209
|
-
return PlayingMode(
|
227
|
+
return PlayingMode(
|
228
|
+
self.properties.get(PlayerAttribute.PLAYBACK_MODE, PlayingMode.IDLE)
|
229
|
+
)
|
210
230
|
|
211
231
|
@property
|
212
232
|
def loop_mode(self) -> LoopMode:
|
213
233
|
"""Returns the current playlist mode."""
|
214
|
-
return LoopMode(
|
234
|
+
return LoopMode(
|
235
|
+
self.properties.get(
|
236
|
+
PlayerAttribute.PLAYLIST_MODE, LoopMode.CONTINUOUS_PLAYBACK
|
237
|
+
)
|
238
|
+
)
|
215
239
|
|
216
240
|
|
217
241
|
class LinkPlayBridge:
|
@@ -292,7 +316,9 @@ class LinkPlayMultiroom:
|
|
292
316
|
|
293
317
|
async def set_volume(self, value: int) -> None:
|
294
318
|
"""Sets the volume for the multiroom group."""
|
295
|
-
|
319
|
+
if not 0 <= value <= 100:
|
320
|
+
raise ValueError("Volume must be between 0 and 100")
|
321
|
+
|
296
322
|
str_vol = str(value)
|
297
323
|
await self.leader.request(LinkPlayCommand.MULTIROOM_VOL.format(str_vol)) # type: ignore[str-format]
|
298
324
|
|
linkplay/consts.py
CHANGED
@@ -4,6 +4,8 @@ API_ENDPOINT: str = "{}/httpapi.asp?command={}"
|
|
4
4
|
API_TIMEOUT: int = 2
|
5
5
|
UNKNOWN_TRACK_PLAYING: str = "Unknown"
|
6
6
|
UPNP_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1"
|
7
|
+
TCPPORT = 8899
|
8
|
+
TCP_MESSAGE_LENGTH = 1024
|
7
9
|
|
8
10
|
MTLS_CERTIFICATE_CONTENTS = """
|
9
11
|
-----BEGIN PRIVATE KEY-----
|
@@ -89,6 +91,17 @@ class LinkPlayCommand(StrEnum):
|
|
89
91
|
MULTIROOM_MUTE = "setPlayerCmd:slave_mute:mute"
|
90
92
|
MULTIROOM_UNMUTE = "setPlayerCmd:slave_mute:unmute"
|
91
93
|
MULTIROOM_JOIN = "ConnectMasterAp:JoinGroupMaster:eth{}:wifi0.0.0.0"
|
94
|
+
PLAY_PRESET = "MCUKeyShortClick:{}"
|
95
|
+
|
96
|
+
|
97
|
+
class LinkPlayTcpUartCommand(StrEnum):
|
98
|
+
"""Defined LinkPlay TCPUART commands."""
|
99
|
+
|
100
|
+
GET_METADATA = "MCU+MEA+GET"
|
101
|
+
PRESET_PLAY = "MCU+KEY+{:03}"
|
102
|
+
PRESET_NEXT = "MCU+KEY+NXT"
|
103
|
+
INPUT_WIFI = "MCU+PLM+000"
|
104
|
+
INPUT_BLUETOOTH = "MCU+PLM+006"
|
92
105
|
|
93
106
|
|
94
107
|
class SpeakerType(StrEnum):
|
linkplay/controller.py
CHANGED
@@ -29,6 +29,14 @@ class LinkPlayController:
|
|
29
29
|
]
|
30
30
|
self.bridges.extend(new_bridges)
|
31
31
|
|
32
|
+
async def add_bridge(self, bridge_to_add: LinkPlayBridge) -> None:
|
33
|
+
"""Add given LinkPlay device if not already added."""
|
34
|
+
|
35
|
+
# Add bridge
|
36
|
+
current_bridges = [bridge.device.uuid for bridge in self.bridges]
|
37
|
+
if bridge_to_add.device.uuid not in current_bridges:
|
38
|
+
self.bridges.append(bridge_to_add)
|
39
|
+
|
32
40
|
async def discover_multirooms(self) -> None:
|
33
41
|
"""Attempts to discover multirooms on the local network."""
|
34
42
|
|
linkplay/endpoint.py
CHANGED
@@ -1,8 +1,15 @@
|
|
1
|
+
import asyncio
|
1
2
|
from abc import ABC, abstractmethod
|
2
3
|
|
3
4
|
from aiohttp import ClientSession
|
4
5
|
|
5
|
-
from linkplay.
|
6
|
+
from linkplay.consts import TCPPORT
|
7
|
+
from linkplay.utils import (
|
8
|
+
call_tcpuart,
|
9
|
+
call_tcpuart_json,
|
10
|
+
session_call_api_json,
|
11
|
+
session_call_api_ok,
|
12
|
+
)
|
6
13
|
|
7
14
|
|
8
15
|
class LinkPlayEndpoint(ABC):
|
@@ -38,3 +45,20 @@ class LinkPlayApiEndpoint(LinkPlayEndpoint):
|
|
38
45
|
|
39
46
|
def __str__(self) -> str:
|
40
47
|
return self._endpoint
|
48
|
+
|
49
|
+
|
50
|
+
class LinkPlayTcpUartEndpoint(LinkPlayEndpoint):
|
51
|
+
"""Represents a LinkPlay TCPUART API endpoint."""
|
52
|
+
|
53
|
+
def __init__(
|
54
|
+
self, *, connection: tuple[asyncio.StreamReader, asyncio.StreamWriter]
|
55
|
+
):
|
56
|
+
self._connection = connection
|
57
|
+
|
58
|
+
async def request(self, command: str) -> None:
|
59
|
+
reader, writer = self._connection
|
60
|
+
await call_tcpuart(reader, writer, command)
|
61
|
+
|
62
|
+
async def json_request(self, command: str) -> dict[str, str]:
|
63
|
+
reader, writer = self._connection
|
64
|
+
return await call_tcpuart_json(reader, writer, command)
|
linkplay/utils.py
CHANGED
@@ -1,20 +1,31 @@
|
|
1
1
|
import asyncio
|
2
2
|
import contextlib
|
3
3
|
import json
|
4
|
+
import logging
|
4
5
|
import os
|
5
6
|
import socket
|
6
7
|
import ssl
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
7
9
|
from http import HTTPStatus
|
8
|
-
from typing import Dict
|
9
10
|
|
10
11
|
import aiofiles
|
11
12
|
import async_timeout
|
12
13
|
from aiohttp import ClientError, ClientSession, TCPConnector
|
13
14
|
from appdirs import AppDirs
|
14
|
-
|
15
|
-
|
15
|
+
from deprecated import deprecated
|
16
|
+
|
17
|
+
from linkplay.consts import (
|
18
|
+
API_ENDPOINT,
|
19
|
+
API_TIMEOUT,
|
20
|
+
MTLS_CERTIFICATE_CONTENTS,
|
21
|
+
TCP_MESSAGE_LENGTH,
|
22
|
+
PlayerAttribute,
|
23
|
+
PlayingStatus,
|
24
|
+
)
|
16
25
|
from linkplay.exceptions import LinkPlayRequestException
|
17
26
|
|
27
|
+
_LOGGER = logging.getLogger(__name__)
|
28
|
+
|
18
29
|
|
19
30
|
async def session_call_api(endpoint: str, session: ClientSession, command: str) -> str:
|
20
31
|
"""Calls the LinkPlay API and returns the result as a string.
|
@@ -51,7 +62,7 @@ async def session_call_api(endpoint: str, session: ClientSession, command: str)
|
|
51
62
|
|
52
63
|
async def session_call_api_json(
|
53
64
|
endpoint: str, session: ClientSession, command: str
|
54
|
-
) ->
|
65
|
+
) -> dict[str, str]:
|
55
66
|
"""Calls the LinkPlay API and returns the result as a JSON object."""
|
56
67
|
result = await session_call_api(endpoint, session, command)
|
57
68
|
return json.loads(result) # type: ignore
|
@@ -67,6 +78,44 @@ async def session_call_api_ok(
|
|
67
78
|
raise LinkPlayRequestException(f"Didn't receive expected OK from {endpoint}")
|
68
79
|
|
69
80
|
|
81
|
+
async def call_tcpuart(
|
82
|
+
reader: asyncio.StreamReader, writer: asyncio.StreamWriter, cmd: str
|
83
|
+
) -> str:
|
84
|
+
"""Get the latest data from TCP UART service."""
|
85
|
+
payload_header: str = "18 96 18 20 "
|
86
|
+
payload_length: str = format(len(cmd), "02x")
|
87
|
+
payload_command_header: str = " 00 00 00 c1 02 00 00 00 00 00 00 00 00 00 00 "
|
88
|
+
payload_command_content: str = " ".join(hex(ord(c))[2:] for c in cmd)
|
89
|
+
|
90
|
+
async with async_timeout.timeout(API_TIMEOUT):
|
91
|
+
writer.write(
|
92
|
+
bytes.fromhex(
|
93
|
+
payload_header
|
94
|
+
+ payload_length
|
95
|
+
+ payload_command_header
|
96
|
+
+ payload_command_content
|
97
|
+
)
|
98
|
+
)
|
99
|
+
|
100
|
+
data: bytes = await reader.read(TCP_MESSAGE_LENGTH)
|
101
|
+
|
102
|
+
if data == b"":
|
103
|
+
raise LinkPlayRequestException("No data received from socket")
|
104
|
+
|
105
|
+
return str(repr(data))
|
106
|
+
|
107
|
+
|
108
|
+
async def call_tcpuart_json(
|
109
|
+
reader: asyncio.StreamReader, writer: asyncio.StreamWriter, cmd: str
|
110
|
+
) -> dict[str, str]:
|
111
|
+
"""Get JSON data from TCPUART service."""
|
112
|
+
raw_response: str = await call_tcpuart(reader, writer, cmd)
|
113
|
+
strip_start = raw_response.find("{")
|
114
|
+
strip_end = raw_response.find("}", strip_start) + 1
|
115
|
+
data = raw_response[strip_start:strip_end]
|
116
|
+
return json.loads(data) # type: ignore
|
117
|
+
|
118
|
+
|
70
119
|
def decode_hexstr(hexstr: str) -> str:
|
71
120
|
"""Decode a hex string."""
|
72
121
|
try:
|
@@ -75,6 +124,7 @@ def decode_hexstr(hexstr: str) -> str:
|
|
75
124
|
return hexstr
|
76
125
|
|
77
126
|
|
127
|
+
@deprecated(version="0.0.9", reason="Use async_create_unverified_context instead")
|
78
128
|
def create_unverified_context() -> ssl.SSLContext:
|
79
129
|
"""Creates an unverified SSL context with the default mTLS certificate."""
|
80
130
|
dirs = AppDirs("python-linkplay")
|
@@ -90,16 +140,38 @@ def create_unverified_context() -> ssl.SSLContext:
|
|
90
140
|
return create_ssl_context(path=mtls_certificate_path)
|
91
141
|
|
92
142
|
|
93
|
-
async def async_create_unverified_context(
|
143
|
+
async def async_create_unverified_context(
|
144
|
+
executor: ThreadPoolExecutor | None = None,
|
145
|
+
) -> ssl.SSLContext:
|
94
146
|
"""Asynchronously creates an unverified SSL context with the default mTLS certificate."""
|
95
147
|
async with aiofiles.tempfile.NamedTemporaryFile(
|
96
148
|
"w", encoding="utf-8"
|
97
149
|
) as mtls_certificate:
|
98
150
|
await mtls_certificate.write(MTLS_CERTIFICATE_CONTENTS)
|
99
151
|
await mtls_certificate.flush()
|
100
|
-
|
152
|
+
certfile: str = str(mtls_certificate.name)
|
153
|
+
return await async_create_ssl_context(certfile=certfile, executor=executor)
|
101
154
|
|
102
155
|
|
156
|
+
async def async_create_ssl_context(
|
157
|
+
*, certfile: str, executor: ThreadPoolExecutor | None = None
|
158
|
+
) -> ssl.SSLContext:
|
159
|
+
"""Creates an SSL context from given certificate file."""
|
160
|
+
sslcontext: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
161
|
+
sslcontext.check_hostname = False
|
162
|
+
sslcontext.verify_mode = ssl.CERT_NONE
|
163
|
+
|
164
|
+
loop = asyncio.get_running_loop()
|
165
|
+
await loop.run_in_executor(executor, sslcontext.load_cert_chain, certfile)
|
166
|
+
|
167
|
+
with contextlib.suppress(AttributeError):
|
168
|
+
# This only works for OpenSSL >= 1.0.0
|
169
|
+
sslcontext.options |= ssl.OP_NO_COMPRESSION
|
170
|
+
sslcontext.set_default_verify_paths()
|
171
|
+
return sslcontext
|
172
|
+
|
173
|
+
|
174
|
+
@deprecated(version="0.0.9", reason="Use async_create_ssl_context instead")
|
103
175
|
def create_ssl_context(path: str) -> ssl.SSLContext:
|
104
176
|
"""Creates an SSL context from given certificate file."""
|
105
177
|
sslcontext: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
@@ -113,6 +185,9 @@ def create_ssl_context(path: str) -> ssl.SSLContext:
|
|
113
185
|
return sslcontext
|
114
186
|
|
115
187
|
|
188
|
+
@deprecated(
|
189
|
+
version="0.0.9", reason="Use async_create_unverified_client_session instead"
|
190
|
+
)
|
116
191
|
def create_unverified_client_session() -> ClientSession:
|
117
192
|
"""Creates a ClientSession using the default unverified SSL context"""
|
118
193
|
context: ssl.SSLContext = create_unverified_context()
|
@@ -125,3 +200,27 @@ async def async_create_unverified_client_session() -> ClientSession:
|
|
125
200
|
context: ssl.SSLContext = await async_create_unverified_context()
|
126
201
|
connector: TCPConnector = TCPConnector(family=socket.AF_UNSPEC, ssl=context)
|
127
202
|
return ClientSession(connector=connector)
|
203
|
+
|
204
|
+
|
205
|
+
def fixup_player_properties(
|
206
|
+
properties: dict[PlayerAttribute, str],
|
207
|
+
) -> dict[PlayerAttribute, str]:
|
208
|
+
"""Fixes up PlayerAttribute in a dict."""
|
209
|
+
properties[PlayerAttribute.TITLE] = decode_hexstr(
|
210
|
+
properties.get(PlayerAttribute.TITLE, "")
|
211
|
+
)
|
212
|
+
properties[PlayerAttribute.ARTIST] = decode_hexstr(
|
213
|
+
properties.get(PlayerAttribute.ARTIST, "")
|
214
|
+
)
|
215
|
+
properties[PlayerAttribute.ALBUM] = decode_hexstr(
|
216
|
+
properties.get(PlayerAttribute.ALBUM, "")
|
217
|
+
)
|
218
|
+
|
219
|
+
# Fixup playing status "none" by setting it to "stopped"
|
220
|
+
if (
|
221
|
+
properties.get(PlayerAttribute.PLAYING_STATUS, "")
|
222
|
+
not in PlayingStatus.__members__.values()
|
223
|
+
):
|
224
|
+
properties[PlayerAttribute.PLAYING_STATUS] = PlayingStatus.STOPPED
|
225
|
+
|
226
|
+
return properties
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: python_linkplay
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.10
|
4
4
|
Summary: A Python Library for Seamless LinkPlay Device Control
|
5
5
|
Author: Velleman Group nv
|
6
6
|
License: MIT
|
@@ -34,7 +34,7 @@ A Python Library for Seamless LinkPlay Device Control
|
|
34
34
|
|
35
35
|
## Intro
|
36
36
|
|
37
|
-
Welcome to python-linkplay, a powerful and user-friendly Python library designed to simplify the integration and control of LinkPlay-enabled devices in your projects. LinkPlay technology empowers a wide range of smart audio devices, making them interconnected and easily controllable. With python-
|
37
|
+
Welcome to python-linkplay, a powerful and user-friendly Python library designed to simplify the integration and control of LinkPlay-enabled devices in your projects. LinkPlay technology empowers a wide range of smart audio devices, making them interconnected and easily controllable. With python-linkplay, you can harness this capability and seamlessly manage your LinkPlay devices from within your Python applications.
|
38
38
|
|
39
39
|
## Key features
|
40
40
|
|
@@ -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=op0YgbBDjpD8XP_2_aEIvFkIRz9w0nlrOp56GUt0XxQ,23
|
4
|
+
linkplay/bridge.py,sha256=LXUc1zcRh1Hx1QauhlpA9da5k7f6h3KLfGRA1jAbTPU,11602
|
5
|
+
linkplay/consts.py,sha256=OGEj34YTiEWRBPjIebokDOVKOsa-DpZkCkUpThO8IIc,13068
|
6
|
+
linkplay/controller.py,sha256=IYoXvHh2zhrsRoRG7gwYFoWSIrL5Hl9hR7c2dhGPNX8,2484
|
7
|
+
linkplay/discovery.py,sha256=aEzN_94pKLmHKYIL7DxSW0FYRsaF2ruZe2bwXz0zf5U,4299
|
8
|
+
linkplay/endpoint.py,sha256=aWNiiU6h3gIWiNzcnavfA8IMZLufv9A8Cm5qphRpRvA,2158
|
9
|
+
linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
|
10
|
+
linkplay/utils.py,sha256=WVKdxITDymLCmKGqlD9Ieyb96qZ-QSC9oIe-KGW4IFU,7827
|
11
|
+
python_linkplay-0.0.10.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
|
12
|
+
python_linkplay-0.0.10.dist-info/METADATA,sha256=ZzxS4W64XCLZXbDssg4YNhtbAmDkAgg-_aF_tY6SJYs,2988
|
13
|
+
python_linkplay-0.0.10.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
14
|
+
python_linkplay-0.0.10.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
|
15
|
+
python_linkplay-0.0.10.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=YQY-i8MemQTvHljd9BqOGgeJEu3FrzurE3TEKDwAax4,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=gEFrajQHejetKaVpYWxkZLn7Nh4W3PaQSknDMU2eKIU,4520
|
11
|
-
python_linkplay-0.0.8.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
|
12
|
-
python_linkplay-0.0.8.dist-info/METADATA,sha256=IAO7Ix1g0elKKcDGjYpXlrTx1bDfb9vMnitiAOdgWcA,2987
|
13
|
-
python_linkplay-0.0.8.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
|
14
|
-
python_linkplay-0.0.8.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
|
15
|
-
python_linkplay-0.0.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|