spotifyify 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spotifyify/__init__.py +51 -0
- spotifyify/credentials.py +13 -0
- spotifyify/device_resolver.py +14 -0
- spotifyify/mcp/cli.py +5 -0
- spotifyify/mcp/server.py +483 -0
- spotifyify/schemas.py +214 -0
- spotifyify/service.py +532 -0
- spotifyify/views.py +29 -0
- spotifyify-0.1.0.dist-info/METADATA +222 -0
- spotifyify-0.1.0.dist-info/RECORD +13 -0
- spotifyify-0.1.0.dist-info/WHEEL +5 -0
- spotifyify-0.1.0.dist-info/entry_points.txt +2 -0
- spotifyify-0.1.0.dist-info/top_level.txt +1 -0
spotifyify/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from .service import AsyncSpotify
|
|
2
|
+
from .schemas import (
|
|
3
|
+
DevicesResponse,
|
|
4
|
+
PlaybackState,
|
|
5
|
+
RecentlyPlayedResponse,
|
|
6
|
+
SearchResponse,
|
|
7
|
+
SavedTracksResponse,
|
|
8
|
+
PlaylistsResponse,
|
|
9
|
+
Playlist,
|
|
10
|
+
ShowsSearchResult,
|
|
11
|
+
EpisodesResponse,
|
|
12
|
+
TopTracksResponse,
|
|
13
|
+
TopArtistsResponse,
|
|
14
|
+
Artist,
|
|
15
|
+
AlbumDetails,
|
|
16
|
+
AlbumTracksResponse,
|
|
17
|
+
QueueResponse,
|
|
18
|
+
SavedShowsResponse,
|
|
19
|
+
SavedAlbumsResponse,
|
|
20
|
+
NewReleasesResponse,
|
|
21
|
+
CurrentlyPlayingResponse,
|
|
22
|
+
)
|
|
23
|
+
from .credentials import SpotifyCredentials
|
|
24
|
+
|
|
25
|
+
from .views import SpotifyScope, ActionSuccessResponse
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AsyncSpotify",
|
|
29
|
+
"SpotifyCredentials",
|
|
30
|
+
"DevicesResponse",
|
|
31
|
+
"PlaybackState",
|
|
32
|
+
"RecentlyPlayedResponse",
|
|
33
|
+
"SearchResponse",
|
|
34
|
+
"SavedTracksResponse",
|
|
35
|
+
"PlaylistsResponse",
|
|
36
|
+
"Playlist",
|
|
37
|
+
"ShowsSearchResult",
|
|
38
|
+
"EpisodesResponse",
|
|
39
|
+
"TopTracksResponse",
|
|
40
|
+
"TopArtistsResponse",
|
|
41
|
+
"Artist",
|
|
42
|
+
"AlbumDetails",
|
|
43
|
+
"AlbumTracksResponse",
|
|
44
|
+
"QueueResponse",
|
|
45
|
+
"SavedShowsResponse",
|
|
46
|
+
"SavedAlbumsResponse",
|
|
47
|
+
"NewReleasesResponse",
|
|
48
|
+
"CurrentlyPlayingResponse",
|
|
49
|
+
"SpotifyScope",
|
|
50
|
+
"ActionSuccessResponse",
|
|
51
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SpotifyCredentials(BaseSettings):
|
|
5
|
+
spotify_client_id: str
|
|
6
|
+
spotify_client_secret: str
|
|
7
|
+
spotify_redirect_uri: str = "http://127.0.0.1:8080"
|
|
8
|
+
|
|
9
|
+
model_config = {
|
|
10
|
+
"env_file": ".env",
|
|
11
|
+
"env_file_encoding": "utf-8",
|
|
12
|
+
"extra": "ignore",
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class DeviceResolver:
|
|
2
|
+
def __init__(self):
|
|
3
|
+
self._device_map: dict[str, str] = {}
|
|
4
|
+
|
|
5
|
+
def set_device(self, name: str, device_id: str) -> None:
|
|
6
|
+
self._device_map[name.lower()] = device_id
|
|
7
|
+
|
|
8
|
+
def resolve(self, device_name: str | None) -> str | None:
|
|
9
|
+
if not device_name:
|
|
10
|
+
return None
|
|
11
|
+
return self._device_map.get(device_name.lower())
|
|
12
|
+
|
|
13
|
+
def invalidate(self) -> None:
|
|
14
|
+
self._device_map.clear()
|
spotifyify/mcp/cli.py
ADDED
spotifyify/mcp/server.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
|
|
6
|
+
from spotifyify import AsyncSpotify, SpotifyScope
|
|
7
|
+
from spotifyify.credentials import SpotifyCredentials
|
|
8
|
+
from spotifyify.device_resolver import DeviceResolver
|
|
9
|
+
from spotifyify.schemas import (
|
|
10
|
+
AlbumDetails,
|
|
11
|
+
Artist,
|
|
12
|
+
CurrentlyPlayingResponse,
|
|
13
|
+
DevicesResponse,
|
|
14
|
+
Episode,
|
|
15
|
+
NewReleasesResponse,
|
|
16
|
+
PlaybackState,
|
|
17
|
+
Playlist,
|
|
18
|
+
PlaylistsResponse,
|
|
19
|
+
QueueResponse,
|
|
20
|
+
RecentlyPlayedResponse,
|
|
21
|
+
SavedAlbumsResponse,
|
|
22
|
+
SimplifiedAlbum,
|
|
23
|
+
Track,
|
|
24
|
+
)
|
|
25
|
+
from spotifyify.views import ActionSuccessResponse
|
|
26
|
+
|
|
27
|
+
_SPOTIFY_SCOPES = [
|
|
28
|
+
SpotifyScope.USER_READ_PLAYBACK_STATE,
|
|
29
|
+
SpotifyScope.USER_MODIFY_PLAYBACK_STATE,
|
|
30
|
+
SpotifyScope.USER_LIBRARY_READ,
|
|
31
|
+
SpotifyScope.USER_LIBRARY_MODIFY,
|
|
32
|
+
SpotifyScope.USER_TOP_READ,
|
|
33
|
+
SpotifyScope.USER_READ_RECENTLY_PLAYED,
|
|
34
|
+
SpotifyScope.PLAYLIST_MODIFY_PUBLIC,
|
|
35
|
+
SpotifyScope.PLAYLIST_MODIFY_PRIVATE,
|
|
36
|
+
SpotifyScope.PLAYLIST_READ_PRIVATE,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
_spotify_client: AsyncSpotify | None = None
|
|
40
|
+
_device_resolver: DeviceResolver | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@asynccontextmanager
|
|
44
|
+
async def lifespan(mcp: FastMCP) -> AsyncIterator[None]:
|
|
45
|
+
global _spotify_client, _device_resolver
|
|
46
|
+
|
|
47
|
+
_spotify_client = AsyncSpotify(
|
|
48
|
+
credentials=SpotifyCredentials(),
|
|
49
|
+
scopes=_SPOTIFY_SCOPES,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_device_resolver = DeviceResolver()
|
|
53
|
+
devices = await _spotify_client.devices()
|
|
54
|
+
for device in devices.devices:
|
|
55
|
+
_device_resolver.set_device(device.name, device.id)
|
|
56
|
+
|
|
57
|
+
yield
|
|
58
|
+
|
|
59
|
+
if _device_resolver:
|
|
60
|
+
_device_resolver.invalidate()
|
|
61
|
+
_device_resolver = None
|
|
62
|
+
_spotify_client = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
mcp = FastMCP(name="Spotify MCP Server", lifespan=lifespan)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
async def get_current_playback(market: str | None = None) -> PlaybackState | None:
|
|
70
|
+
return await _spotify_client.current_playback(market=market)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@mcp.tool(
|
|
74
|
+
description="Get available devices and update the device resolver cache. - use this when u cant find a device."
|
|
75
|
+
)
|
|
76
|
+
async def get_devices() -> DevicesResponse:
|
|
77
|
+
devices = await _spotify_client.devices()
|
|
78
|
+
for device in devices.devices:
|
|
79
|
+
_device_resolver.set_device(device.name, device.id)
|
|
80
|
+
return devices
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp.tool(
|
|
84
|
+
description="""Start or resume playback on a device.
|
|
85
|
+
IMPORTANT: If Spotify is paused or inactive, first call transfer_playback to activate the device,
|
|
86
|
+
then start_playback. Without an active device the API will return a 'No active device' error."""
|
|
87
|
+
)
|
|
88
|
+
async def start_playback(
|
|
89
|
+
device_name: str | None = None,
|
|
90
|
+
context_uri: str | None = None,
|
|
91
|
+
uris: list[str] | None = None,
|
|
92
|
+
) -> ActionSuccessResponse:
|
|
93
|
+
device_id = _device_resolver.resolve(device_name)
|
|
94
|
+
await _spotify_client.start_playback(
|
|
95
|
+
device_id=device_id,
|
|
96
|
+
context_uri=context_uri,
|
|
97
|
+
uris=uris,
|
|
98
|
+
)
|
|
99
|
+
return ActionSuccessResponse(message="Playback started")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@mcp.tool()
|
|
103
|
+
async def pause_playback(device_name: str | None = None) -> ActionSuccessResponse:
|
|
104
|
+
device_id = _device_resolver.resolve(device_name)
|
|
105
|
+
await _spotify_client.pause_playback(device_id=device_id)
|
|
106
|
+
return ActionSuccessResponse(message="Playback paused")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
async def add_to_queue(
|
|
111
|
+
uri: str, device_name: str | None = None
|
|
112
|
+
) -> ActionSuccessResponse:
|
|
113
|
+
device_id = _device_resolver.resolve(device_name)
|
|
114
|
+
await _spotify_client.add_to_queue(uri=uri, device_id=device_id)
|
|
115
|
+
return ActionSuccessResponse(message=f"Added {uri} to queue")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@mcp.tool()
|
|
119
|
+
async def set_volume(
|
|
120
|
+
volume_percent: int, device_name: str | None = None
|
|
121
|
+
) -> ActionSuccessResponse:
|
|
122
|
+
device_id = _device_resolver.resolve(device_name)
|
|
123
|
+
await _spotify_client.volume(volume_percent=volume_percent, device_id=device_id)
|
|
124
|
+
return ActionSuccessResponse(message=f"Volume set to {volume_percent}%")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@mcp.tool(
|
|
128
|
+
description="""Transfer playback to a device and force it to play.
|
|
129
|
+
Use this BEFORE start_playback if the device is paused or inactive.
|
|
130
|
+
Also use when switching between devices."""
|
|
131
|
+
)
|
|
132
|
+
async def transfer_playback(device_name: str) -> ActionSuccessResponse:
|
|
133
|
+
device_id = _device_resolver.resolve(device_name)
|
|
134
|
+
if not device_id:
|
|
135
|
+
return ActionSuccessResponse(message=f"Device '{device_name}' not found")
|
|
136
|
+
await _spotify_client.transfer_playback(device_id=device_id, force_play=True)
|
|
137
|
+
return ActionSuccessResponse(
|
|
138
|
+
message=f"Playback transferred to device {device_name}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@mcp.tool()
|
|
143
|
+
async def search_tracks(
|
|
144
|
+
query: str,
|
|
145
|
+
limit: int = 10,
|
|
146
|
+
market: str | None = None,
|
|
147
|
+
) -> list[Track]:
|
|
148
|
+
result = await _spotify_client.search(
|
|
149
|
+
q=query, type="track", limit=limit, market=market
|
|
150
|
+
)
|
|
151
|
+
return result.tracks.items if result.tracks else []
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@mcp.tool()
|
|
155
|
+
async def search_albums(
|
|
156
|
+
query: str,
|
|
157
|
+
limit: int = 10,
|
|
158
|
+
market: str | None = None,
|
|
159
|
+
) -> list[SimplifiedAlbum]:
|
|
160
|
+
result = await _spotify_client.search(
|
|
161
|
+
q=query, type="album", limit=limit, market=market
|
|
162
|
+
)
|
|
163
|
+
return result.albums.items if result.albums else []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.tool()
|
|
167
|
+
async def next_track(device_name: str | None = None) -> ActionSuccessResponse:
|
|
168
|
+
device_id = _device_resolver.resolve(device_name)
|
|
169
|
+
await _spotify_client.next_track(device_id=device_id)
|
|
170
|
+
return ActionSuccessResponse(message="Skipped to next track")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@mcp.tool()
|
|
174
|
+
async def previous_track(device_name: str | None = None) -> ActionSuccessResponse:
|
|
175
|
+
device_id = _device_resolver.resolve(device_name)
|
|
176
|
+
await _spotify_client.previous_track(device_id=device_id)
|
|
177
|
+
return ActionSuccessResponse(message="Skipped to previous track")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@mcp.tool()
|
|
181
|
+
async def set_shuffle(
|
|
182
|
+
state: bool, device_name: str | None = None
|
|
183
|
+
) -> ActionSuccessResponse:
|
|
184
|
+
device_id = _device_resolver.resolve(device_name)
|
|
185
|
+
await _spotify_client.shuffle(state=state, device_id=device_id)
|
|
186
|
+
status = "enabled" if state else "disabled"
|
|
187
|
+
return ActionSuccessResponse(message=f"Shuffle {status}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
async def play_album(
|
|
192
|
+
album_uri: str, device_name: str | None = None
|
|
193
|
+
) -> ActionSuccessResponse:
|
|
194
|
+
device_id = _device_resolver.resolve(device_name)
|
|
195
|
+
await _spotify_client.start_playback(device_id=device_id, context_uri=album_uri)
|
|
196
|
+
return ActionSuccessResponse(message=f"Playing album {album_uri}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@mcp.tool()
|
|
200
|
+
async def get_recently_played(
|
|
201
|
+
limit: int = 20,
|
|
202
|
+
after: int | None = None,
|
|
203
|
+
before: int | None = None,
|
|
204
|
+
) -> RecentlyPlayedResponse:
|
|
205
|
+
return await _spotify_client.current_user_recently_played(
|
|
206
|
+
limit=limit, after=after, before=before
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@mcp.tool()
|
|
211
|
+
async def create_playlist(
|
|
212
|
+
name: str,
|
|
213
|
+
description: str | None = None,
|
|
214
|
+
public: bool = True,
|
|
215
|
+
) -> Playlist:
|
|
216
|
+
return await _spotify_client.create_playlist(
|
|
217
|
+
name=name, description=description, public=public
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@mcp.tool()
|
|
222
|
+
async def add_tracks_to_playlist(
|
|
223
|
+
playlist_id: str,
|
|
224
|
+
uris: list[str],
|
|
225
|
+
) -> ActionSuccessResponse:
|
|
226
|
+
await _spotify_client.add_tracks_to_playlist(playlist_id=playlist_id, uris=uris)
|
|
227
|
+
return ActionSuccessResponse(message=f"Added {len(uris)} track(s) to playlist")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@mcp.tool()
|
|
231
|
+
async def remove_tracks_from_playlist(
|
|
232
|
+
playlist_id: str,
|
|
233
|
+
uris: list[str],
|
|
234
|
+
) -> ActionSuccessResponse:
|
|
235
|
+
await _spotify_client.remove_tracks_from_playlist(
|
|
236
|
+
playlist_id=playlist_id, uris=uris
|
|
237
|
+
)
|
|
238
|
+
return ActionSuccessResponse(message=f"Removed {len(uris)} track(s) from playlist")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@mcp.tool()
|
|
242
|
+
async def get_user_playlists(
|
|
243
|
+
limit: int = 50,
|
|
244
|
+
offset: int = 0,
|
|
245
|
+
) -> PlaylistsResponse:
|
|
246
|
+
return await _spotify_client.get_user_playlists(limit=limit, offset=offset)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@mcp.tool()
|
|
250
|
+
async def play_playlist(
|
|
251
|
+
playlist_uri: str,
|
|
252
|
+
device_name: str | None = None,
|
|
253
|
+
) -> ActionSuccessResponse:
|
|
254
|
+
device_id = _device_resolver.resolve(device_name)
|
|
255
|
+
await _spotify_client.start_playback(device_id=device_id, context_uri=playlist_uri)
|
|
256
|
+
return ActionSuccessResponse(message=f"Playing playlist {playlist_uri}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@mcp.tool()
|
|
260
|
+
async def search_shows(
|
|
261
|
+
query: str,
|
|
262
|
+
limit: int = 10,
|
|
263
|
+
market: str | None = None,
|
|
264
|
+
) -> list:
|
|
265
|
+
result = await _spotify_client.search_shows(q=query, limit=limit, market=market)
|
|
266
|
+
return result.items if result.items else []
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@mcp.tool()
|
|
270
|
+
async def get_show_episodes(
|
|
271
|
+
show_id: str,
|
|
272
|
+
limit: int = 50,
|
|
273
|
+
offset: int = 0,
|
|
274
|
+
) -> list[Episode]:
|
|
275
|
+
result = await _spotify_client.get_show_episodes(
|
|
276
|
+
show_id=show_id, limit=limit, offset=offset
|
|
277
|
+
)
|
|
278
|
+
return result.items if result.items else []
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@mcp.tool()
|
|
282
|
+
async def play_episode(
|
|
283
|
+
episode_uri: str,
|
|
284
|
+
device_name: str | None = None,
|
|
285
|
+
) -> ActionSuccessResponse:
|
|
286
|
+
device_id = _device_resolver.resolve(device_name)
|
|
287
|
+
await _spotify_client.start_playback(device_id=device_id, uris=[episode_uri])
|
|
288
|
+
return ActionSuccessResponse(message=f"Playing episode {episode_uri}")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@mcp.tool()
|
|
292
|
+
async def get_saved_tracks(
|
|
293
|
+
limit: int = 20,
|
|
294
|
+
offset: int = 0,
|
|
295
|
+
) -> list[Track]:
|
|
296
|
+
result = await _spotify_client.get_saved_tracks(limit=limit, offset=offset)
|
|
297
|
+
return result.items if result.items else []
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@mcp.tool()
|
|
301
|
+
async def save_tracks(
|
|
302
|
+
uris: list[str],
|
|
303
|
+
) -> ActionSuccessResponse:
|
|
304
|
+
await _spotify_client.save_tracks(uris=uris)
|
|
305
|
+
return ActionSuccessResponse(message=f"Saved {len(uris)} track(s) to library")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@mcp.tool()
|
|
309
|
+
async def remove_saved_tracks(
|
|
310
|
+
uris: list[str],
|
|
311
|
+
) -> ActionSuccessResponse:
|
|
312
|
+
await _spotify_client.remove_saved_tracks(uris=uris)
|
|
313
|
+
return ActionSuccessResponse(message=f"Removed {len(uris)} track(s) from library")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@mcp.tool()
|
|
317
|
+
async def is_track_saved(
|
|
318
|
+
uri: str,
|
|
319
|
+
) -> dict:
|
|
320
|
+
saved = await _spotify_client.is_track_saved(uri=uri)
|
|
321
|
+
return {"uri": uri, "saved": saved}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@mcp.tool()
|
|
325
|
+
async def get_top_tracks(
|
|
326
|
+
time_range: str = "medium_term",
|
|
327
|
+
limit: int = 20,
|
|
328
|
+
offset: int = 0,
|
|
329
|
+
) -> list[Track]:
|
|
330
|
+
result = await _spotify_client.get_top_tracks(
|
|
331
|
+
time_range=time_range, limit=limit, offset=offset
|
|
332
|
+
)
|
|
333
|
+
return result.items if result.items else []
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@mcp.tool()
|
|
337
|
+
async def get_top_artists(
|
|
338
|
+
time_range: str = "medium_term",
|
|
339
|
+
limit: int = 20,
|
|
340
|
+
offset: int = 0,
|
|
341
|
+
) -> list[Artist]:
|
|
342
|
+
result = await _spotify_client.get_top_artists(
|
|
343
|
+
time_range=time_range, limit=limit, offset=offset
|
|
344
|
+
)
|
|
345
|
+
return result.items if result.items else []
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@mcp.tool()
|
|
349
|
+
async def get_artist(
|
|
350
|
+
artist_id: str,
|
|
351
|
+
) -> Artist:
|
|
352
|
+
return await _spotify_client.get_artist(artist_id=artist_id)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@mcp.tool()
|
|
356
|
+
async def get_artist_top_tracks(
|
|
357
|
+
artist_id: str,
|
|
358
|
+
market: str | None = None,
|
|
359
|
+
) -> list[Track]:
|
|
360
|
+
result = await _spotify_client.get_artist_top_tracks(
|
|
361
|
+
artist_id=artist_id, market=market
|
|
362
|
+
)
|
|
363
|
+
return [Track.model_validate(track) for track in result]
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mcp.tool()
|
|
367
|
+
async def get_album(
|
|
368
|
+
album_id: str,
|
|
369
|
+
) -> AlbumDetails:
|
|
370
|
+
return await _spotify_client.get_album(album_id=album_id)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@mcp.tool()
|
|
374
|
+
async def set_repeat(
|
|
375
|
+
state: str,
|
|
376
|
+
device_name: str | None = None,
|
|
377
|
+
) -> ActionSuccessResponse:
|
|
378
|
+
device_id = _device_resolver.resolve(device_name)
|
|
379
|
+
await _spotify_client.set_repeat(state=state, device_id=device_id)
|
|
380
|
+
return ActionSuccessResponse(message=f"Repeat mode set to {state}")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@mcp.tool()
|
|
384
|
+
async def get_queue() -> QueueResponse:
|
|
385
|
+
return await _spotify_client.queue()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@mcp.tool()
|
|
389
|
+
async def seek_track(
|
|
390
|
+
position_ms: int,
|
|
391
|
+
device_name: str | None = None,
|
|
392
|
+
) -> ActionSuccessResponse:
|
|
393
|
+
device_id = _device_resolver.resolve(device_name)
|
|
394
|
+
await _spotify_client.seek_track(position_ms=position_ms, device_id=device_id)
|
|
395
|
+
return ActionSuccessResponse(message=f"Seeked to {position_ms}ms")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@mcp.tool()
|
|
399
|
+
async def get_saved_shows(
|
|
400
|
+
limit: int = 20,
|
|
401
|
+
offset: int = 0,
|
|
402
|
+
) -> list:
|
|
403
|
+
result = await _spotify_client.get_saved_shows(limit=limit, offset=offset)
|
|
404
|
+
return result.items if result.items else []
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@mcp.tool()
|
|
408
|
+
async def get_saved_albums(
|
|
409
|
+
limit: int = 20,
|
|
410
|
+
offset: int = 0,
|
|
411
|
+
) -> SavedAlbumsResponse:
|
|
412
|
+
return await _spotify_client.get_saved_albums(limit=limit, offset=offset)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@mcp.tool()
|
|
416
|
+
async def remove_saved_albums(
|
|
417
|
+
album_ids: list[str],
|
|
418
|
+
) -> ActionSuccessResponse:
|
|
419
|
+
await _spotify_client.remove_saved_albums(album_ids=album_ids)
|
|
420
|
+
return ActionSuccessResponse(
|
|
421
|
+
message=f"Removed {len(album_ids)} album(s) from library"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@mcp.tool()
|
|
426
|
+
async def save_albums(
|
|
427
|
+
album_ids: list[str],
|
|
428
|
+
) -> ActionSuccessResponse:
|
|
429
|
+
await _spotify_client.save_albums(album_ids=album_ids)
|
|
430
|
+
return ActionSuccessResponse(message=f"Saved {len(album_ids)} album(s) to library")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@mcp.tool()
|
|
434
|
+
async def is_album_saved(
|
|
435
|
+
album_id: str,
|
|
436
|
+
) -> dict:
|
|
437
|
+
saved = await _spotify_client.is_album_saved(album_id=album_id)
|
|
438
|
+
return {"album_id": album_id, "saved": saved}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@mcp.tool()
|
|
442
|
+
async def get_album_tracks(
|
|
443
|
+
album_id: str,
|
|
444
|
+
limit: int = 50,
|
|
445
|
+
offset: int = 0,
|
|
446
|
+
) -> list[Track]:
|
|
447
|
+
result = await _spotify_client.album_tracks(
|
|
448
|
+
album_id=album_id, limit=limit, offset=offset
|
|
449
|
+
)
|
|
450
|
+
return result.items
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@mcp.tool()
|
|
454
|
+
async def get_playlist(
|
|
455
|
+
playlist_id: str,
|
|
456
|
+
) -> Playlist:
|
|
457
|
+
return await _spotify_client.get_playlist(playlist_id=playlist_id)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@mcp.tool()
|
|
461
|
+
async def delete_playlist(
|
|
462
|
+
playlist_id: str,
|
|
463
|
+
) -> ActionSuccessResponse:
|
|
464
|
+
await _spotify_client.delete_playlist(playlist_id=playlist_id)
|
|
465
|
+
return ActionSuccessResponse(message=f"Playlist {playlist_id} deleted")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@mcp.tool()
|
|
469
|
+
async def get_new_releases(
|
|
470
|
+
country: str | None = None,
|
|
471
|
+
limit: int = 20,
|
|
472
|
+
offset: int = 0,
|
|
473
|
+
) -> NewReleasesResponse:
|
|
474
|
+
return await _spotify_client.get_new_releases(
|
|
475
|
+
country=country, limit=limit, offset=offset
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@mcp.tool()
|
|
480
|
+
async def get_currently_playing(
|
|
481
|
+
market: str | None = None,
|
|
482
|
+
) -> CurrentlyPlayingResponse | None:
|
|
483
|
+
return await _spotify_client.currently_playing(market=market)
|