spotifyify 0.1.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.
- spotifyify-0.1.0/PKG-INFO +222 -0
- spotifyify-0.1.0/README.md +208 -0
- spotifyify-0.1.0/pyproject.toml +37 -0
- spotifyify-0.1.0/setup.cfg +4 -0
- spotifyify-0.1.0/spotifyify/__init__.py +51 -0
- spotifyify-0.1.0/spotifyify/credentials.py +13 -0
- spotifyify-0.1.0/spotifyify/device_resolver.py +14 -0
- spotifyify-0.1.0/spotifyify/mcp/cli.py +5 -0
- spotifyify-0.1.0/spotifyify/mcp/server.py +483 -0
- spotifyify-0.1.0/spotifyify/schemas.py +214 -0
- spotifyify-0.1.0/spotifyify/service.py +532 -0
- spotifyify-0.1.0/spotifyify/views.py +29 -0
- spotifyify-0.1.0/spotifyify.egg-info/PKG-INFO +222 -0
- spotifyify-0.1.0/spotifyify.egg-info/SOURCES.txt +16 -0
- spotifyify-0.1.0/spotifyify.egg-info/dependency_links.txt +1 -0
- spotifyify-0.1.0/spotifyify.egg-info/entry_points.txt +2 -0
- spotifyify-0.1.0/spotifyify.egg-info/requires.txt +9 -0
- spotifyify-0.1.0/spotifyify.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spotifyify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async wrapper for spotipy with a focus on integration with MCP agents.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
8
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
9
|
+
Requires-Dist: spotipy>=2.25.2
|
|
10
|
+
Provides-Extra: mcp
|
|
11
|
+
Requires-Dist: mcp[cli]>=1.0.0; extra == "mcp"
|
|
12
|
+
Provides-Extra: agents
|
|
13
|
+
Requires-Dist: openai-agents>=0.6.7; extra == "agents"
|
|
14
|
+
|
|
15
|
+
# spotifyify
|
|
16
|
+
|
|
17
|
+
Async wrapper around [spotipy](https://github.com/spotipy-dev/spotipy) with Pydantic models, built for integration with MCP agents.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Core library only
|
|
23
|
+
pip install spotifyify
|
|
24
|
+
|
|
25
|
+
# With MCP server
|
|
26
|
+
pip install spotifyify[mcp]
|
|
27
|
+
|
|
28
|
+
# With OpenAI Agents SDK (for testing mcp capabilities)
|
|
29
|
+
pip install spotifyify[mcp,agents]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
Create a `.env` file:
|
|
35
|
+
|
|
36
|
+
```env
|
|
37
|
+
SPOTIFY_CLIENT_ID=your_client_id
|
|
38
|
+
SPOTIFY_CLIENT_SECRET=your_client_secret
|
|
39
|
+
SPOTIFY_REDIRECT_URI=http://127.0.0.1:8080
|
|
40
|
+
|
|
41
|
+
# Required for agents extra
|
|
42
|
+
OPENAI_API_KEY=your_openai_api_key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Get your Spotify credentials at [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard).
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### As a Python library
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import asyncio
|
|
53
|
+
from spotifyify import AsyncSpotify
|
|
54
|
+
from spotifyify.credentials import SpotifyCredentials
|
|
55
|
+
from spotifyify.types import SpotifyScope
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
client = AsyncSpotify(
|
|
59
|
+
credentials=SpotifyCredentials(),
|
|
60
|
+
scopes=[
|
|
61
|
+
SpotifyScope.USER_READ_PLAYBACK_STATE,
|
|
62
|
+
SpotifyScope.USER_MODIFY_PLAYBACK_STATE,
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
playback = await client.current_playback()
|
|
67
|
+
if playback and playback.item:
|
|
68
|
+
print(f"Now playing: {playback.item.name}")
|
|
69
|
+
|
|
70
|
+
results = await client.search(q="Fred again", type="track", limit=5)
|
|
71
|
+
for track in results.tracks.items:
|
|
72
|
+
print(f"{track.name} — {', '.join(a.name for a in track.artists)}")
|
|
73
|
+
|
|
74
|
+
asyncio.run(main())
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### As an MCP server
|
|
78
|
+
|
|
79
|
+
Run the server directly:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv run spotify-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or connect it to an agent:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import asyncio
|
|
89
|
+
from agents import Agent, Runner
|
|
90
|
+
from agents.mcp import MCPServerStdio
|
|
91
|
+
from dotenv import load_dotenv
|
|
92
|
+
|
|
93
|
+
load_dotenv(override=True)
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
async with MCPServerStdio(
|
|
97
|
+
name="Spotify Server",
|
|
98
|
+
params={"command": "uv", "args": ["run", "spotify-mcp"]},
|
|
99
|
+
) as spotify_server:
|
|
100
|
+
agent = Agent(
|
|
101
|
+
name="Spotify DJ",
|
|
102
|
+
instructions="Du bist ein professioneller DJ Assistant.",
|
|
103
|
+
mcp_servers=[spotify_server],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
history = []
|
|
107
|
+
while True:
|
|
108
|
+
user_input = input("Du: ").strip()
|
|
109
|
+
if user_input.lower() in ["exit", "quit"]:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
history.append({"role": "user", "content": user_input})
|
|
113
|
+
result = await Runner.run(agent, history)
|
|
114
|
+
history = result.to_input_list()
|
|
115
|
+
print(f"Assistant: {result.final_output}\n")
|
|
116
|
+
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## MCP Tools
|
|
121
|
+
|
|
122
|
+
### Playback
|
|
123
|
+
| Tool | Description |
|
|
124
|
+
|------|-------------|
|
|
125
|
+
| `get_current_playback` | Current playback state and track info |
|
|
126
|
+
| `get_currently_playing` | Lightweight currently playing track |
|
|
127
|
+
| `start_playback` | Start playback via context URI or track URIs |
|
|
128
|
+
| `pause_playback` | Pause current playback |
|
|
129
|
+
| `next_track` | Skip to next track |
|
|
130
|
+
| `previous_track` | Skip to previous track |
|
|
131
|
+
| `set_shuffle` | Enable/disable shuffle |
|
|
132
|
+
| `set_repeat` | Set repeat mode (`track`, `context`, `off`) |
|
|
133
|
+
| `seek_track` | Seek to position in current track |
|
|
134
|
+
| `set_volume` | Set device volume (0–100) |
|
|
135
|
+
|
|
136
|
+
### Devices
|
|
137
|
+
| Tool | Description |
|
|
138
|
+
|------|-------------|
|
|
139
|
+
| `get_devices` | List available devices and refresh cache |
|
|
140
|
+
| `transfer_playback` | Transfer playback to another device |
|
|
141
|
+
|
|
142
|
+
### Queue
|
|
143
|
+
| Tool | Description |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `get_queue` | Get current playback queue |
|
|
146
|
+
| `add_to_queue` | Add track to queue |
|
|
147
|
+
|
|
148
|
+
### Search
|
|
149
|
+
| Tool | Description |
|
|
150
|
+
|------|-------------|
|
|
151
|
+
| `search_tracks` | Search for tracks |
|
|
152
|
+
| `search_albums` | Search for albums |
|
|
153
|
+
| `search_shows` | Search for podcasts/shows |
|
|
154
|
+
|
|
155
|
+
### Library
|
|
156
|
+
| Tool | Description |
|
|
157
|
+
|------|-------------|
|
|
158
|
+
| `get_saved_tracks` | Get liked songs |
|
|
159
|
+
| `save_tracks` | Like tracks |
|
|
160
|
+
| `remove_saved_tracks` | Unlike tracks |
|
|
161
|
+
| `is_track_saved` | Check if track is liked |
|
|
162
|
+
| `get_saved_albums` | Get saved albums |
|
|
163
|
+
| `save_albums` | Save albums to library |
|
|
164
|
+
| `remove_saved_albums` | Remove albums from library |
|
|
165
|
+
| `is_album_saved` | Check if album is saved |
|
|
166
|
+
| `get_saved_shows` | Get saved podcasts |
|
|
167
|
+
|
|
168
|
+
### Playlists
|
|
169
|
+
| Tool | Description |
|
|
170
|
+
|------|-------------|
|
|
171
|
+
| `get_user_playlists` | List user playlists |
|
|
172
|
+
| `create_playlist` | Create a new playlist |
|
|
173
|
+
| `add_tracks_to_playlist` | Add tracks to playlist |
|
|
174
|
+
| `remove_tracks_from_playlist` | Remove tracks from playlist |
|
|
175
|
+
| `play_playlist` | Play a playlist |
|
|
176
|
+
| `delete_playlist` | Delete (unfollow) a playlist |
|
|
177
|
+
|
|
178
|
+
### Artists & Albums
|
|
179
|
+
| Tool | Description |
|
|
180
|
+
|------|-------------|
|
|
181
|
+
| `get_artist` | Get artist details |
|
|
182
|
+
| `get_artist_top_tracks` | Get artist's top 10 tracks |
|
|
183
|
+
| `get_album` | Get album details |
|
|
184
|
+
| `album_tracks` | Get all tracks of an album |
|
|
185
|
+
| `play_album` | Play an album |
|
|
186
|
+
|
|
187
|
+
### Discovery
|
|
188
|
+
| Tool | Description |
|
|
189
|
+
|------|-------------|
|
|
190
|
+
| `get_recently_played` | Recently played tracks |
|
|
191
|
+
| `get_top_tracks` | User's top tracks (`short_term`, `medium_term`, `long_term`) |
|
|
192
|
+
| `get_top_artists` | User's top artists |
|
|
193
|
+
| `get_new_releases` | New album releases |
|
|
194
|
+
| `get_show_episodes` | Episodes of a podcast |
|
|
195
|
+
| `play_episode` | Play a podcast episode |
|
|
196
|
+
|
|
197
|
+
## Device Resolution
|
|
198
|
+
|
|
199
|
+
Devices are resolved by name at startup and cached automatically. Names are case-insensitive.
|
|
200
|
+
|
|
201
|
+
If a device isn't found, call `get_devices` to refresh the cache — e.g. after opening Spotify on a new device.
|
|
202
|
+
|
|
203
|
+
> **Note:** If Spotify is paused or inactive, call `transfer_playback` before `start_playback` to activate the device first.
|
|
204
|
+
|
|
205
|
+
## Architecture
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
spotifyify/
|
|
209
|
+
├── __init__.py # AsyncSpotify, SpotifyScope
|
|
210
|
+
├── credentials.py # Pydantic settings (reads from .env)
|
|
211
|
+
├── device_resolver.py # Device name → ID cache
|
|
212
|
+
├── schemas.py # Pydantic models for all API responses
|
|
213
|
+
├── views.py # Shared response types
|
|
214
|
+
└── mcp/
|
|
215
|
+
├── server.py # FastMCP server + tool definitions
|
|
216
|
+
└── cli.py # Entry point for spotify-mcp command
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Requirements
|
|
220
|
+
|
|
221
|
+
- Python 3.13+
|
|
222
|
+
- Spotify Premium (required for playback control)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# spotifyify
|
|
2
|
+
|
|
3
|
+
Async wrapper around [spotipy](https://github.com/spotipy-dev/spotipy) with Pydantic models, built for integration with MCP agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Core library only
|
|
9
|
+
pip install spotifyify
|
|
10
|
+
|
|
11
|
+
# With MCP server
|
|
12
|
+
pip install spotifyify[mcp]
|
|
13
|
+
|
|
14
|
+
# With OpenAI Agents SDK (for testing mcp capabilities)
|
|
15
|
+
pip install spotifyify[mcp,agents]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Create a `.env` file:
|
|
21
|
+
|
|
22
|
+
```env
|
|
23
|
+
SPOTIFY_CLIENT_ID=your_client_id
|
|
24
|
+
SPOTIFY_CLIENT_SECRET=your_client_secret
|
|
25
|
+
SPOTIFY_REDIRECT_URI=http://127.0.0.1:8080
|
|
26
|
+
|
|
27
|
+
# Required for agents extra
|
|
28
|
+
OPENAI_API_KEY=your_openai_api_key
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Get your Spotify credentials at [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard).
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### As a Python library
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from spotifyify import AsyncSpotify
|
|
40
|
+
from spotifyify.credentials import SpotifyCredentials
|
|
41
|
+
from spotifyify.types import SpotifyScope
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
client = AsyncSpotify(
|
|
45
|
+
credentials=SpotifyCredentials(),
|
|
46
|
+
scopes=[
|
|
47
|
+
SpotifyScope.USER_READ_PLAYBACK_STATE,
|
|
48
|
+
SpotifyScope.USER_MODIFY_PLAYBACK_STATE,
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
playback = await client.current_playback()
|
|
53
|
+
if playback and playback.item:
|
|
54
|
+
print(f"Now playing: {playback.item.name}")
|
|
55
|
+
|
|
56
|
+
results = await client.search(q="Fred again", type="track", limit=5)
|
|
57
|
+
for track in results.tracks.items:
|
|
58
|
+
print(f"{track.name} — {', '.join(a.name for a in track.artists)}")
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### As an MCP server
|
|
64
|
+
|
|
65
|
+
Run the server directly:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv run spotify-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or connect it to an agent:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import asyncio
|
|
75
|
+
from agents import Agent, Runner
|
|
76
|
+
from agents.mcp import MCPServerStdio
|
|
77
|
+
from dotenv import load_dotenv
|
|
78
|
+
|
|
79
|
+
load_dotenv(override=True)
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
async with MCPServerStdio(
|
|
83
|
+
name="Spotify Server",
|
|
84
|
+
params={"command": "uv", "args": ["run", "spotify-mcp"]},
|
|
85
|
+
) as spotify_server:
|
|
86
|
+
agent = Agent(
|
|
87
|
+
name="Spotify DJ",
|
|
88
|
+
instructions="Du bist ein professioneller DJ Assistant.",
|
|
89
|
+
mcp_servers=[spotify_server],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
history = []
|
|
93
|
+
while True:
|
|
94
|
+
user_input = input("Du: ").strip()
|
|
95
|
+
if user_input.lower() in ["exit", "quit"]:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
history.append({"role": "user", "content": user_input})
|
|
99
|
+
result = await Runner.run(agent, history)
|
|
100
|
+
history = result.to_input_list()
|
|
101
|
+
print(f"Assistant: {result.final_output}\n")
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## MCP Tools
|
|
107
|
+
|
|
108
|
+
### Playback
|
|
109
|
+
| Tool | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `get_current_playback` | Current playback state and track info |
|
|
112
|
+
| `get_currently_playing` | Lightweight currently playing track |
|
|
113
|
+
| `start_playback` | Start playback via context URI or track URIs |
|
|
114
|
+
| `pause_playback` | Pause current playback |
|
|
115
|
+
| `next_track` | Skip to next track |
|
|
116
|
+
| `previous_track` | Skip to previous track |
|
|
117
|
+
| `set_shuffle` | Enable/disable shuffle |
|
|
118
|
+
| `set_repeat` | Set repeat mode (`track`, `context`, `off`) |
|
|
119
|
+
| `seek_track` | Seek to position in current track |
|
|
120
|
+
| `set_volume` | Set device volume (0–100) |
|
|
121
|
+
|
|
122
|
+
### Devices
|
|
123
|
+
| Tool | Description |
|
|
124
|
+
|------|-------------|
|
|
125
|
+
| `get_devices` | List available devices and refresh cache |
|
|
126
|
+
| `transfer_playback` | Transfer playback to another device |
|
|
127
|
+
|
|
128
|
+
### Queue
|
|
129
|
+
| Tool | Description |
|
|
130
|
+
|------|-------------|
|
|
131
|
+
| `get_queue` | Get current playback queue |
|
|
132
|
+
| `add_to_queue` | Add track to queue |
|
|
133
|
+
|
|
134
|
+
### Search
|
|
135
|
+
| Tool | Description |
|
|
136
|
+
|------|-------------|
|
|
137
|
+
| `search_tracks` | Search for tracks |
|
|
138
|
+
| `search_albums` | Search for albums |
|
|
139
|
+
| `search_shows` | Search for podcasts/shows |
|
|
140
|
+
|
|
141
|
+
### Library
|
|
142
|
+
| Tool | Description |
|
|
143
|
+
|------|-------------|
|
|
144
|
+
| `get_saved_tracks` | Get liked songs |
|
|
145
|
+
| `save_tracks` | Like tracks |
|
|
146
|
+
| `remove_saved_tracks` | Unlike tracks |
|
|
147
|
+
| `is_track_saved` | Check if track is liked |
|
|
148
|
+
| `get_saved_albums` | Get saved albums |
|
|
149
|
+
| `save_albums` | Save albums to library |
|
|
150
|
+
| `remove_saved_albums` | Remove albums from library |
|
|
151
|
+
| `is_album_saved` | Check if album is saved |
|
|
152
|
+
| `get_saved_shows` | Get saved podcasts |
|
|
153
|
+
|
|
154
|
+
### Playlists
|
|
155
|
+
| Tool | Description |
|
|
156
|
+
|------|-------------|
|
|
157
|
+
| `get_user_playlists` | List user playlists |
|
|
158
|
+
| `create_playlist` | Create a new playlist |
|
|
159
|
+
| `add_tracks_to_playlist` | Add tracks to playlist |
|
|
160
|
+
| `remove_tracks_from_playlist` | Remove tracks from playlist |
|
|
161
|
+
| `play_playlist` | Play a playlist |
|
|
162
|
+
| `delete_playlist` | Delete (unfollow) a playlist |
|
|
163
|
+
|
|
164
|
+
### Artists & Albums
|
|
165
|
+
| Tool | Description |
|
|
166
|
+
|------|-------------|
|
|
167
|
+
| `get_artist` | Get artist details |
|
|
168
|
+
| `get_artist_top_tracks` | Get artist's top 10 tracks |
|
|
169
|
+
| `get_album` | Get album details |
|
|
170
|
+
| `album_tracks` | Get all tracks of an album |
|
|
171
|
+
| `play_album` | Play an album |
|
|
172
|
+
|
|
173
|
+
### Discovery
|
|
174
|
+
| Tool | Description |
|
|
175
|
+
|------|-------------|
|
|
176
|
+
| `get_recently_played` | Recently played tracks |
|
|
177
|
+
| `get_top_tracks` | User's top tracks (`short_term`, `medium_term`, `long_term`) |
|
|
178
|
+
| `get_top_artists` | User's top artists |
|
|
179
|
+
| `get_new_releases` | New album releases |
|
|
180
|
+
| `get_show_episodes` | Episodes of a podcast |
|
|
181
|
+
| `play_episode` | Play a podcast episode |
|
|
182
|
+
|
|
183
|
+
## Device Resolution
|
|
184
|
+
|
|
185
|
+
Devices are resolved by name at startup and cached automatically. Names are case-insensitive.
|
|
186
|
+
|
|
187
|
+
If a device isn't found, call `get_devices` to refresh the cache — e.g. after opening Spotify on a new device.
|
|
188
|
+
|
|
189
|
+
> **Note:** If Spotify is paused or inactive, call `transfer_playback` before `start_playback` to activate the device first.
|
|
190
|
+
|
|
191
|
+
## Architecture
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
spotifyify/
|
|
195
|
+
├── __init__.py # AsyncSpotify, SpotifyScope
|
|
196
|
+
├── credentials.py # Pydantic settings (reads from .env)
|
|
197
|
+
├── device_resolver.py # Device name → ID cache
|
|
198
|
+
├── schemas.py # Pydantic models for all API responses
|
|
199
|
+
├── views.py # Shared response types
|
|
200
|
+
└── mcp/
|
|
201
|
+
├── server.py # FastMCP server + tool definitions
|
|
202
|
+
└── cli.py # Entry point for spotify-mcp command
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Requirements
|
|
206
|
+
|
|
207
|
+
- Python 3.13+
|
|
208
|
+
- Spotify Premium (required for playback control)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "spotifyify"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Async wrapper for spotipy with a focus on integration with MCP agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pydantic-settings>=2.12.0",
|
|
9
|
+
"python-dotenv>=1.2.1",
|
|
10
|
+
"spotipy>=2.25.2",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
mcp = [
|
|
15
|
+
"mcp[cli]>=1.0.0",
|
|
16
|
+
]
|
|
17
|
+
agents = [
|
|
18
|
+
"openai-agents>=0.6.7",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"black>=25.1.0,<26",
|
|
24
|
+
"pytest>=8.4.1,<9",
|
|
25
|
+
"pytest-asyncio>=1.1.0,<2",
|
|
26
|
+
"pytest-mock>=3.14.1,<4",
|
|
27
|
+
"pytest-cov>=6.2.1,<7",
|
|
28
|
+
"isort>=6.0.1,<7",
|
|
29
|
+
"ruff>=0.13.1",
|
|
30
|
+
"pre-commit>=4.3.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
spotify-mcp = "spotifyify.mcp.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.uv]
|
|
37
|
+
package = true
|
|
@@ -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()
|