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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()
@@ -0,0 +1,5 @@
1
+ from spotifyify.mcp.server import mcp
2
+
3
+
4
+ def main():
5
+ mcp.run()