ma-http-client 0.1.2__tar.gz → 0.2.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.
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/PKG-INFO +29 -3
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/README.md +28 -2
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/__init__.py +10 -1
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/client.py +178 -11
- ma_http_client-0.2.0/ma_http_client/version.py +1 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/PKG-INFO +29 -3
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/SOURCES.txt +4 -0
- ma_http_client-0.2.0/test/test_client_browse.py +52 -0
- ma_http_client-0.2.0/test/test_client_catalog.py +124 -0
- ma_http_client-0.2.0/test/test_client_genres.py +87 -0
- ma_http_client-0.2.0/test/test_client_playlists.py +93 -0
- ma_http_client-0.1.2/ma_http_client/version.py +0 -1
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/LICENSE +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/claude_tools.py +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/cli.py +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/debug.py +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/install_skill.py +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/skills/music-assistant/SKILL.md +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/dependency_links.txt +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/entry_points.txt +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/requires.txt +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/top_level.txt +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/pyproject.toml +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/setup.cfg +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/test/test_claude_tools.py +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/test/test_cli.py +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/test/test_client.py +0 -0
- {ma_http_client-0.1.2 → ma_http_client-0.2.0}/test/test_debug_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ma-http-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A simple HTTP client for the Music Assistant API
|
|
5
5
|
Author-email: Mike Gray/Oscillate Labs <mike@oscillatelabs.net>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -50,12 +50,38 @@ players = client.get_players()
|
|
|
50
50
|
# Search for media
|
|
51
51
|
results = client.search_media("Radiohead", limit=5)
|
|
52
52
|
|
|
53
|
-
#
|
|
53
|
+
# Resolve one item by name; the server matches across types, or narrow it
|
|
54
|
+
album = client.get_item_by_name("Abbey Road", artist="The Beatles")
|
|
55
|
+
track = client.get_track_by_name("Come Together", artist_name="The Beatles")
|
|
56
|
+
|
|
57
|
+
# Play media on a player (a uri or a list of uris; shuffle is optional)
|
|
54
58
|
client.play_media(queue_id=players[0].player_id, media="library://artist/204")
|
|
59
|
+
client.play_media(queue_id=players[0].player_id, media=album["uri"], shuffle=True)
|
|
55
60
|
|
|
56
|
-
# Queue controls
|
|
61
|
+
# Queue controls and modes
|
|
57
62
|
client.queue_command_pause(players[0].player_id)
|
|
58
63
|
client.queue_command_next(players[0].player_id)
|
|
64
|
+
client.queue_set_shuffle(players[0].player_id, True)
|
|
65
|
+
client.queue_set_repeat(players[0].player_id, RepeatMode.ALL) # from music_assistant_models.enums
|
|
66
|
+
|
|
67
|
+
# Favorites
|
|
68
|
+
client.add_favorite(track["uri"])
|
|
69
|
+
|
|
70
|
+
# Library listings and genres ("play some jazz", "play some music")
|
|
71
|
+
from ma_http_client import SORT_RANDOM
|
|
72
|
+
jazz = client.search_genres("jazz")[0]
|
|
73
|
+
tracks = client.genre_tracks(jazz["item_id"], limit=30, sort_field=SORT_RANDOM)
|
|
74
|
+
random_tracks = client.library_items(MediaType.TRACK, limit=20, sort_field=SORT_RANDOM)
|
|
75
|
+
client.play_media(players[0].player_id, [t["uri"] for t in tracks], shuffle=True)
|
|
76
|
+
|
|
77
|
+
# Playlists
|
|
78
|
+
workout = client.library_playlists(search="workout") or [client.create_playlist("Workout")]
|
|
79
|
+
client.add_playlist_tracks(workout[0]["item_id"], [track["uri"]])
|
|
80
|
+
client.save_queue_as_playlist(players[0].player_id, "Tonight")
|
|
81
|
+
|
|
82
|
+
# Browse provider folders, e.g. radio stations by country, language, or tag
|
|
83
|
+
stations = client.browse("radiobrowser://category/tag/jazz", limit=10)
|
|
84
|
+
client.play_media(players[0].player_id, [s["uri"] for s in stations])
|
|
59
85
|
|
|
60
86
|
# Volume
|
|
61
87
|
client.player_command_volume_set(players[0].player_id, 50)
|
|
@@ -26,12 +26,38 @@ players = client.get_players()
|
|
|
26
26
|
# Search for media
|
|
27
27
|
results = client.search_media("Radiohead", limit=5)
|
|
28
28
|
|
|
29
|
-
#
|
|
29
|
+
# Resolve one item by name; the server matches across types, or narrow it
|
|
30
|
+
album = client.get_item_by_name("Abbey Road", artist="The Beatles")
|
|
31
|
+
track = client.get_track_by_name("Come Together", artist_name="The Beatles")
|
|
32
|
+
|
|
33
|
+
# Play media on a player (a uri or a list of uris; shuffle is optional)
|
|
30
34
|
client.play_media(queue_id=players[0].player_id, media="library://artist/204")
|
|
35
|
+
client.play_media(queue_id=players[0].player_id, media=album["uri"], shuffle=True)
|
|
31
36
|
|
|
32
|
-
# Queue controls
|
|
37
|
+
# Queue controls and modes
|
|
33
38
|
client.queue_command_pause(players[0].player_id)
|
|
34
39
|
client.queue_command_next(players[0].player_id)
|
|
40
|
+
client.queue_set_shuffle(players[0].player_id, True)
|
|
41
|
+
client.queue_set_repeat(players[0].player_id, RepeatMode.ALL) # from music_assistant_models.enums
|
|
42
|
+
|
|
43
|
+
# Favorites
|
|
44
|
+
client.add_favorite(track["uri"])
|
|
45
|
+
|
|
46
|
+
# Library listings and genres ("play some jazz", "play some music")
|
|
47
|
+
from ma_http_client import SORT_RANDOM
|
|
48
|
+
jazz = client.search_genres("jazz")[0]
|
|
49
|
+
tracks = client.genre_tracks(jazz["item_id"], limit=30, sort_field=SORT_RANDOM)
|
|
50
|
+
random_tracks = client.library_items(MediaType.TRACK, limit=20, sort_field=SORT_RANDOM)
|
|
51
|
+
client.play_media(players[0].player_id, [t["uri"] for t in tracks], shuffle=True)
|
|
52
|
+
|
|
53
|
+
# Playlists
|
|
54
|
+
workout = client.library_playlists(search="workout") or [client.create_playlist("Workout")]
|
|
55
|
+
client.add_playlist_tracks(workout[0]["item_id"], [track["uri"]])
|
|
56
|
+
client.save_queue_as_playlist(players[0].player_id, "Tonight")
|
|
57
|
+
|
|
58
|
+
# Browse provider folders, e.g. radio stations by country, language, or tag
|
|
59
|
+
stations = client.browse("radiobrowser://category/tag/jazz", limit=10)
|
|
60
|
+
client.play_media(players[0].player_id, [s["uri"] for s in stations])
|
|
35
61
|
|
|
36
62
|
# Volume
|
|
37
63
|
client.player_command_volume_set(players[0].player_id, 50)
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
from ma_http_client.client import
|
|
1
|
+
from ma_http_client.client import (
|
|
2
|
+
SORT_NAME,
|
|
3
|
+
SORT_RANDOM,
|
|
4
|
+
SORT_RANDOM_PLAY_COUNT,
|
|
5
|
+
SimpleHTTPMusicAssistantClient,
|
|
6
|
+
debug_method,
|
|
7
|
+
)
|
|
2
8
|
from ma_http_client.debug import DebugMusicAssistantClient
|
|
3
9
|
from ma_http_client.version import __version__
|
|
4
10
|
|
|
5
11
|
|
|
6
12
|
__all__ = [
|
|
7
13
|
"__version__",
|
|
14
|
+
"SORT_NAME",
|
|
15
|
+
"SORT_RANDOM",
|
|
16
|
+
"SORT_RANDOM_PLAY_COUNT",
|
|
8
17
|
"SimpleHTTPMusicAssistantClient",
|
|
9
18
|
"DebugMusicAssistantClient",
|
|
10
19
|
"debug_method",
|
|
@@ -5,11 +5,17 @@ import uuid
|
|
|
5
5
|
from typing import Any, Dict, List, Optional
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
|
-
from music_assistant_models.enums import MediaType, QueueOption
|
|
8
|
+
from music_assistant_models.enums import MediaType, QueueOption, RepeatMode
|
|
9
9
|
from music_assistant_models.errors import MusicAssistantError
|
|
10
10
|
from music_assistant_models.player import Player
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
# Server SortField values accepted by library listings (music_assistant_models.enums.SortField)
|
|
14
|
+
SORT_RANDOM = "random"
|
|
15
|
+
SORT_RANDOM_PLAY_COUNT = "random_play_count"
|
|
16
|
+
SORT_NAME = "sort_name"
|
|
17
|
+
|
|
18
|
+
|
|
13
19
|
def debug_method(func):
|
|
14
20
|
"""Decorator to log method inputs and outputs for debugging purposes."""
|
|
15
21
|
|
|
@@ -116,25 +122,178 @@ class SimpleHTTPMusicAssistantClient:
|
|
|
116
122
|
"""Search for media."""
|
|
117
123
|
return self.send_command("music/recently_played_items")
|
|
118
124
|
|
|
125
|
+
def get_item_by_name(
|
|
126
|
+
self,
|
|
127
|
+
name: str,
|
|
128
|
+
artist: Optional[str] = None,
|
|
129
|
+
album: Optional[str] = None,
|
|
130
|
+
media_type: Optional[MediaType] = None,
|
|
131
|
+
) -> Optional[Dict[str, Any]]:
|
|
132
|
+
"""Resolve one media item by name, letting the server do the matching.
|
|
133
|
+
|
|
134
|
+
Without ``media_type`` the server tries every type, which suits a bare
|
|
135
|
+
"play X" request. Returns the item dict or ``None`` when nothing matched.
|
|
136
|
+
"""
|
|
137
|
+
args: Dict[str, Any] = {"name": name}
|
|
138
|
+
if artist:
|
|
139
|
+
args["artist"] = artist
|
|
140
|
+
if album:
|
|
141
|
+
args["album"] = album
|
|
142
|
+
if media_type:
|
|
143
|
+
args["media_type"] = media_type.value
|
|
144
|
+
return self.send_command("music/item_by_name", **args)
|
|
145
|
+
|
|
146
|
+
def get_track_by_name(
|
|
147
|
+
self,
|
|
148
|
+
track_name: str,
|
|
149
|
+
artist_name: Optional[str] = None,
|
|
150
|
+
album_name: Optional[str] = None,
|
|
151
|
+
) -> Optional[Dict[str, Any]]:
|
|
152
|
+
"""Resolve a track by title, with optional artist and album filters.
|
|
153
|
+
|
|
154
|
+
The server matches title and version, then artist and album when given,
|
|
155
|
+
and falls back to a looser album match before giving up. Returns the
|
|
156
|
+
track dict or ``None``.
|
|
157
|
+
"""
|
|
158
|
+
args: Dict[str, Any] = {"track_name": track_name}
|
|
159
|
+
if artist_name:
|
|
160
|
+
args["artist_name"] = artist_name
|
|
161
|
+
if album_name:
|
|
162
|
+
args["album_name"] = album_name
|
|
163
|
+
return self.send_command("music/track_by_name", **args)
|
|
164
|
+
|
|
165
|
+
def library_items(
|
|
166
|
+
self,
|
|
167
|
+
media_type: MediaType,
|
|
168
|
+
search: Optional[str] = None,
|
|
169
|
+
limit: int = 25,
|
|
170
|
+
favorite: Optional[bool] = None,
|
|
171
|
+
sort_field: Optional[str] = None,
|
|
172
|
+
genre_ids: Optional[List[int]] = None,
|
|
173
|
+
) -> List[Dict[str, Any]]:
|
|
174
|
+
"""List library items of one type, with optional search, favorite, sort and genre filters.
|
|
175
|
+
|
|
176
|
+
``sort_field`` takes the server's SortField values; ``SORT_RANDOM`` gives a
|
|
177
|
+
random sample, which is how "play some music" picks tracks.
|
|
178
|
+
"""
|
|
179
|
+
args: Dict[str, Any] = {"limit": limit}
|
|
180
|
+
if search:
|
|
181
|
+
args["search"] = search
|
|
182
|
+
if favorite is not None:
|
|
183
|
+
args["favorite"] = favorite
|
|
184
|
+
if sort_field:
|
|
185
|
+
args["sort_field"] = sort_field
|
|
186
|
+
if genre_ids:
|
|
187
|
+
args["genre"] = genre_ids
|
|
188
|
+
return self.send_command(f"music/{media_type.value}s/library_items", **args)
|
|
189
|
+
|
|
190
|
+
def search_genres(self, name: str, limit: int = 5) -> List[Dict[str, Any]]:
|
|
191
|
+
"""Find library genres whose name or alias matches ``name`` (server-side, alias aware)."""
|
|
192
|
+
return self.send_command("music/genres/library_items", search=name, limit=limit)
|
|
193
|
+
|
|
194
|
+
def genre_tracks(self, genre_id: str | int, limit: int = 50, sort_field: Optional[str] = None):
|
|
195
|
+
"""List tracks mapped to a library genre."""
|
|
196
|
+
args: Dict[str, Any] = {"item_id": genre_id, "limit": limit}
|
|
197
|
+
if sort_field:
|
|
198
|
+
args["sort_field"] = sort_field
|
|
199
|
+
return self.send_command("music/genres/tracks", **args)
|
|
200
|
+
|
|
201
|
+
def genre_albums(self, genre_id: str | int, limit: int = 50, sort_field: Optional[str] = None):
|
|
202
|
+
"""List albums mapped to a library genre."""
|
|
203
|
+
args: Dict[str, Any] = {"item_id": genre_id, "limit": limit}
|
|
204
|
+
if sort_field:
|
|
205
|
+
args["sort_field"] = sort_field
|
|
206
|
+
return self.send_command("music/genres/albums", **args)
|
|
207
|
+
|
|
208
|
+
# Playlists
|
|
209
|
+
def library_playlists(self, search: Optional[str] = None, limit: int = 25) -> List[Dict[str, Any]]:
|
|
210
|
+
"""List library playlists, optionally filtered by a name search."""
|
|
211
|
+
return self.library_items(MediaType.PLAYLIST, search=search, limit=limit)
|
|
212
|
+
|
|
213
|
+
def create_playlist(self, name: str, provider: Optional[str] = None) -> Dict[str, Any]:
|
|
214
|
+
"""Create a playlist and return it. ``provider`` defaults to the server's builtin provider."""
|
|
215
|
+
args: Dict[str, Any] = {"name": name}
|
|
216
|
+
if provider:
|
|
217
|
+
args["provider_instance_or_domain"] = provider
|
|
218
|
+
return self.send_command("music/playlists/create_playlist", **args)
|
|
219
|
+
|
|
220
|
+
def playlist_tracks(
|
|
221
|
+
self, playlist_id: str | int, provider: str = "library", limit: int = 500
|
|
222
|
+
) -> List[Dict[str, Any]]:
|
|
223
|
+
"""List the tracks of a playlist in order; positions are the list indexes."""
|
|
224
|
+
return self.send_command(
|
|
225
|
+
"music/playlists/playlist_tracks",
|
|
226
|
+
item_id=str(playlist_id),
|
|
227
|
+
provider_instance_id_or_domain=provider,
|
|
228
|
+
limit=limit,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def add_playlist_tracks(self, playlist_id: str | int, uris: List[str]):
|
|
232
|
+
"""Append tracks (by uri) to a library playlist."""
|
|
233
|
+
return self.send_command("music/playlists/add_playlist_tracks", db_playlist_id=playlist_id, uris=uris)
|
|
234
|
+
|
|
235
|
+
def remove_playlist_tracks(self, playlist_id: str | int, positions: List[int]):
|
|
236
|
+
"""Remove tracks from a library playlist by their positions (see playlist_tracks)."""
|
|
237
|
+
return self.send_command(
|
|
238
|
+
"music/playlists/remove_playlist_tracks", db_playlist_id=playlist_id, positions_to_remove=positions
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def save_queue_as_playlist(self, queue_id: str, name: str):
|
|
242
|
+
"""Save the current queue of a player as a new library playlist."""
|
|
243
|
+
return self.send_command("player_queues/save_as_playlist", queue_id=queue_id, name=name)
|
|
244
|
+
|
|
245
|
+
# Browse (provider folders, e.g. radio-browser categories)
|
|
246
|
+
def browse(self, path: Optional[str] = None, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
|
|
247
|
+
"""Browse a provider path; ``None`` lists the top-level provider folders.
|
|
248
|
+
|
|
249
|
+
Radio by country, language, or genre tag uses the radio-browser provider's
|
|
250
|
+
folders, for example ``radiobrowser://category/country/ES``,
|
|
251
|
+
``radiobrowser://category/language/spanish``, ``radiobrowser://category/tag/jazz``
|
|
252
|
+
and ``radiobrowser://popularity/popular``. Folder listings return items with
|
|
253
|
+
``media_type`` ``folder``; station listings return ``radio`` items.
|
|
254
|
+
"""
|
|
255
|
+
args: Dict[str, Any] = {"limit": limit, "offset": offset}
|
|
256
|
+
if path:
|
|
257
|
+
args["path"] = path
|
|
258
|
+
return self.send_command("music/browse", **args)
|
|
259
|
+
|
|
260
|
+
def add_favorite(self, uri: str):
|
|
261
|
+
"""Mark the item at ``uri`` as a favorite (adds it to the library if needed)."""
|
|
262
|
+
return self.send_command("music/favorites/add_item", item=uri)
|
|
263
|
+
|
|
264
|
+
def remove_favorite(self, media_type: MediaType, library_item_id: str | int):
|
|
265
|
+
"""Remove a library item from favorites."""
|
|
266
|
+
return self.send_command(
|
|
267
|
+
"music/favorites/remove_item", media_type=media_type.value, library_item_id=library_item_id
|
|
268
|
+
)
|
|
269
|
+
|
|
119
270
|
def play_media(
|
|
120
271
|
self,
|
|
121
272
|
queue_id: str,
|
|
122
|
-
media: str,
|
|
273
|
+
media: str | List[str],
|
|
123
274
|
option: QueueOption = QueueOption.PLAY, # type: ignore
|
|
124
275
|
radio_mode: bool = False,
|
|
276
|
+
shuffle: Optional[bool] = None,
|
|
125
277
|
):
|
|
126
|
-
"""Play media on a player queue.
|
|
278
|
+
"""Play media on a player queue.
|
|
279
|
+
|
|
280
|
+
``media`` is a uri or a list of uris. ``shuffle`` is only sent when set,
|
|
281
|
+
so older servers that lack the argument keep working; it applies to the
|
|
282
|
+
PLAY and REPLACE options.
|
|
283
|
+
"""
|
|
127
284
|
self.log.info(
|
|
128
285
|
f"Sending play_media: queue_id={queue_id}, media={media}, option={option.value}, "
|
|
129
|
-
f"radio_mode={radio_mode}"
|
|
130
|
-
)
|
|
131
|
-
return self.send_command(
|
|
132
|
-
command="player_queues/play_media",
|
|
133
|
-
queue_id=queue_id,
|
|
134
|
-
media=media,
|
|
135
|
-
option=option.value,
|
|
136
|
-
radio_mode=radio_mode,
|
|
286
|
+
f"radio_mode={radio_mode}, shuffle={shuffle}"
|
|
137
287
|
)
|
|
288
|
+
args: Dict[str, Any] = {
|
|
289
|
+
"queue_id": queue_id,
|
|
290
|
+
"media": media,
|
|
291
|
+
"option": option.value,
|
|
292
|
+
"radio_mode": radio_mode,
|
|
293
|
+
}
|
|
294
|
+
if shuffle is not None:
|
|
295
|
+
args["shuffle"] = shuffle
|
|
296
|
+
return self.send_command("player_queues/play_media", **args)
|
|
138
297
|
|
|
139
298
|
def queue_command_play(self, queue_id: str):
|
|
140
299
|
"""Send PLAY command to given queue."""
|
|
@@ -152,6 +311,14 @@ class SimpleHTTPMusicAssistantClient:
|
|
|
152
311
|
"""Go to previous track."""
|
|
153
312
|
return self.send_command("player_queues/previous", queue_id=queue_id)
|
|
154
313
|
|
|
314
|
+
def queue_set_shuffle(self, queue_id: str, enabled: bool):
|
|
315
|
+
"""Turn shuffle on or off for a queue."""
|
|
316
|
+
return self.send_command("player_queues/shuffle", queue_id=queue_id, shuffle_enabled=enabled)
|
|
317
|
+
|
|
318
|
+
def queue_set_repeat(self, queue_id: str, mode: RepeatMode):
|
|
319
|
+
"""Set the repeat mode (off, one, all) for a queue."""
|
|
320
|
+
return self.send_command("player_queues/repeat", queue_id=queue_id, repeat_mode=mode.value)
|
|
321
|
+
|
|
155
322
|
def player_command_power_on(self, player_id: str):
|
|
156
323
|
"""Power on a player."""
|
|
157
324
|
return self.send_command("players/player_command_power_on", player_id=player_id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ma-http-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A simple HTTP client for the Music Assistant API
|
|
5
5
|
Author-email: Mike Gray/Oscillate Labs <mike@oscillatelabs.net>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -50,12 +50,38 @@ players = client.get_players()
|
|
|
50
50
|
# Search for media
|
|
51
51
|
results = client.search_media("Radiohead", limit=5)
|
|
52
52
|
|
|
53
|
-
#
|
|
53
|
+
# Resolve one item by name; the server matches across types, or narrow it
|
|
54
|
+
album = client.get_item_by_name("Abbey Road", artist="The Beatles")
|
|
55
|
+
track = client.get_track_by_name("Come Together", artist_name="The Beatles")
|
|
56
|
+
|
|
57
|
+
# Play media on a player (a uri or a list of uris; shuffle is optional)
|
|
54
58
|
client.play_media(queue_id=players[0].player_id, media="library://artist/204")
|
|
59
|
+
client.play_media(queue_id=players[0].player_id, media=album["uri"], shuffle=True)
|
|
55
60
|
|
|
56
|
-
# Queue controls
|
|
61
|
+
# Queue controls and modes
|
|
57
62
|
client.queue_command_pause(players[0].player_id)
|
|
58
63
|
client.queue_command_next(players[0].player_id)
|
|
64
|
+
client.queue_set_shuffle(players[0].player_id, True)
|
|
65
|
+
client.queue_set_repeat(players[0].player_id, RepeatMode.ALL) # from music_assistant_models.enums
|
|
66
|
+
|
|
67
|
+
# Favorites
|
|
68
|
+
client.add_favorite(track["uri"])
|
|
69
|
+
|
|
70
|
+
# Library listings and genres ("play some jazz", "play some music")
|
|
71
|
+
from ma_http_client import SORT_RANDOM
|
|
72
|
+
jazz = client.search_genres("jazz")[0]
|
|
73
|
+
tracks = client.genre_tracks(jazz["item_id"], limit=30, sort_field=SORT_RANDOM)
|
|
74
|
+
random_tracks = client.library_items(MediaType.TRACK, limit=20, sort_field=SORT_RANDOM)
|
|
75
|
+
client.play_media(players[0].player_id, [t["uri"] for t in tracks], shuffle=True)
|
|
76
|
+
|
|
77
|
+
# Playlists
|
|
78
|
+
workout = client.library_playlists(search="workout") or [client.create_playlist("Workout")]
|
|
79
|
+
client.add_playlist_tracks(workout[0]["item_id"], [track["uri"]])
|
|
80
|
+
client.save_queue_as_playlist(players[0].player_id, "Tonight")
|
|
81
|
+
|
|
82
|
+
# Browse provider folders, e.g. radio stations by country, language, or tag
|
|
83
|
+
stations = client.browse("radiobrowser://category/tag/jazz", limit=10)
|
|
84
|
+
client.play_media(players[0].player_id, [s["uri"] for s in stations])
|
|
59
85
|
|
|
60
86
|
# Volume
|
|
61
87
|
client.player_command_volume_set(players[0].player_id, 50)
|
|
@@ -18,4 +18,8 @@ ma_http_client/skills/music-assistant/SKILL.md
|
|
|
18
18
|
test/test_claude_tools.py
|
|
19
19
|
test/test_cli.py
|
|
20
20
|
test/test_client.py
|
|
21
|
+
test/test_client_browse.py
|
|
22
|
+
test/test_client_catalog.py
|
|
23
|
+
test/test_client_genres.py
|
|
24
|
+
test/test_client_playlists.py
|
|
21
25
|
test/test_debug_client.py
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Tests for the browse method."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from ma_http_client import SimpleHTTPMusicAssistantClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_session():
|
|
13
|
+
return Mock(spec=requests.Session)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def client(mock_session):
|
|
18
|
+
return SimpleHTTPMusicAssistantClient(server_url="http://test-server:8095", session=mock_session)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _respond(mock_session, payload):
|
|
22
|
+
response = Mock()
|
|
23
|
+
response.status_code = 200
|
|
24
|
+
response.json.return_value = payload
|
|
25
|
+
mock_session.post.return_value = response
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sent(mock_session):
|
|
29
|
+
return mock_session.post.call_args[1]["json"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_browse_root_omits_path(client, mock_session):
|
|
33
|
+
_respond(mock_session, [{"media_type": "folder", "path": "radiobrowser://"}])
|
|
34
|
+
|
|
35
|
+
result = client.browse()
|
|
36
|
+
|
|
37
|
+
sent = _sent(mock_session)
|
|
38
|
+
assert sent["command"] == "music/browse"
|
|
39
|
+
assert sent["args"] == {"limit": 100, "offset": 0}
|
|
40
|
+
assert result[0]["media_type"] == "folder"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_browse_radio_country(client, mock_session):
|
|
44
|
+
_respond(mock_session, [])
|
|
45
|
+
|
|
46
|
+
client.browse("radiobrowser://category/country/ES", limit=20, offset=40)
|
|
47
|
+
|
|
48
|
+
assert _sent(mock_session)["args"] == {
|
|
49
|
+
"limit": 20,
|
|
50
|
+
"offset": 40,
|
|
51
|
+
"path": "radiobrowser://category/country/ES",
|
|
52
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Tests for the by-name lookup, favorites, and queue mode methods."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import requests
|
|
7
|
+
from music_assistant_models.enums import MediaType, QueueOption, RepeatMode
|
|
8
|
+
|
|
9
|
+
from ma_http_client import SimpleHTTPMusicAssistantClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_session():
|
|
14
|
+
return Mock(spec=requests.Session)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client(mock_session):
|
|
19
|
+
return SimpleHTTPMusicAssistantClient(server_url="http://test-server:8095", session=mock_session)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _respond(mock_session, payload):
|
|
23
|
+
response = Mock()
|
|
24
|
+
response.status_code = 200
|
|
25
|
+
response.json.return_value = payload
|
|
26
|
+
mock_session.post.return_value = response
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _sent(mock_session):
|
|
30
|
+
return mock_session.post.call_args[1]["json"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestItemByName:
|
|
34
|
+
def test_bare_name_sends_only_name(self, client, mock_session):
|
|
35
|
+
_respond(mock_session, {"name": "Abbey Road", "media_type": "album"})
|
|
36
|
+
|
|
37
|
+
result = client.get_item_by_name("Abbey Road")
|
|
38
|
+
|
|
39
|
+
sent = _sent(mock_session)
|
|
40
|
+
assert sent["command"] == "music/item_by_name"
|
|
41
|
+
assert sent["args"] == {"name": "Abbey Road"}
|
|
42
|
+
assert result["name"] == "Abbey Road"
|
|
43
|
+
|
|
44
|
+
def test_filters_are_sent_when_given(self, client, mock_session):
|
|
45
|
+
_respond(mock_session, None)
|
|
46
|
+
|
|
47
|
+
result = client.get_item_by_name(
|
|
48
|
+
"Come Together", artist="The Beatles", album="Abbey Road", media_type=MediaType.TRACK
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert _sent(mock_session)["args"] == {
|
|
52
|
+
"name": "Come Together",
|
|
53
|
+
"artist": "The Beatles",
|
|
54
|
+
"album": "Abbey Road",
|
|
55
|
+
"media_type": "track",
|
|
56
|
+
}
|
|
57
|
+
assert result is None
|
|
58
|
+
|
|
59
|
+
def test_track_by_name(self, client, mock_session):
|
|
60
|
+
_respond(mock_session, {"name": "Come Together"})
|
|
61
|
+
|
|
62
|
+
client.get_track_by_name("Come Together", artist_name="The Beatles")
|
|
63
|
+
|
|
64
|
+
sent = _sent(mock_session)
|
|
65
|
+
assert sent["command"] == "music/track_by_name"
|
|
66
|
+
assert sent["args"] == {"track_name": "Come Together", "artist_name": "The Beatles"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestFavorites:
|
|
70
|
+
def test_add_favorite_sends_uri(self, client, mock_session):
|
|
71
|
+
_respond(mock_session, None)
|
|
72
|
+
|
|
73
|
+
client.add_favorite("library://track/42")
|
|
74
|
+
|
|
75
|
+
sent = _sent(mock_session)
|
|
76
|
+
assert sent["command"] == "music/favorites/add_item"
|
|
77
|
+
assert sent["args"] == {"item": "library://track/42"}
|
|
78
|
+
|
|
79
|
+
def test_remove_favorite_sends_type_and_id(self, client, mock_session):
|
|
80
|
+
_respond(mock_session, None)
|
|
81
|
+
|
|
82
|
+
client.remove_favorite(MediaType.TRACK, 42)
|
|
83
|
+
|
|
84
|
+
sent = _sent(mock_session)
|
|
85
|
+
assert sent["command"] == "music/favorites/remove_item"
|
|
86
|
+
assert sent["args"] == {"media_type": "track", "library_item_id": 42}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestQueueModes:
|
|
90
|
+
def test_set_shuffle(self, client, mock_session):
|
|
91
|
+
_respond(mock_session, None)
|
|
92
|
+
|
|
93
|
+
client.queue_set_shuffle("queue-1", True)
|
|
94
|
+
|
|
95
|
+
sent = _sent(mock_session)
|
|
96
|
+
assert sent["command"] == "player_queues/shuffle"
|
|
97
|
+
assert sent["args"] == {"queue_id": "queue-1", "shuffle_enabled": True}
|
|
98
|
+
|
|
99
|
+
def test_set_repeat(self, client, mock_session):
|
|
100
|
+
_respond(mock_session, None)
|
|
101
|
+
|
|
102
|
+
client.queue_set_repeat("queue-1", RepeatMode.ALL)
|
|
103
|
+
|
|
104
|
+
sent = _sent(mock_session)
|
|
105
|
+
assert sent["command"] == "player_queues/repeat"
|
|
106
|
+
assert sent["args"] == {"queue_id": "queue-1", "repeat_mode": "all"}
|
|
107
|
+
|
|
108
|
+
def test_play_media_omits_shuffle_by_default(self, client, mock_session):
|
|
109
|
+
_respond(mock_session, None)
|
|
110
|
+
|
|
111
|
+
client.play_media("queue-1", "library://artist/1")
|
|
112
|
+
|
|
113
|
+
args = _sent(mock_session)["args"]
|
|
114
|
+
assert "shuffle" not in args
|
|
115
|
+
assert args["option"] == QueueOption.PLAY.value
|
|
116
|
+
|
|
117
|
+
def test_play_media_sends_shuffle_and_uri_list(self, client, mock_session):
|
|
118
|
+
_respond(mock_session, None)
|
|
119
|
+
|
|
120
|
+
client.play_media("queue-1", ["library://track/1", "library://track/2"], shuffle=True)
|
|
121
|
+
|
|
122
|
+
args = _sent(mock_session)["args"]
|
|
123
|
+
assert args["media"] == ["library://track/1", "library://track/2"]
|
|
124
|
+
assert args["shuffle"] is True
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tests for library listing and genre methods."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import requests
|
|
7
|
+
from music_assistant_models.enums import MediaType
|
|
8
|
+
|
|
9
|
+
from ma_http_client import SORT_RANDOM, SimpleHTTPMusicAssistantClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_session():
|
|
14
|
+
return Mock(spec=requests.Session)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client(mock_session):
|
|
19
|
+
return SimpleHTTPMusicAssistantClient(server_url="http://test-server:8095", session=mock_session)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _respond(mock_session, payload):
|
|
23
|
+
response = Mock()
|
|
24
|
+
response.status_code = 200
|
|
25
|
+
response.json.return_value = payload
|
|
26
|
+
mock_session.post.return_value = response
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _sent(mock_session):
|
|
30
|
+
return mock_session.post.call_args[1]["json"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestLibraryItems:
|
|
34
|
+
def test_random_tracks(self, client, mock_session):
|
|
35
|
+
_respond(mock_session, [])
|
|
36
|
+
|
|
37
|
+
client.library_items(MediaType.TRACK, limit=20, sort_field=SORT_RANDOM)
|
|
38
|
+
|
|
39
|
+
sent = _sent(mock_session)
|
|
40
|
+
assert sent["command"] == "music/tracks/library_items"
|
|
41
|
+
assert sent["args"] == {"limit": 20, "sort_field": "random"}
|
|
42
|
+
|
|
43
|
+
def test_favorite_albums_with_search(self, client, mock_session):
|
|
44
|
+
_respond(mock_session, [])
|
|
45
|
+
|
|
46
|
+
client.library_items(MediaType.ALBUM, search="abbey", favorite=True, genre_ids=[3, 4])
|
|
47
|
+
|
|
48
|
+
sent = _sent(mock_session)
|
|
49
|
+
assert sent["command"] == "music/albums/library_items"
|
|
50
|
+
assert sent["args"] == {"limit": 25, "search": "abbey", "favorite": True, "genre": [3, 4]}
|
|
51
|
+
|
|
52
|
+
def test_favorite_false_is_sent(self, client, mock_session):
|
|
53
|
+
_respond(mock_session, [])
|
|
54
|
+
|
|
55
|
+
client.library_items(MediaType.TRACK, favorite=False)
|
|
56
|
+
|
|
57
|
+
assert _sent(mock_session)["args"]["favorite"] is False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestGenres:
|
|
61
|
+
def test_search_genres(self, client, mock_session):
|
|
62
|
+
_respond(mock_session, [{"name": "Jazz", "item_id": "7"}])
|
|
63
|
+
|
|
64
|
+
result = client.search_genres("jazz")
|
|
65
|
+
|
|
66
|
+
sent = _sent(mock_session)
|
|
67
|
+
assert sent["command"] == "music/genres/library_items"
|
|
68
|
+
assert sent["args"] == {"search": "jazz", "limit": 5}
|
|
69
|
+
assert result[0]["name"] == "Jazz"
|
|
70
|
+
|
|
71
|
+
def test_genre_tracks_random(self, client, mock_session):
|
|
72
|
+
_respond(mock_session, [])
|
|
73
|
+
|
|
74
|
+
client.genre_tracks("7", limit=30, sort_field=SORT_RANDOM)
|
|
75
|
+
|
|
76
|
+
sent = _sent(mock_session)
|
|
77
|
+
assert sent["command"] == "music/genres/tracks"
|
|
78
|
+
assert sent["args"] == {"item_id": "7", "limit": 30, "sort_field": "random"}
|
|
79
|
+
|
|
80
|
+
def test_genre_albums_default(self, client, mock_session):
|
|
81
|
+
_respond(mock_session, [])
|
|
82
|
+
|
|
83
|
+
client.genre_albums(7)
|
|
84
|
+
|
|
85
|
+
sent = _sent(mock_session)
|
|
86
|
+
assert sent["command"] == "music/genres/albums"
|
|
87
|
+
assert sent["args"] == {"item_id": 7, "limit": 50}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Tests for the playlist methods."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from ma_http_client import SimpleHTTPMusicAssistantClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_session():
|
|
13
|
+
return Mock(spec=requests.Session)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def client(mock_session):
|
|
18
|
+
return SimpleHTTPMusicAssistantClient(server_url="http://test-server:8095", session=mock_session)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _respond(mock_session, payload):
|
|
22
|
+
response = Mock()
|
|
23
|
+
response.status_code = 200
|
|
24
|
+
response.json.return_value = payload
|
|
25
|
+
mock_session.post.return_value = response
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sent(mock_session):
|
|
29
|
+
return mock_session.post.call_args[1]["json"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_library_playlists_search(client, mock_session):
|
|
33
|
+
_respond(mock_session, [{"name": "Workout"}])
|
|
34
|
+
|
|
35
|
+
result = client.library_playlists(search="workout")
|
|
36
|
+
|
|
37
|
+
sent = _sent(mock_session)
|
|
38
|
+
assert sent["command"] == "music/playlists/library_items"
|
|
39
|
+
assert sent["args"] == {"limit": 25, "search": "workout"}
|
|
40
|
+
assert result[0]["name"] == "Workout"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_create_playlist_default_provider(client, mock_session):
|
|
44
|
+
_respond(mock_session, {"item_id": "12", "name": "Workout"})
|
|
45
|
+
|
|
46
|
+
result = client.create_playlist("Workout")
|
|
47
|
+
|
|
48
|
+
sent = _sent(mock_session)
|
|
49
|
+
assert sent["command"] == "music/playlists/create_playlist"
|
|
50
|
+
assert sent["args"] == {"name": "Workout"}
|
|
51
|
+
assert result["item_id"] == "12"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_create_playlist_with_provider(client, mock_session):
|
|
55
|
+
_respond(mock_session, {})
|
|
56
|
+
|
|
57
|
+
client.create_playlist("Workout", provider="spotify")
|
|
58
|
+
|
|
59
|
+
assert _sent(mock_session)["args"]["provider_instance_or_domain"] == "spotify"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_playlist_tracks(client, mock_session):
|
|
63
|
+
_respond(mock_session, [])
|
|
64
|
+
|
|
65
|
+
client.playlist_tracks(12, limit=10)
|
|
66
|
+
|
|
67
|
+
sent = _sent(mock_session)
|
|
68
|
+
assert sent["command"] == "music/playlists/playlist_tracks"
|
|
69
|
+
assert sent["args"] == {"item_id": "12", "provider_instance_id_or_domain": "library", "limit": 10}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_add_and_remove_playlist_tracks(client, mock_session):
|
|
73
|
+
_respond(mock_session, None)
|
|
74
|
+
|
|
75
|
+
client.add_playlist_tracks(12, ["library://track/1"])
|
|
76
|
+
sent = _sent(mock_session)
|
|
77
|
+
assert sent["command"] == "music/playlists/add_playlist_tracks"
|
|
78
|
+
assert sent["args"] == {"db_playlist_id": 12, "uris": ["library://track/1"]}
|
|
79
|
+
|
|
80
|
+
client.remove_playlist_tracks(12, [0, 3])
|
|
81
|
+
sent = _sent(mock_session)
|
|
82
|
+
assert sent["command"] == "music/playlists/remove_playlist_tracks"
|
|
83
|
+
assert sent["args"] == {"db_playlist_id": 12, "positions_to_remove": [0, 3]}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_save_queue_as_playlist(client, mock_session):
|
|
87
|
+
_respond(mock_session, None)
|
|
88
|
+
|
|
89
|
+
client.save_queue_as_playlist("queue-1", "Tonight")
|
|
90
|
+
|
|
91
|
+
sent = _sent(mock_session)
|
|
92
|
+
assert sent["command"] == "player_queues/save_as_playlist"
|
|
93
|
+
assert sent["args"] == {"queue_id": "queue-1", "name": "Tonight"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/skills/music-assistant/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|