python-linkplay 0.0.0__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.
- python_linkplay-0.0.0/LICENSE +21 -0
- python_linkplay-0.0.0/PKG-INFO +57 -0
- python_linkplay-0.0.0/README.md +34 -0
- python_linkplay-0.0.0/pyproject.toml +28 -0
- python_linkplay-0.0.0/setup.cfg +48 -0
- python_linkplay-0.0.0/setup.py +5 -0
- python_linkplay-0.0.0/src/linkplay/__init__.py +1 -0
- python_linkplay-0.0.0/src/linkplay/__main__.py +14 -0
- python_linkplay-0.0.0/src/linkplay/__version__.py +1 -0
- python_linkplay-0.0.0/src/linkplay/bridge.py +282 -0
- python_linkplay-0.0.0/src/linkplay/consts.py +272 -0
- python_linkplay-0.0.0/src/linkplay/discovery.py +63 -0
- python_linkplay-0.0.0/src/linkplay/exceptions.py +6 -0
- python_linkplay-0.0.0/src/linkplay/utils.py +59 -0
- python_linkplay-0.0.0/src/python_linkplay.egg-info/PKG-INFO +57 -0
- python_linkplay-0.0.0/src/python_linkplay.egg-info/SOURCES.txt +19 -0
- python_linkplay-0.0.0/src/python_linkplay.egg-info/dependency_links.txt +1 -0
- python_linkplay-0.0.0/src/python_linkplay.egg-info/not-zip-safe +1 -0
- python_linkplay-0.0.0/src/python_linkplay.egg-info/requires.txt +13 -0
- python_linkplay-0.0.0/src/python_linkplay.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Velleman Group nv
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,57 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: python_linkplay
|
3
|
+
Version: 0.0.0
|
4
|
+
Summary: A Python Library for Seamless LinkPlay Device Control
|
5
|
+
Author: Velleman Group nv
|
6
|
+
License: MIT
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
8
|
+
Requires-Python: >=3.8
|
9
|
+
Description-Content-Type: text/markdown
|
10
|
+
License-File: LICENSE
|
11
|
+
Requires-Dist: async-timeout==4.0.3
|
12
|
+
Requires-Dist: aiohttp==3.9.1
|
13
|
+
Requires-Dist: async_upnp_client==0.38.0
|
14
|
+
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"
|
23
|
+
|
24
|
+
|
25
|
+
[](https://pypi.org/project/python-linkplay/)
|
26
|
+
|
27
|
+
[](https://github.com/velleman/python-linkplay/actions/workflows/release.yaml)
|
28
|
+
|
29
|
+
# python-linkplay
|
30
|
+
A Python Library for Seamless LinkPlay Device Control
|
31
|
+
|
32
|
+
## Intro
|
33
|
+
|
34
|
+
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-linkpaly, you can harness this capability and seamlessly manage your LinkPlay devices from within your Python applications.
|
35
|
+
|
36
|
+
## Key features
|
37
|
+
|
38
|
+
1. Unified Control: python-linkplay provides a unified interface for controlling various LinkPlay-enabled devices, streamlining the process of interacting with speakers, smart home audio systems, and more.
|
39
|
+
|
40
|
+
2. Device Discovery: Easily discover and connect to LinkPlay devices on your network, ensuring a hassle-free setup and integration into your Python applications.
|
41
|
+
|
42
|
+
3. Playback Management: Take charge of audio playback on LinkPlay devices with functions to play, pause, skip tracks, adjust volume, and more, offering a comprehensive set of controls for a seamless user experience.
|
43
|
+
|
44
|
+
4. Metadata Retrieval: Retrieve essential metadata such as track information, artist details, and album data, enabling you to enhance the user interface and display relevant information in your applications.
|
45
|
+
|
46
|
+
## LinkPlay API documentation
|
47
|
+
|
48
|
+
- https://github.com/n4archive/LinkPlayAPI
|
49
|
+
- https://github.com/nagyrobi/home-assistant-custom-components-linkplay
|
50
|
+
- https://github.com/ramikg/linkplay-cli
|
51
|
+
- https://developer.arylic.com/httpapi/
|
52
|
+
- http://airscope-audio.net/core2/pdf/airscope-module-http.pdf
|
53
|
+
- https://www.wiimhome.com/pdf/HTTP%20API%20for%20WiiM%20Mini.pdf
|
54
|
+
|
55
|
+
## Multiroom
|
56
|
+
|
57
|
+

|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
[](https://pypi.org/project/python-linkplay/)
|
3
|
+
|
4
|
+
[](https://github.com/velleman/python-linkplay/actions/workflows/release.yaml)
|
5
|
+
|
6
|
+
# python-linkplay
|
7
|
+
A Python Library for Seamless LinkPlay Device Control
|
8
|
+
|
9
|
+
## Intro
|
10
|
+
|
11
|
+
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-linkpaly, you can harness this capability and seamlessly manage your LinkPlay devices from within your Python applications.
|
12
|
+
|
13
|
+
## Key features
|
14
|
+
|
15
|
+
1. Unified Control: python-linkplay provides a unified interface for controlling various LinkPlay-enabled devices, streamlining the process of interacting with speakers, smart home audio systems, and more.
|
16
|
+
|
17
|
+
2. Device Discovery: Easily discover and connect to LinkPlay devices on your network, ensuring a hassle-free setup and integration into your Python applications.
|
18
|
+
|
19
|
+
3. Playback Management: Take charge of audio playback on LinkPlay devices with functions to play, pause, skip tracks, adjust volume, and more, offering a comprehensive set of controls for a seamless user experience.
|
20
|
+
|
21
|
+
4. Metadata Retrieval: Retrieve essential metadata such as track information, artist details, and album data, enabling you to enhance the user interface and display relevant information in your applications.
|
22
|
+
|
23
|
+
## LinkPlay API documentation
|
24
|
+
|
25
|
+
- https://github.com/n4archive/LinkPlayAPI
|
26
|
+
- https://github.com/nagyrobi/home-assistant-custom-components-linkplay
|
27
|
+
- https://github.com/ramikg/linkplay-cli
|
28
|
+
- https://developer.arylic.com/httpapi/
|
29
|
+
- http://airscope-audio.net/core2/pdf/airscope-module-http.pdf
|
30
|
+
- https://www.wiimhome.com/pdf/HTTP%20API%20for%20WiiM%20Mini.pdf
|
31
|
+
|
32
|
+
## Multiroom
|
33
|
+
|
34
|
+

|
@@ -0,0 +1,28 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=42.0", "wheel"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[tool.pytest.ini_options]
|
6
|
+
asyncio_mode = "auto"
|
7
|
+
testpaths = [
|
8
|
+
"tests",
|
9
|
+
]
|
10
|
+
|
11
|
+
log_cli = true
|
12
|
+
log_cli_level = "INFO"
|
13
|
+
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
14
|
+
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
15
|
+
|
16
|
+
[tool.mypy]
|
17
|
+
mypy_path = "src"
|
18
|
+
check_untyped_defs = true
|
19
|
+
disallow_any_generics = true
|
20
|
+
ignore_missing_imports = true
|
21
|
+
no_implicit_optional = true
|
22
|
+
show_error_codes = true
|
23
|
+
strict_equality = true
|
24
|
+
warn_redundant_casts = true
|
25
|
+
warn_return_any = true
|
26
|
+
warn_unreachable = true
|
27
|
+
warn_unused_configs = true
|
28
|
+
no_implicit_reexport = true
|
@@ -0,0 +1,48 @@
|
|
1
|
+
[metadata]
|
2
|
+
name = python_linkplay
|
3
|
+
description = A Python Library for Seamless LinkPlay Device Control
|
4
|
+
long_description = file: README.md
|
5
|
+
long_description_content_type = text/markdown
|
6
|
+
author = Velleman Group nv
|
7
|
+
version = attr: linkplay.VERSION
|
8
|
+
license = MIT
|
9
|
+
classifiers =
|
10
|
+
Programming Language :: Python :: 3
|
11
|
+
|
12
|
+
[options]
|
13
|
+
packages = find_namespace:
|
14
|
+
install_requires =
|
15
|
+
async-timeout==4.0.3
|
16
|
+
aiohttp==3.9.1
|
17
|
+
async_upnp_client==0.38.0
|
18
|
+
python_requires = >=3.8
|
19
|
+
package_dir =
|
20
|
+
=src
|
21
|
+
zip_safe = no
|
22
|
+
|
23
|
+
[options.package_data]
|
24
|
+
linkplay = py.typed
|
25
|
+
|
26
|
+
[options.packages.find]
|
27
|
+
where = src
|
28
|
+
|
29
|
+
[options.extras_require]
|
30
|
+
testing =
|
31
|
+
pytest>=7.3.1
|
32
|
+
pytest-cov>=4.1.0
|
33
|
+
pytest-mock>=3.10.0
|
34
|
+
pytest-asyncio>=0.23.3
|
35
|
+
mypy>=1.3.0
|
36
|
+
flake8>=6.0.0
|
37
|
+
tox>=4.6.0
|
38
|
+
typing-extensions>=4.6.3
|
39
|
+
|
40
|
+
[flake8]
|
41
|
+
max-line-length = 160
|
42
|
+
per-file-ignores =
|
43
|
+
*/__init__.py: F401
|
44
|
+
|
45
|
+
[egg_info]
|
46
|
+
tag_build =
|
47
|
+
tag_date = 0
|
48
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
from linkplay.__version__ import __version__ as VERSION
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import asyncio
|
2
|
+
import aiohttp
|
3
|
+
|
4
|
+
from linkplay.discovery import discover_linkplay_bridges, discover_multirooms
|
5
|
+
|
6
|
+
|
7
|
+
async def main():
|
8
|
+
async with aiohttp.ClientSession() as session:
|
9
|
+
bridges = await discover_linkplay_bridges(session)
|
10
|
+
multirooms = await discover_multirooms(bridges)
|
11
|
+
return bridges, multirooms
|
12
|
+
|
13
|
+
if __name__ == "__main__":
|
14
|
+
asyncio.run(main())
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = '0.0.0'
|
@@ -0,0 +1,282 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Dict, List
|
3
|
+
|
4
|
+
from aiohttp import ClientSession
|
5
|
+
|
6
|
+
from linkplay.consts import (
|
7
|
+
ChannelType,
|
8
|
+
LinkPlayCommand,
|
9
|
+
DeviceAttribute,
|
10
|
+
PlayerAttribute,
|
11
|
+
MuteMode,
|
12
|
+
EqualizerMode,
|
13
|
+
LoopMode,
|
14
|
+
PlaybackMode,
|
15
|
+
PLAYBACK_MODE_MAP,
|
16
|
+
PlayingStatus,
|
17
|
+
PlaymodeSupport,
|
18
|
+
SpeakerType
|
19
|
+
)
|
20
|
+
from linkplay.utils import session_call_api_json, session_call_api_ok, decode_hexstr
|
21
|
+
|
22
|
+
|
23
|
+
class LinkPlayDevice():
|
24
|
+
"""Represents a LinkPlay device."""
|
25
|
+
|
26
|
+
bridge: LinkPlayBridge
|
27
|
+
properties: Dict[DeviceAttribute, str] = dict.fromkeys(DeviceAttribute.__members__.values(), "")
|
28
|
+
|
29
|
+
def __init__(self, bridge: LinkPlayBridge):
|
30
|
+
self.bridge = bridge
|
31
|
+
|
32
|
+
async def update_status(self) -> None:
|
33
|
+
"""Update the device status."""
|
34
|
+
self.properties = await self.bridge.json_request(LinkPlayCommand.DEVICE_STATUS) # type: ignore[assignment]
|
35
|
+
|
36
|
+
async def reboot(self):
|
37
|
+
"""Reboot the device."""
|
38
|
+
await self.bridge.request(LinkPlayCommand.REBOOT)
|
39
|
+
|
40
|
+
@property
|
41
|
+
def uuid(self) -> str:
|
42
|
+
"""The UUID of the device."""
|
43
|
+
return self.properties[DeviceAttribute.UUID]
|
44
|
+
|
45
|
+
@property
|
46
|
+
def name(self) -> str:
|
47
|
+
"""The name of the device."""
|
48
|
+
return self.properties[DeviceAttribute.DEVICE_NAME]
|
49
|
+
|
50
|
+
@property
|
51
|
+
def playmode_support(self) -> PlaymodeSupport:
|
52
|
+
"""Returns the player playmode support."""
|
53
|
+
return PlaymodeSupport(int(self.properties[DeviceAttribute.PLAYMODE_SUPPORT], base=16))
|
54
|
+
|
55
|
+
@property
|
56
|
+
def eth(self) -> str:
|
57
|
+
"""Returns the ethernet address."""
|
58
|
+
return self.properties[DeviceAttribute.ETH_DHCP]
|
59
|
+
|
60
|
+
|
61
|
+
class LinkPlayPlayer():
|
62
|
+
"""Represents a LinkPlay player."""
|
63
|
+
|
64
|
+
bridge: LinkPlayBridge
|
65
|
+
properties: Dict[PlayerAttribute, str] = dict.fromkeys(PlayerAttribute.__members__.values(), "")
|
66
|
+
|
67
|
+
def __init__(self, bridge: LinkPlayBridge):
|
68
|
+
self.bridge = bridge
|
69
|
+
|
70
|
+
async def update_status(self):
|
71
|
+
"""Update the player status."""
|
72
|
+
self.properties = await self.bridge.json_request(LinkPlayCommand.PLAYER_STATUS) # type: ignore[assignment]
|
73
|
+
self.properties[PlayerAttribute.TITLE] = decode_hexstr(self.title)
|
74
|
+
self.properties[PlayerAttribute.ARTIST] = decode_hexstr(self.artist)
|
75
|
+
self.properties[PlayerAttribute.ALBUM] = decode_hexstr(self.album)
|
76
|
+
|
77
|
+
async def next(self):
|
78
|
+
"""Play the next song in the playlist."""
|
79
|
+
await self.bridge.request(LinkPlayCommand.NEXT)
|
80
|
+
|
81
|
+
async def previous(self):
|
82
|
+
"""Play the previous song in the playlist."""
|
83
|
+
await self.bridge.request(LinkPlayCommand.PREVIOUS)
|
84
|
+
|
85
|
+
async def play(self, value: str):
|
86
|
+
"""Start playing the selected track."""
|
87
|
+
await self.bridge.request(LinkPlayCommand.PLAY.format(value)) # type: ignore[str-format]
|
88
|
+
|
89
|
+
async def resume(self):
|
90
|
+
"""Resume playing the current track."""
|
91
|
+
await self.bridge.request(LinkPlayCommand.RESUME)
|
92
|
+
|
93
|
+
async def mute(self):
|
94
|
+
"""Mute the player."""
|
95
|
+
await self.bridge.request(LinkPlayCommand.MUTE)
|
96
|
+
self.properties[PlayerAttribute.MUTED] = MuteMode.MUTED
|
97
|
+
|
98
|
+
async def unmute(self):
|
99
|
+
"""Unmute the player."""
|
100
|
+
await self.bridge.request(LinkPlayCommand.UNMUTE)
|
101
|
+
self.properties[PlayerAttribute.MUTED] = MuteMode.UNMUTED
|
102
|
+
|
103
|
+
async def play_playlist(self, index: int):
|
104
|
+
"""Start playing chosen playlist by index number."""
|
105
|
+
await self.bridge.request(LinkPlayCommand.PLAYLIST.format(index)) # type: ignore[str-format]
|
106
|
+
|
107
|
+
async def pause(self):
|
108
|
+
"""Pause the current playing track."""
|
109
|
+
await self.bridge.request(LinkPlayCommand.PAUSE)
|
110
|
+
self.properties[PlayerAttribute.PLAYING_STATUS] = PlayingStatus.PAUSED
|
111
|
+
|
112
|
+
async def toggle(self):
|
113
|
+
"""Start playing if the player is currently not playing. Stops playing if it is."""
|
114
|
+
await self.bridge.request(LinkPlayCommand.TOGGLE)
|
115
|
+
|
116
|
+
async def set_volume(self, value: int):
|
117
|
+
"""Set the player volume."""
|
118
|
+
if not 0 <= value <= 100:
|
119
|
+
raise ValueError("Volume must be between 0 and 100.")
|
120
|
+
|
121
|
+
await self.bridge.request(LinkPlayCommand.VOLUME.format(value)) # type: ignore[str-format]
|
122
|
+
self.properties[PlayerAttribute.VOLUME] = str(value)
|
123
|
+
|
124
|
+
async def set_equalizer_mode(self, mode: EqualizerMode):
|
125
|
+
"""Set the equalizer mode."""
|
126
|
+
await self.bridge.request(LinkPlayCommand.EQUALIZER_MODE.format(mode)) # type: ignore[str-format]
|
127
|
+
self.properties[PlayerAttribute.EQUALIZER_MODE] = mode
|
128
|
+
|
129
|
+
async def set_loop_mode(self, mode: LoopMode):
|
130
|
+
"""Set the loop mode."""
|
131
|
+
await self.bridge.request(LinkPlayCommand.LOOP_MODE.format(mode)) # type: ignore[str-format]
|
132
|
+
self.properties[PlayerAttribute.PLAYLIST_MODE] = mode
|
133
|
+
|
134
|
+
async def set_play_mode(self, mode: PlaybackMode):
|
135
|
+
"""Set the play mode."""
|
136
|
+
await self.bridge.request(LinkPlayCommand.SWITCH_MODE.format(PLAYBACK_MODE_MAP[mode])) # type: ignore[str-format]
|
137
|
+
self.properties[PlayerAttribute.PLAYBACK_MODE] = mode
|
138
|
+
|
139
|
+
@property
|
140
|
+
def muted(self) -> bool:
|
141
|
+
"""Returns if the player is muted."""
|
142
|
+
return self.properties[PlayerAttribute.MUTED] == MuteMode.MUTED
|
143
|
+
|
144
|
+
@property
|
145
|
+
def title(self) -> str:
|
146
|
+
"""Returns if the currently playing title of the track."""
|
147
|
+
return self.properties[PlayerAttribute.TITLE]
|
148
|
+
|
149
|
+
@property
|
150
|
+
def artist(self) -> str:
|
151
|
+
"""Returns if the currently playing artist."""
|
152
|
+
return self.properties[PlayerAttribute.ARTIST]
|
153
|
+
|
154
|
+
@property
|
155
|
+
def album(self) -> str:
|
156
|
+
"""Returns if the currently playing album."""
|
157
|
+
return self.properties[PlayerAttribute.ALBUM]
|
158
|
+
|
159
|
+
@property
|
160
|
+
def volume(self) -> int:
|
161
|
+
"""Returns the player volume, expressed in %."""
|
162
|
+
return int(self.properties[PlayerAttribute.VOLUME])
|
163
|
+
|
164
|
+
@property
|
165
|
+
def current_position(self) -> int:
|
166
|
+
"""Returns the current position of the track in milliseconds."""
|
167
|
+
return int(self.properties[PlayerAttribute.CURRENT_POSITION])
|
168
|
+
|
169
|
+
@property
|
170
|
+
def total_length(self) -> int:
|
171
|
+
"""Returns the total length of the track in milliseconds."""
|
172
|
+
return int(self.properties[PlayerAttribute.TOTAL_LENGTH])
|
173
|
+
|
174
|
+
@property
|
175
|
+
def status(self) -> PlayingStatus:
|
176
|
+
"""Returns the current playing status."""
|
177
|
+
return PlayingStatus(self.properties[PlayerAttribute.PLAYING_STATUS])
|
178
|
+
|
179
|
+
@property
|
180
|
+
def equalizer_mode(self) -> EqualizerMode:
|
181
|
+
"""Returns the current equalizer mode."""
|
182
|
+
return EqualizerMode(self.properties[PlayerAttribute.EQUALIZER_MODE])
|
183
|
+
|
184
|
+
@property
|
185
|
+
def speaker_type(self) -> SpeakerType:
|
186
|
+
"""Returns the current speaker the player is playing on."""
|
187
|
+
return SpeakerType(self.properties[PlayerAttribute.SPEAKER_TYPE])
|
188
|
+
|
189
|
+
@property
|
190
|
+
def channel_type(self) -> ChannelType:
|
191
|
+
"""Returns the channel the player is playing on."""
|
192
|
+
return ChannelType(self.properties[PlayerAttribute.CHANNEL_TYPE])
|
193
|
+
|
194
|
+
@property
|
195
|
+
def playback_mode(self) -> PlaybackMode:
|
196
|
+
"""Returns the channel the player is playing on."""
|
197
|
+
return PlaybackMode(self.properties[PlayerAttribute.PLAYBACK_MODE])
|
198
|
+
|
199
|
+
@property
|
200
|
+
def loop_mode(self) -> LoopMode:
|
201
|
+
"""Returns the current playlist mode."""
|
202
|
+
return LoopMode(self.properties[PlayerAttribute.PLAYLIST_MODE])
|
203
|
+
|
204
|
+
|
205
|
+
class LinkPlayBridge():
|
206
|
+
"""Represents a LinkPlay bridge to control the device and player attached to it."""
|
207
|
+
|
208
|
+
protocol: str
|
209
|
+
ip_address: str
|
210
|
+
session: ClientSession
|
211
|
+
device: LinkPlayDevice
|
212
|
+
player: LinkPlayPlayer
|
213
|
+
|
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
|
218
|
+
self.device = LinkPlayDevice(self)
|
219
|
+
self.player = LinkPlayPlayer(self)
|
220
|
+
|
221
|
+
def __repr__(self) -> str:
|
222
|
+
if self.device.name == "":
|
223
|
+
return self.endpoint
|
224
|
+
|
225
|
+
return self.device.name
|
226
|
+
|
227
|
+
@property
|
228
|
+
def endpoint(self) -> str:
|
229
|
+
"""Returns the current player endpoint."""
|
230
|
+
return f"{self.protocol}://{self.ip_address}"
|
231
|
+
|
232
|
+
async def json_request(self, command: str) -> Dict[str, str]:
|
233
|
+
"""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)
|
235
|
+
|
236
|
+
async def request(self, command: str) -> None:
|
237
|
+
"""Performs a GET request on the given command and verifies the result."""
|
238
|
+
await session_call_api_ok(self.endpoint, self.session, command)
|
239
|
+
|
240
|
+
|
241
|
+
class LinkPlayMultiroom():
|
242
|
+
"""Represents a LinkPlay multiroom group. Contains a leader and a list of followers.
|
243
|
+
The leader is the device that controls the group."""
|
244
|
+
|
245
|
+
leader: LinkPlayBridge
|
246
|
+
followers: List[LinkPlayBridge]
|
247
|
+
|
248
|
+
def __init__(self, leader: LinkPlayBridge, followers: List[LinkPlayBridge]):
|
249
|
+
self.leader = leader
|
250
|
+
self.followers = followers
|
251
|
+
|
252
|
+
async def ungroup(self):
|
253
|
+
"""Ungroups the multiroom group."""
|
254
|
+
await self.leader.request(LinkPlayCommand.MULTIROOM_UNGROUP)
|
255
|
+
self.followers = []
|
256
|
+
|
257
|
+
async def add_follower(self, follower: LinkPlayBridge):
|
258
|
+
"""Adds a follower to the multiroom group."""
|
259
|
+
await follower.request(LinkPlayCommand.MULTIROOM_JOIN.format(self.leader.device.eth)) # type: ignore[str-format]
|
260
|
+
self.followers.append(follower)
|
261
|
+
|
262
|
+
async def remove_follower(self, follower: LinkPlayBridge):
|
263
|
+
"""Removes a follower from the multiroom group."""
|
264
|
+
await self.leader.request(LinkPlayCommand.MULTIROOM_KICK.format(follower.device.eth)) # type: ignore[str-format]
|
265
|
+
self.followers.remove(follower)
|
266
|
+
|
267
|
+
async def set_volume(self, value: int):
|
268
|
+
"""Sets the volume for the multiroom group."""
|
269
|
+
assert 0 < value <= 100
|
270
|
+
str_vol = str(value)
|
271
|
+
await self.leader.request(LinkPlayCommand.MULTIROOM_VOL.format(str_vol)) # type: ignore[str-format]
|
272
|
+
|
273
|
+
for bridge in [self.leader] + self.followers:
|
274
|
+
bridge.player.properties[PlayerAttribute.VOLUME] = str_vol
|
275
|
+
|
276
|
+
async def mute(self):
|
277
|
+
"""Mutes the multiroom group."""
|
278
|
+
await self.leader.request(LinkPlayCommand.MULTIROOM_MUTE)
|
279
|
+
|
280
|
+
async def unmute(self):
|
281
|
+
"""Unmutes the multiroom group."""
|
282
|
+
await self.leader.request(LinkPlayCommand.MULTIROOM_UNMUTE)
|
@@ -0,0 +1,272 @@
|
|
1
|
+
from enum import StrEnum, IntFlag
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
API_ENDPOINT: str = "{}/httpapi.asp?command={}"
|
5
|
+
API_TIMEOUT: int = 2
|
6
|
+
UNKNOWN_TRACK_PLAYING: str = "Unknown"
|
7
|
+
UPNP_DEVICE_TYPE = 'urn:schemas-upnp-org:device:MediaRenderer:1'
|
8
|
+
|
9
|
+
|
10
|
+
class LinkPlayCommand(StrEnum):
|
11
|
+
"""Defines the LinkPlay commands."""
|
12
|
+
DEVICE_STATUS = "getStatus"
|
13
|
+
SYSLOG = "getsyslog"
|
14
|
+
UPDATE_SERVER = "GetUpdateServer"
|
15
|
+
REBOOT = "reboot"
|
16
|
+
PLAYER_STATUS = "getPlayerStatus"
|
17
|
+
NEXT = "setPlayerCmd:next"
|
18
|
+
PREVIOUS = "setPlayerCmd:prev"
|
19
|
+
UNMUTE = "setPlayerCmd:mute:0"
|
20
|
+
MUTE = "setPlayerCmd:mute:1"
|
21
|
+
RESUME = "setPlayerCmd:resume"
|
22
|
+
PLAY = "setPlayerCmd:play:{}"
|
23
|
+
SEEK = "setPlayerCmd:seek:{}"
|
24
|
+
VOLUME = "setPlayerCmd:vol:{}"
|
25
|
+
PLAYLIST = "setPlayerCmd:playlist:uri:{}"
|
26
|
+
PAUSE = "setPlayerCmd:pause"
|
27
|
+
TOGGLE = "setPlayerCmd:onepause"
|
28
|
+
EQUALIZER_MODE = "setPlayerCmd:equalizer:{}"
|
29
|
+
LOOP_MODE = "setPlayerCmd:loopmode:{}"
|
30
|
+
SWITCH_MODE = "setPlayerCmd:switchmode:{}"
|
31
|
+
M3U_PLAYLIST = "setPlayerCmd:m3u:play:{}"
|
32
|
+
MULTIROOM_LIST = "multiroom:getSlaveList"
|
33
|
+
MULTIROOM_UNGROUP = "multiroom:ungroup"
|
34
|
+
MULTIROOM_KICK = "multiroom:SlaveKickout:{}"
|
35
|
+
MULTIROOM_VOL = "setPlayerCmd:slave_vol:{}"
|
36
|
+
MULTIROOM_MUTE = "setPlayerCmd:slave_mute:mute"
|
37
|
+
MULTIROOM_UNMUTE = "setPlayerCmd:slave_mute:unmute"
|
38
|
+
MULTIROOM_JOIN = "ConnectMasterAp:JoinGroupMaster:eth{}:wifi0.0.0.0"
|
39
|
+
|
40
|
+
|
41
|
+
class SpeakerType(StrEnum):
|
42
|
+
"""Defines the speaker type."""
|
43
|
+
MAIN_SPEAKER = "0"
|
44
|
+
SUB_SPEAKER = "1"
|
45
|
+
|
46
|
+
|
47
|
+
class ChannelType(StrEnum):
|
48
|
+
"""Defines the channel type."""
|
49
|
+
STEREO = "0"
|
50
|
+
LEFT_CHANNEL = "1"
|
51
|
+
RIGHT_CHANNEL = "2"
|
52
|
+
|
53
|
+
|
54
|
+
class PlaybackMode(StrEnum):
|
55
|
+
"""Defines the playback mode."""
|
56
|
+
IDLE = "-1"
|
57
|
+
NONE = "0"
|
58
|
+
AIRPLAY = "1"
|
59
|
+
DLNA = "2"
|
60
|
+
QPLAY = "3"
|
61
|
+
NETWORK = "10"
|
62
|
+
WIIMU_LOCAL = "11"
|
63
|
+
WIIMU_STATION = "12"
|
64
|
+
WIIMU_RADIO = "13"
|
65
|
+
WIIMU_SONGLIST = "14"
|
66
|
+
TF_CARD_1 = "16"
|
67
|
+
WIIMU_MAX = "19"
|
68
|
+
API = "20"
|
69
|
+
UDISK = "21"
|
70
|
+
HTTP_MAX = "29"
|
71
|
+
ALARM = "30"
|
72
|
+
SPOTIFY = "31"
|
73
|
+
LINE_IN = "40"
|
74
|
+
BLUETOOTH = "41"
|
75
|
+
EXT_LOCAL = "42"
|
76
|
+
OPTICAL = "43"
|
77
|
+
RCA = "44"
|
78
|
+
CO_AXIAL = "45"
|
79
|
+
FM = "46"
|
80
|
+
LINE_IN_2 = "47"
|
81
|
+
XLR = "48"
|
82
|
+
HDMI = "49"
|
83
|
+
MIRROR = "50"
|
84
|
+
USB_DAC = "51"
|
85
|
+
TF_CARD_2 = "52"
|
86
|
+
TALK = "60"
|
87
|
+
SLAVE = "99"
|
88
|
+
|
89
|
+
|
90
|
+
PLAYBACK_MODE_MAP: Dict[PlaybackMode, str] = {
|
91
|
+
PlaybackMode.IDLE: 'Idle',
|
92
|
+
PlaybackMode.NONE: 'Idle',
|
93
|
+
PlaybackMode.AIRPLAY: 'Airplay',
|
94
|
+
PlaybackMode.DLNA: 'DLNA',
|
95
|
+
PlaybackMode.QPLAY: 'QPlay',
|
96
|
+
PlaybackMode.NETWORK: 'wifi',
|
97
|
+
PlaybackMode.WIIMU_LOCAL: 'udisk',
|
98
|
+
PlaybackMode.TF_CARD_1: 'TFcard',
|
99
|
+
PlaybackMode.API: 'API',
|
100
|
+
PlaybackMode.UDISK: 'udisk',
|
101
|
+
PlaybackMode.ALARM: 'Alarm',
|
102
|
+
PlaybackMode.SPOTIFY: 'Spotify',
|
103
|
+
PlaybackMode.LINE_IN: 'line-in',
|
104
|
+
PlaybackMode.BLUETOOTH: 'bluetooth',
|
105
|
+
PlaybackMode.OPTICAL: 'optical',
|
106
|
+
PlaybackMode.RCA: 'RCA',
|
107
|
+
PlaybackMode.CO_AXIAL: 'co-axial',
|
108
|
+
PlaybackMode.FM: 'FM',
|
109
|
+
PlaybackMode.LINE_IN_2: 'line-in2',
|
110
|
+
PlaybackMode.XLR: 'XLR',
|
111
|
+
PlaybackMode.HDMI: 'HDMI',
|
112
|
+
PlaybackMode.MIRROR: 'cd',
|
113
|
+
PlaybackMode.USB_DAC: 'USB DAC',
|
114
|
+
PlaybackMode.TF_CARD_2: 'TFcard',
|
115
|
+
PlaybackMode.TALK: 'Talk',
|
116
|
+
PlaybackMode.SLAVE: 'Idle'
|
117
|
+
}
|
118
|
+
|
119
|
+
|
120
|
+
class LoopMode(StrEnum):
|
121
|
+
"""Defines the loop mode."""
|
122
|
+
CONTINOUS_PLAY_ONE_SONG = "-1"
|
123
|
+
PLAY_IN_ORDER = "0"
|
124
|
+
CONTINUOUS_PLAYBACK = "1"
|
125
|
+
RANDOM_PLAYBACK = "2"
|
126
|
+
LIST_CYCLE = "3"
|
127
|
+
|
128
|
+
|
129
|
+
class EqualizerMode(StrEnum):
|
130
|
+
"""Defines the equalizer mode."""
|
131
|
+
NONE = "0"
|
132
|
+
CLASSIC = "1"
|
133
|
+
POP = "2"
|
134
|
+
JAZZ = "3"
|
135
|
+
VOCAL = "4"
|
136
|
+
|
137
|
+
|
138
|
+
class PlayingStatus(StrEnum):
|
139
|
+
"""Defines the playing status."""
|
140
|
+
PLAYING = "play"
|
141
|
+
LOADING = "load"
|
142
|
+
STOPPED = "stop"
|
143
|
+
PAUSED = "pause"
|
144
|
+
|
145
|
+
|
146
|
+
class MuteMode(StrEnum):
|
147
|
+
"""Defines the mute mode."""
|
148
|
+
UNMUTED = "0"
|
149
|
+
MUTED = "1"
|
150
|
+
|
151
|
+
|
152
|
+
class PlaymodeSupport(IntFlag):
|
153
|
+
"""Defines which modes the player supports."""
|
154
|
+
LINE_IN = 2
|
155
|
+
BLUETOOTH = 4
|
156
|
+
USB = 8
|
157
|
+
OPTICAL = 16
|
158
|
+
COAXIAL = 64
|
159
|
+
LINE_IN_2 = 256
|
160
|
+
USBDAC = 32768
|
161
|
+
OPTICAL_2 = 262144
|
162
|
+
|
163
|
+
|
164
|
+
class PlayerAttribute(StrEnum):
|
165
|
+
"""Defines the player attributes."""
|
166
|
+
SPEAKER_TYPE = "type"
|
167
|
+
CHANNEL_TYPE = "ch"
|
168
|
+
PLAYBACK_MODE = "mode"
|
169
|
+
PLAYLIST_MODE = "loop"
|
170
|
+
EQUALIZER_MODE = "eq"
|
171
|
+
PLAYING_STATUS = "status"
|
172
|
+
CURRENT_POSITION = "curpos"
|
173
|
+
OFFSET_POSITION = "offset_pts"
|
174
|
+
TOTAL_LENGTH = "totlen"
|
175
|
+
TITLE = "Title"
|
176
|
+
ARTIST = "Artist"
|
177
|
+
ALBUM = "Album"
|
178
|
+
ALARM_FLAG = "alarmflag"
|
179
|
+
PLAYLIST_COUNT = "plicount"
|
180
|
+
PLAYLIST_INDEX = "plicurr"
|
181
|
+
VOLUME = "vol"
|
182
|
+
MUTED = "mute"
|
183
|
+
|
184
|
+
|
185
|
+
class DeviceAttribute(StrEnum):
|
186
|
+
"""Defines the device attributes."""
|
187
|
+
UUID = "uuid"
|
188
|
+
DEVICE_NAME = "DeviceName"
|
189
|
+
GROUP_NAME = "GroupName"
|
190
|
+
SSID = "ssid"
|
191
|
+
LANGUAGE = "language"
|
192
|
+
FIRMWARE = "firmware"
|
193
|
+
HARDWARE = "hardware"
|
194
|
+
BUILD = "build"
|
195
|
+
PROJECT = "project"
|
196
|
+
PRIV_PRJ = "priv_prj"
|
197
|
+
PROJECT_BUILD_NAME = "project_build_name"
|
198
|
+
RELEASE = "Release"
|
199
|
+
TEMP_UUID = "temp_uuid"
|
200
|
+
HIDE_SSID = "hideSSID"
|
201
|
+
SSID_STRATEGY = "SSIDStrategy"
|
202
|
+
BRANCH = "branch"
|
203
|
+
GROUP = "group"
|
204
|
+
WMRM_VERSION = "wmrm_version"
|
205
|
+
INTERNET = "internet"
|
206
|
+
MAC_ADDRESS = "MAC"
|
207
|
+
STA_MAC_ADDRESS = "STA_MAC"
|
208
|
+
COUNTRY_CODE = "CountryCode"
|
209
|
+
COUNTRY_REGION = "CountryRegion"
|
210
|
+
NET_STAT = "netstat"
|
211
|
+
ESSID = "essid"
|
212
|
+
APCLI0 = "apcli0"
|
213
|
+
ETH2 = "eth2"
|
214
|
+
RA0 = "ra0"
|
215
|
+
ETH_DHCP = "eth_dhcp"
|
216
|
+
VERSION_UPDATE = "VersionUpdate"
|
217
|
+
NEW_VER = "NewVer"
|
218
|
+
SET_DNS_ENABLE = "set_dns_enable"
|
219
|
+
MCU_VER = "mcu_ver"
|
220
|
+
MCU_VER_NEW = "mcu_ver_new"
|
221
|
+
DSP_VER = "dsp_ver"
|
222
|
+
DSP_VER_NEW = "dsp_ver_new"
|
223
|
+
DATE = "date"
|
224
|
+
TIME = "time"
|
225
|
+
TIMEZONE = "tz"
|
226
|
+
DST_ENABLE = "dst_enable"
|
227
|
+
REGION = "region"
|
228
|
+
PROMPT_STATUS = "prompt_status"
|
229
|
+
IOT_VER = "iot_ver"
|
230
|
+
UPNP_VERSION = "upnp_version"
|
231
|
+
CAP1 = "cap1"
|
232
|
+
CAPABILITY = "capability"
|
233
|
+
LANGUAGES = "languages"
|
234
|
+
STREAMS_ALL = "streams_all"
|
235
|
+
STREAMS = "streams"
|
236
|
+
EXTERNAL = "external"
|
237
|
+
PLAYMODE_SUPPORT = "plm_support"
|
238
|
+
PRESET_KEY = "preset_key"
|
239
|
+
SPOTIFY_ACTIVE = "spotify_active"
|
240
|
+
LBC_SUPPORT = "lbc_support"
|
241
|
+
PRIVACY_MODE = "privacy_mode"
|
242
|
+
WIFI_CHANNEL = "WifiChannel"
|
243
|
+
RSSI = "RSSI"
|
244
|
+
BSSID = "BSSID"
|
245
|
+
BATTERY = "battery"
|
246
|
+
BATTERY_PERCENT = "battery_percent"
|
247
|
+
SECURE_MODE = "securemode"
|
248
|
+
AUTH = "auth"
|
249
|
+
ENCRYPTION = "encry"
|
250
|
+
UPNP_UUID = "upnp_uuid"
|
251
|
+
UART_PASS_PORT = "uart_pass_port"
|
252
|
+
COMMUNICATION_PORT = "communication_port"
|
253
|
+
WEB_FIRMWARE_UPDATE_HIDE = "web_firmware_update_hide"
|
254
|
+
IGNORE_TALKSTART = "ignore_talkstart"
|
255
|
+
WEB_LOGIN_RESULT = "web_login_result"
|
256
|
+
SILENCE_OTA_TIME = "silenceOTATime"
|
257
|
+
IGNORE_SILENCE_OTA_TIME = "ignore_silenceOTATime"
|
258
|
+
NEW_TUNEIN_PRESET_AND_ALARM = "new_tunein_preset_and_alarm"
|
259
|
+
IHEARTRADIO_NEW = "iheartradio_new"
|
260
|
+
NEW_IHEART_PODCAST = "new_iheart_podcast"
|
261
|
+
TIDAL_VERSION = "tidal_version"
|
262
|
+
SERVICE_VERSION = "service_version"
|
263
|
+
ETH_MAC_ADDRESS = "ETH_MAC"
|
264
|
+
SECURITY = "security"
|
265
|
+
SECURITY_VERSION = "security_version"
|
266
|
+
|
267
|
+
|
268
|
+
class MultiroomAttribute(StrEnum):
|
269
|
+
"""Defines the player attributes."""
|
270
|
+
NUM_FOLLOWERS = "slaves"
|
271
|
+
FOLLOWER_LIST = "slave_list"
|
272
|
+
UUID = "uuid"
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from typing import Any, Dict, List
|
2
|
+
|
3
|
+
from aiohttp import ClientSession
|
4
|
+
from async_upnp_client.search import async_search
|
5
|
+
from async_upnp_client.utils import CaseInsensitiveDict
|
6
|
+
|
7
|
+
from linkplay.consts import UPNP_DEVICE_TYPE, LinkPlayCommand, MultiroomAttribute
|
8
|
+
from linkplay.bridge import LinkPlayBridge, LinkPlayMultiroom
|
9
|
+
from linkplay.exceptions import LinkPlayRequestException
|
10
|
+
|
11
|
+
|
12
|
+
async def linkplay_factory_bridge(ip_address: str, session: ClientSession) -> LinkPlayBridge | None:
|
13
|
+
"""Attempts to create a LinkPlayBridge from the given IP address.
|
14
|
+
Returns None if the device is not an expected LinkPlay device."""
|
15
|
+
bridge = LinkPlayBridge("http", ip_address, session)
|
16
|
+
try:
|
17
|
+
await bridge.device.update_status()
|
18
|
+
await bridge.player.update_status()
|
19
|
+
except LinkPlayRequestException:
|
20
|
+
return None
|
21
|
+
return bridge
|
22
|
+
|
23
|
+
|
24
|
+
async def discover_linkplay_bridges(session: ClientSession) -> List[LinkPlayBridge]:
|
25
|
+
"""Attempts to discover LinkPlay devices on the local network."""
|
26
|
+
devices: List[LinkPlayBridge] = []
|
27
|
+
|
28
|
+
async def add_linkplay_device_to_list(upnp_device: CaseInsensitiveDict):
|
29
|
+
ip_address: str | None = upnp_device.get('_host')
|
30
|
+
|
31
|
+
if not ip_address:
|
32
|
+
return
|
33
|
+
|
34
|
+
if bridge := await linkplay_factory_bridge(ip_address, session):
|
35
|
+
devices.append(bridge)
|
36
|
+
|
37
|
+
await async_search(
|
38
|
+
search_target=UPNP_DEVICE_TYPE,
|
39
|
+
async_callback=add_linkplay_device_to_list
|
40
|
+
)
|
41
|
+
|
42
|
+
return devices
|
43
|
+
|
44
|
+
|
45
|
+
async def discover_multirooms(bridges: List[LinkPlayBridge]) -> List[LinkPlayMultiroom]:
|
46
|
+
"""Discovers multirooms through the list of provided bridges."""
|
47
|
+
multirooms: List[LinkPlayMultiroom] = []
|
48
|
+
|
49
|
+
for bridge in bridges:
|
50
|
+
properties: Dict[Any, Any] = await bridge.json_request(LinkPlayCommand.MULTIROOM_LIST)
|
51
|
+
|
52
|
+
if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
|
53
|
+
continue
|
54
|
+
|
55
|
+
followers: List[LinkPlayBridge] = []
|
56
|
+
for follower in properties[MultiroomAttribute.FOLLOWER_LIST]:
|
57
|
+
follower_uuid = follower[MultiroomAttribute.UUID]
|
58
|
+
if follower_bridge := next((b for b in bridges if b.device.uuid == follower_uuid), None):
|
59
|
+
followers.append(follower_bridge)
|
60
|
+
|
61
|
+
multirooms.append(LinkPlayMultiroom(bridge, followers))
|
62
|
+
|
63
|
+
return multirooms
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Dict
|
3
|
+
import json
|
4
|
+
from http import HTTPStatus
|
5
|
+
|
6
|
+
import async_timeout
|
7
|
+
from aiohttp import ClientSession, ClientError
|
8
|
+
|
9
|
+
from linkplay.consts import API_ENDPOINT, API_TIMEOUT
|
10
|
+
from linkplay.exceptions import LinkPlayRequestException
|
11
|
+
|
12
|
+
|
13
|
+
async def session_call_api(endpoint: str, session: ClientSession, command: str):
|
14
|
+
"""Calls the LinkPlay API and returns the result as a string.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
endpoint (str): The endpoint to use.
|
18
|
+
session (ClientSession): The client session to use.
|
19
|
+
command (str): The command to use.
|
20
|
+
|
21
|
+
Raises:
|
22
|
+
LinkPlayRequestException: Thrown when the request fails or an invalid response is received.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
str: The response of the API call.
|
26
|
+
"""
|
27
|
+
url = API_ENDPOINT.format(endpoint, command)
|
28
|
+
|
29
|
+
try:
|
30
|
+
async with async_timeout.timeout(API_TIMEOUT):
|
31
|
+
response = await session.get(url, ssl=False)
|
32
|
+
|
33
|
+
except (asyncio.TimeoutError, ClientError, asyncio.CancelledError) as error:
|
34
|
+
raise LinkPlayRequestException(f"Error requesting data from '{url}'") from error
|
35
|
+
|
36
|
+
if response.status != HTTPStatus.OK:
|
37
|
+
raise LinkPlayRequestException(f"Unexpected HTTPStatus {response.status} received from '{url}'")
|
38
|
+
|
39
|
+
return await response.text()
|
40
|
+
|
41
|
+
|
42
|
+
async def session_call_api_json(endpoint: str, session: ClientSession,
|
43
|
+
command: str) -> Dict[str, str]:
|
44
|
+
"""Calls the LinkPlay API and returns the result as a JSON object."""
|
45
|
+
result = await session_call_api(endpoint, session, command)
|
46
|
+
return json.loads(result) # type: ignore
|
47
|
+
|
48
|
+
|
49
|
+
async def session_call_api_ok(endpoint: str, session: ClientSession, command: str):
|
50
|
+
"""Calls the LinkPlay API and checks if the response is OK. Throws exception if not."""
|
51
|
+
result = await session_call_api(endpoint, session, command)
|
52
|
+
|
53
|
+
if result != "OK":
|
54
|
+
raise LinkPlayRequestException(f"Didn't receive expected OK from {endpoint}")
|
55
|
+
|
56
|
+
|
57
|
+
def decode_hexstr(hexstr: str) -> str:
|
58
|
+
"""Decode a hex string."""
|
59
|
+
return bytes.fromhex(hexstr).decode("utf-8")
|
@@ -0,0 +1,57 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: python_linkplay
|
3
|
+
Version: 0.0.0
|
4
|
+
Summary: A Python Library for Seamless LinkPlay Device Control
|
5
|
+
Author: Velleman Group nv
|
6
|
+
License: MIT
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
8
|
+
Requires-Python: >=3.8
|
9
|
+
Description-Content-Type: text/markdown
|
10
|
+
License-File: LICENSE
|
11
|
+
Requires-Dist: async-timeout==4.0.3
|
12
|
+
Requires-Dist: aiohttp==3.9.1
|
13
|
+
Requires-Dist: async_upnp_client==0.38.0
|
14
|
+
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"
|
23
|
+
|
24
|
+
|
25
|
+
[](https://pypi.org/project/python-linkplay/)
|
26
|
+
|
27
|
+
[](https://github.com/velleman/python-linkplay/actions/workflows/release.yaml)
|
28
|
+
|
29
|
+
# python-linkplay
|
30
|
+
A Python Library for Seamless LinkPlay Device Control
|
31
|
+
|
32
|
+
## Intro
|
33
|
+
|
34
|
+
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-linkpaly, you can harness this capability and seamlessly manage your LinkPlay devices from within your Python applications.
|
35
|
+
|
36
|
+
## Key features
|
37
|
+
|
38
|
+
1. Unified Control: python-linkplay provides a unified interface for controlling various LinkPlay-enabled devices, streamlining the process of interacting with speakers, smart home audio systems, and more.
|
39
|
+
|
40
|
+
2. Device Discovery: Easily discover and connect to LinkPlay devices on your network, ensuring a hassle-free setup and integration into your Python applications.
|
41
|
+
|
42
|
+
3. Playback Management: Take charge of audio playback on LinkPlay devices with functions to play, pause, skip tracks, adjust volume, and more, offering a comprehensive set of controls for a seamless user experience.
|
43
|
+
|
44
|
+
4. Metadata Retrieval: Retrieve essential metadata such as track information, artist details, and album data, enabling you to enhance the user interface and display relevant information in your applications.
|
45
|
+
|
46
|
+
## LinkPlay API documentation
|
47
|
+
|
48
|
+
- https://github.com/n4archive/LinkPlayAPI
|
49
|
+
- https://github.com/nagyrobi/home-assistant-custom-components-linkplay
|
50
|
+
- https://github.com/ramikg/linkplay-cli
|
51
|
+
- https://developer.arylic.com/httpapi/
|
52
|
+
- http://airscope-audio.net/core2/pdf/airscope-module-http.pdf
|
53
|
+
- https://www.wiimhome.com/pdf/HTTP%20API%20for%20WiiM%20Mini.pdf
|
54
|
+
|
55
|
+
## Multiroom
|
56
|
+
|
57
|
+

|
@@ -0,0 +1,19 @@
|
|
1
|
+
LICENSE
|
2
|
+
README.md
|
3
|
+
pyproject.toml
|
4
|
+
setup.cfg
|
5
|
+
setup.py
|
6
|
+
src/linkplay/__init__.py
|
7
|
+
src/linkplay/__main__.py
|
8
|
+
src/linkplay/__version__.py
|
9
|
+
src/linkplay/bridge.py
|
10
|
+
src/linkplay/consts.py
|
11
|
+
src/linkplay/discovery.py
|
12
|
+
src/linkplay/exceptions.py
|
13
|
+
src/linkplay/utils.py
|
14
|
+
src/python_linkplay.egg-info/PKG-INFO
|
15
|
+
src/python_linkplay.egg-info/SOURCES.txt
|
16
|
+
src/python_linkplay.egg-info/dependency_links.txt
|
17
|
+
src/python_linkplay.egg-info/not-zip-safe
|
18
|
+
src/python_linkplay.egg-info/requires.txt
|
19
|
+
src/python_linkplay.egg-info/top_level.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
linkplay
|