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.
Files changed (28) hide show
  1. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/PKG-INFO +29 -3
  2. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/README.md +28 -2
  3. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/__init__.py +10 -1
  4. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/client.py +178 -11
  5. ma_http_client-0.2.0/ma_http_client/version.py +1 -0
  6. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/PKG-INFO +29 -3
  7. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/SOURCES.txt +4 -0
  8. ma_http_client-0.2.0/test/test_client_browse.py +52 -0
  9. ma_http_client-0.2.0/test/test_client_catalog.py +124 -0
  10. ma_http_client-0.2.0/test/test_client_genres.py +87 -0
  11. ma_http_client-0.2.0/test/test_client_playlists.py +93 -0
  12. ma_http_client-0.1.2/ma_http_client/version.py +0 -1
  13. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/LICENSE +0 -0
  14. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/claude_tools.py +0 -0
  15. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/cli.py +0 -0
  16. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/debug.py +0 -0
  17. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/install_skill.py +0 -0
  18. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client/skills/music-assistant/SKILL.md +0 -0
  19. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/dependency_links.txt +0 -0
  20. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/entry_points.txt +0 -0
  21. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/requires.txt +0 -0
  22. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/ma_http_client.egg-info/top_level.txt +0 -0
  23. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/pyproject.toml +0 -0
  24. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/setup.cfg +0 -0
  25. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/test/test_claude_tools.py +0 -0
  26. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/test/test_cli.py +0 -0
  27. {ma_http_client-0.1.2 → ma_http_client-0.2.0}/test/test_client.py +0 -0
  28. {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.1.2
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
- # Play media on a player
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
- # Play media on a player
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 SimpleHTTPMusicAssistantClient, debug_method
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.1.2
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
- # Play media on a player
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