hey-spotify-cli 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 prayag
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: hey-spotify-cli
3
+ Version: 1.0.0
4
+ Summary: A premium, voice-controlled terminal interface for Spotify playback.
5
+ Author-email: Prayag Tushar <prayagtushar@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/prayagtushar/hey-spotify-cli
8
+ Project-URL: Documentation, https://prayagtushar.github.io/hey-spotify-cli/
9
+ Project-URL: Repository, https://github.com/prayagtushar/hey-spotify-cli.git
10
+ Project-URL: BugTracker, https://github.com/prayagtushar/hey-spotify-cli/issues
11
+ Keywords: spotify,cli,voice-control,voice-command,hands-free,music
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Players
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: License :: OSI Approved :: MIT License
24
+ Classifier: Operating System :: OS Independent
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: spotipy>=2.24.0
29
+ Requires-Dist: python-dotenv>=1.0.1
30
+ Requires-Dist: PyAudio>=0.2.14
31
+ Requires-Dist: SpeechRecognition>=3.10.4
32
+ Requires-Dist: rich>=13.7.1
33
+ Dynamic: license-file
34
+
35
+ # hey-spotify 🎤 🎵
36
+
37
+ [![PyPI version](https://img.shields.io/pypi/v/hey-spotify-cli.svg)](https://pypi.org/project/hey-spotify-cli/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ A premium, voice-controlled terminal interface for Spotify playback. Control your music without leaving your terminal or touching your keyboard.
41
+
42
+ ## 📋 Prerequisites
43
+
44
+ - **Spotify Premium**: A Premium subscription is **required** for playback control via the Spotify API.
45
+ - **Python 3.8+**: Ensure you have a modern Python version installed.
46
+
47
+ ## 🚀 Quick Start
48
+
49
+ ### 1. Install
50
+ Install the package directly via pip:
51
+
52
+ ```bash
53
+ pip install hey-spotify-cli
54
+ ```
55
+
56
+ *Note: Depending on your OS, you might need to install system dependencies for `PyAudio` (e.g., `portaudio` on macOS or `python3-pyaudio` on Linux).*
57
+
58
+ ### 2. Spotify API Setup
59
+ 1. Go to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
60
+ 2. Create a new app and name it (e.g., `hey-spotify`).
61
+ 3. Add `http://127.0.0.1:8888/callback` as a **Redirect URI** in your app settings.
62
+ 4. Set up your environment variables (see [Environment Variables](#environment-variables) section).
63
+
64
+ ### 3. Run
65
+ Launch the CLI:
66
+
67
+ ```bash
68
+ hey-spotify
69
+ ```
70
+
71
+ On the first run, it will open your browser for authentication.
72
+
73
+ ## 🎙️ Voice Commands
74
+
75
+ Once active, just say **"Hey Spotify"** followed by:
76
+
77
+ - `play <song name>` — Plays a specific track.
78
+ - `pause` / `stop` — Pauses current playback.
79
+ - `resume` / `continue` — Resumes playback.
80
+ - `next` / `skip` — Skips to the next track.
81
+ - `previous` — Goes back to the previous track.
82
+ - `volume <0-100>` — Sets volume to a specific level.
83
+ - `volume up` / `volume down` — Adjusts volume by 10%.
84
+ - `now playing` — Shows information about the current track.
85
+ - `exit` / `quit` — Closes the application.
86
+
87
+ ## ⚙️ Configuration
88
+
89
+ ### Environment Variables
90
+ You can create a `.env` file in your working directory or set them in your shell:
91
+
92
+ ```bash
93
+ SPOTIPY_CLIENT_ID='your_client_id'
94
+ SPOTIPY_CLIENT_SECRET='your_client_secret'
95
+ SPOTIPY_REDIRECT_URI='http://127.0.0.1:8888/callback'
96
+ ```
97
+
98
+ ### CLI Options
99
+ - `--list-mics`: List all detected microphone devices.
100
+ - `--mic-index <index>`: Use a non-default microphone.
101
+ - `--no-wake-word`: Disable "Hey Spotify" trigger and listen continuously.
102
+ - `--language <lang>`: Change recognition language (e.g., `en-US`, `es-ES`).
103
+ - `--voice-debug`: See what the engine is hearing in real-time.
104
+
105
+ ## 🛠️ Troubleshooting
106
+ - **Microphone not found**: Ensure you've granted terminal permissions for the microphone and use `--list-mics` to find the correct index.
107
+ - **Spotify Authentication**: Ensure your Redirect URI in the Dashboard exactly matches the one in your environment.
108
+
109
+ ## 📄 License
110
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -0,0 +1,76 @@
1
+ # hey-spotify 🎤 🎵
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/hey-spotify-cli.svg)](https://pypi.org/project/hey-spotify-cli/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A premium, voice-controlled terminal interface for Spotify playback. Control your music without leaving your terminal or touching your keyboard.
7
+
8
+ ## 📋 Prerequisites
9
+
10
+ - **Spotify Premium**: A Premium subscription is **required** for playback control via the Spotify API.
11
+ - **Python 3.8+**: Ensure you have a modern Python version installed.
12
+
13
+ ## 🚀 Quick Start
14
+
15
+ ### 1. Install
16
+ Install the package directly via pip:
17
+
18
+ ```bash
19
+ pip install hey-spotify-cli
20
+ ```
21
+
22
+ *Note: Depending on your OS, you might need to install system dependencies for `PyAudio` (e.g., `portaudio` on macOS or `python3-pyaudio` on Linux).*
23
+
24
+ ### 2. Spotify API Setup
25
+ 1. Go to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
26
+ 2. Create a new app and name it (e.g., `hey-spotify`).
27
+ 3. Add `http://127.0.0.1:8888/callback` as a **Redirect URI** in your app settings.
28
+ 4. Set up your environment variables (see [Environment Variables](#environment-variables) section).
29
+
30
+ ### 3. Run
31
+ Launch the CLI:
32
+
33
+ ```bash
34
+ hey-spotify
35
+ ```
36
+
37
+ On the first run, it will open your browser for authentication.
38
+
39
+ ## 🎙️ Voice Commands
40
+
41
+ Once active, just say **"Hey Spotify"** followed by:
42
+
43
+ - `play <song name>` — Plays a specific track.
44
+ - `pause` / `stop` — Pauses current playback.
45
+ - `resume` / `continue` — Resumes playback.
46
+ - `next` / `skip` — Skips to the next track.
47
+ - `previous` — Goes back to the previous track.
48
+ - `volume <0-100>` — Sets volume to a specific level.
49
+ - `volume up` / `volume down` — Adjusts volume by 10%.
50
+ - `now playing` — Shows information about the current track.
51
+ - `exit` / `quit` — Closes the application.
52
+
53
+ ## ⚙️ Configuration
54
+
55
+ ### Environment Variables
56
+ You can create a `.env` file in your working directory or set them in your shell:
57
+
58
+ ```bash
59
+ SPOTIPY_CLIENT_ID='your_client_id'
60
+ SPOTIPY_CLIENT_SECRET='your_client_secret'
61
+ SPOTIPY_REDIRECT_URI='http://127.0.0.1:8888/callback'
62
+ ```
63
+
64
+ ### CLI Options
65
+ - `--list-mics`: List all detected microphone devices.
66
+ - `--mic-index <index>`: Use a non-default microphone.
67
+ - `--no-wake-word`: Disable "Hey Spotify" trigger and listen continuously.
68
+ - `--language <lang>`: Change recognition language (e.g., `en-US`, `es-ES`).
69
+ - `--voice-debug`: See what the engine is hearing in real-time.
70
+
71
+ ## 🛠️ Troubleshooting
72
+ - **Microphone not found**: Ensure you've granted terminal permissions for the microphone and use `--list-mics` to find the correct index.
73
+ - **Spotify Authentication**: Ensure your Redirect URI in the Dashboard exactly matches the one in your environment.
74
+
75
+ ## 📄 License
76
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -0,0 +1,15 @@
1
+ from importlib import import_module
2
+ from typing import Any
3
+
4
+ __all__ = ["SpotifyClient", "VoiceEngine", "ParsedCommand", "parse_voice_command"]
5
+
6
+
7
+ def __getattr__(name: str) -> Any:
8
+ if name == "SpotifyClient":
9
+ return import_module(".spotify_client", __name__).SpotifyClient
10
+ if name == "VoiceEngine":
11
+ return import_module(".voice_engine", __name__).VoiceEngine
12
+ if name in ("ParsedCommand", "parse_voice_command"):
13
+ utils_module = import_module(".utils", __name__)
14
+ return getattr(utils_module, name)
15
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -0,0 +1,264 @@
1
+ import asyncio
2
+ import argparse
3
+ import re
4
+ from typing import Any, Optional
5
+
6
+ try:
7
+ from dotenv import load_dotenv
8
+ except ModuleNotFoundError:
9
+ def load_dotenv() -> bool: # type: ignore[override]
10
+ return False
11
+
12
+ from .spotify_client import SpotifyClient
13
+ from .utils import parse_voice_command
14
+ from .voice_engine import VoiceEngine
15
+ from .ui.theme import ACCENT, APP_LOGO, APP_SUBTITLE, ERROR, SUCCESS, WARNING
16
+
17
+ RICH_AVAILABLE = True
18
+ try:
19
+ from rich.console import Console
20
+ from rich.live import Live
21
+
22
+ from .ui.cli import generate_boot_banner, generate_layout
23
+ except ModuleNotFoundError:
24
+ RICH_AVAILABLE = False
25
+
26
+ def generate_boot_banner() -> str:
27
+ return f"{APP_LOGO}\n{APP_SUBTITLE}\n"
28
+
29
+ def generate_layout(current_song: str = "None", status: str = "Waiting...", last_command: str = "N/A") -> str:
30
+ return (
31
+ f"Now Playing: {current_song}\n"
32
+ f"Status: {status}\n"
33
+ f"Last Command: {last_command}\n"
34
+ )
35
+
36
+
37
+ class PlainConsole:
38
+ def clear(self) -> None:
39
+ return
40
+
41
+ def print(self, message: Any) -> None:
42
+ print(_strip_markup(str(message)))
43
+
44
+
45
+ class NoopLive:
46
+ def __enter__(self) -> "NoopLive":
47
+ return self
48
+
49
+ def __exit__(self, exc_type: Any, exc: Any, traceback: Any) -> bool:
50
+ return False
51
+
52
+ def update(self, _renderable: Any) -> None:
53
+ return
54
+
55
+
56
+ def _strip_markup(message: str) -> str:
57
+ return re.sub(r"\[/?[^\]]+\]", "", message)
58
+
59
+
60
+ console = Console() if RICH_AVAILABLE else PlainConsole()
61
+
62
+
63
+ def print_status(style: str, message: str) -> None:
64
+ if RICH_AVAILABLE:
65
+ console.print(f"[{style}]{message}[/]")
66
+ return
67
+ console.print(message)
68
+
69
+
70
+ def _update_view(live: Any, current_song: str, status: str, last_command: str) -> None:
71
+ live.update(generate_layout(current_song=current_song, status=status, last_command=last_command))
72
+
73
+
74
+ async def handle_command(spotify: SpotifyClient, command: str) -> str:
75
+ parsed = parse_voice_command(command)
76
+
77
+ if parsed.action == "play":
78
+ return await asyncio.to_thread(spotify.play_specific_track, str(parsed.value or ""))
79
+ if parsed.action == "pause":
80
+ return await asyncio.to_thread(spotify.toggle_playback, "pause")
81
+ if parsed.action == "resume":
82
+ return await asyncio.to_thread(spotify.toggle_playback, "resume")
83
+ if parsed.action == "next":
84
+ return await asyncio.to_thread(spotify.skip_track)
85
+ if parsed.action == "previous":
86
+ return await asyncio.to_thread(spotify.previous_track)
87
+ if parsed.action == "volume_set":
88
+ if parsed.value is not None:
89
+ return await asyncio.to_thread(spotify.set_volume, int(parsed.value))
90
+ return "Invalid volume value."
91
+ if parsed.action == "volume_delta":
92
+ if parsed.value is not None:
93
+ return await asyncio.to_thread(spotify.adjust_volume, int(parsed.value))
94
+ return "Invalid volume value."
95
+ if parsed.action == "now_playing":
96
+ song = await asyncio.to_thread(spotify.current_song)
97
+ return f"Now playing: {song}"
98
+ if parsed.action == "exit":
99
+ return "Exiting hey-spotify."
100
+
101
+ return (
102
+ "I didn't understand the command. Try: "
103
+ "play <song>, pause, resume, next, previous, volume <0-100>."
104
+ )
105
+
106
+
107
+ def list_microphones() -> int:
108
+ microphones = VoiceEngine.list_microphones()
109
+ if not microphones:
110
+ print_status(
111
+ ERROR,
112
+ "No microphones detected. Check OS microphone permissions and input device settings.",
113
+ )
114
+ return 1
115
+
116
+ print_status(SUCCESS, "Detected microphone devices:")
117
+ for index, microphone in enumerate(microphones):
118
+ console.print(f" [{index}] {microphone}")
119
+ return 0
120
+
121
+
122
+ async def main(
123
+ microphone_index: Optional[int] = None,
124
+ voice_debug: bool = False,
125
+ require_wake_word: bool = True,
126
+ language: Optional[str] = None,
127
+ ) -> None:
128
+ load_dotenv()
129
+
130
+ console.clear()
131
+ console.print(generate_boot_banner())
132
+ await asyncio.sleep(0.8)
133
+
134
+ try:
135
+ listener = VoiceEngine(
136
+ microphone_index=microphone_index,
137
+ debug=voice_debug,
138
+ language=language,
139
+ )
140
+ except Exception as exc:
141
+ print_status(ERROR, f"Microphone initialization failed: {exc}")
142
+ return
143
+
144
+ try:
145
+ spotify = SpotifyClient()
146
+ except Exception as exc:
147
+ print_status(ERROR, f"Spotify client initialization failed: {exc}")
148
+ return
149
+
150
+ current_song = "Nothing playing"
151
+ status = "Waiting for wake word"
152
+ last_command = "N/A"
153
+
154
+ print_status(SUCCESS, "hey-spotify is active. Say 'Hey Spotify' to begin.")
155
+ print_status(ACCENT, f"Using microphone: {listener.selected_mic_name}")
156
+ print_status(ACCENT, f"Voice language: {listener.language}")
157
+ if not require_wake_word:
158
+ print_status(WARNING, "Wake word disabled. Listening directly for commands.")
159
+
160
+ live_context = (
161
+ Live(
162
+ generate_layout(current_song=current_song, status=status, last_command=last_command),
163
+ console=console,
164
+ refresh_per_second=6,
165
+ )
166
+ if RICH_AVAILABLE
167
+ else NoopLive()
168
+ )
169
+
170
+ with live_context as live:
171
+ while True:
172
+ pending_command: Optional[str] = None
173
+ if require_wake_word:
174
+ status = "Listening for wake word"
175
+ _update_view(live, current_song, status, last_command)
176
+
177
+ wake_word_detected, pending_command = await asyncio.to_thread(
178
+ listener.listen_for_wake_event
179
+ )
180
+ if not wake_word_detected:
181
+ continue
182
+
183
+ status = "Wake word detected"
184
+ _update_view(live, current_song, status, last_command)
185
+ else:
186
+ status = "Listening for command"
187
+ _update_view(live, current_song, status, last_command)
188
+
189
+ command = pending_command or await asyncio.to_thread(listener.get_command)
190
+ if not command:
191
+ status = "Could not understand command"
192
+ _update_view(live, current_song, status, last_command)
193
+ print_status(WARNING, "Could not understand command.")
194
+ continue
195
+
196
+ last_command = command
197
+ status = "Executing command"
198
+ _update_view(live, current_song, status, last_command)
199
+
200
+ try:
201
+ result = await handle_command(spotify, command)
202
+ if result == "Exiting hey-spotify.":
203
+ print_status(ACCENT, result)
204
+ break
205
+
206
+ current_song = await asyncio.to_thread(spotify.current_song)
207
+ status = result
208
+ _update_view(live, current_song, status, last_command)
209
+ print_status(ACCENT, result)
210
+ except Exception as exc:
211
+ status = "Command failed"
212
+ _update_view(live, current_song, status, last_command)
213
+ print_status(ERROR, f"Error: {exc}")
214
+
215
+
216
+ def cli() -> None:
217
+ parser = argparse.ArgumentParser(description="EchoSpotify voice-controlled Spotify CLI")
218
+ parser.add_argument(
219
+ "--list-mics",
220
+ action="store_true",
221
+ help="List detected microphone devices and exit.",
222
+ )
223
+ parser.add_argument(
224
+ "--mic-index",
225
+ type=int,
226
+ default=None,
227
+ help="Microphone index to use (find indexes with --list-mics).",
228
+ )
229
+ parser.add_argument(
230
+ "--voice-debug",
231
+ action="store_true",
232
+ help="Print recognized wake/command transcripts for debugging.",
233
+ )
234
+ parser.add_argument(
235
+ "--no-wake-word",
236
+ action="store_true",
237
+ help="Skip wake-word detection and listen directly for commands.",
238
+ )
239
+ parser.add_argument(
240
+ "--language",
241
+ type=str,
242
+ default=None,
243
+ help="Speech recognition language (example: en-US). Overrides VOICE_LANGUAGE.",
244
+ )
245
+ args = parser.parse_args()
246
+
247
+ if args.list_mics:
248
+ raise SystemExit(list_microphones())
249
+
250
+ try:
251
+ asyncio.run(
252
+ main(
253
+ microphone_index=args.mic_index,
254
+ voice_debug=args.voice_debug,
255
+ require_wake_word=not args.no_wake_word,
256
+ language=args.language,
257
+ )
258
+ )
259
+ except KeyboardInterrupt:
260
+ print_status(ERROR, "Shutting down... Goodbye.")
261
+
262
+
263
+ if __name__ == "__main__":
264
+ cli()
@@ -0,0 +1 @@
1
+ # Typing marker for PEP 561
@@ -0,0 +1,171 @@
1
+ import os
2
+ from typing import Any, Optional
3
+
4
+ try:
5
+ import spotipy
6
+ from spotipy.oauth2 import SpotifyOAuth
7
+ except ModuleNotFoundError:
8
+ spotipy = None
9
+ SpotifyOAuth = None
10
+
11
+ try:
12
+ from dotenv import load_dotenv
13
+ except ModuleNotFoundError:
14
+ def load_dotenv() -> bool: # type: ignore[override]
15
+ return False
16
+
17
+
18
+ class SpotifyClient:
19
+ def __init__(self) -> None:
20
+ if spotipy is None or SpotifyOAuth is None:
21
+ raise RuntimeError(
22
+ "Missing dependency 'spotipy'. Run: pip install -r requirements.txt"
23
+ )
24
+
25
+ load_dotenv()
26
+ client_id = os.getenv("SPOTIPY_CLIENT_ID") or os.getenv("SPOTIFY_CLIENT_ID")
27
+ client_secret = os.getenv("SPOTIPY_CLIENT_SECRET") or os.getenv("SPOTIFY_CLIENT_SECRET")
28
+ redirect_uri = os.getenv("SPOTIPY_REDIRECT_URI") or os.getenv("SPOTIFY_REDIRECT_URI")
29
+
30
+ missing_envs = []
31
+ if not client_id:
32
+ missing_envs.append("SPOTIPY_CLIENT_ID (or SPOTIFY_CLIENT_ID)")
33
+ if not client_secret:
34
+ missing_envs.append("SPOTIPY_CLIENT_SECRET (or SPOTIFY_CLIENT_SECRET)")
35
+ if not redirect_uri:
36
+ missing_envs.append("SPOTIPY_REDIRECT_URI (or SPOTIFY_REDIRECT_URI)")
37
+ if missing_envs:
38
+ formatted = ", ".join(missing_envs)
39
+ raise RuntimeError(f"Missing environment variables: {formatted}")
40
+
41
+ scope = (
42
+ "user-modify-playback-state "
43
+ "user-read-playback-state "
44
+ "user-read-currently-playing"
45
+ )
46
+ self.sp = spotipy.Spotify(
47
+ auth_manager=SpotifyOAuth(
48
+ client_id=client_id,
49
+ client_secret=client_secret,
50
+ redirect_uri=redirect_uri,
51
+ scope=scope,
52
+ cache_path=".cache",
53
+ )
54
+ )
55
+
56
+ def _active_device_id(self) -> Optional[str]:
57
+ try:
58
+ devices = self.sp.devices().get("devices", [])
59
+ except Exception:
60
+ return None
61
+
62
+ if not devices:
63
+ return None
64
+ for device in devices:
65
+ if device.get("is_active"):
66
+ return device.get("id")
67
+ return devices[0].get("id")
68
+
69
+ def _current_playback(self) -> Optional[dict[str, Any]]:
70
+ try:
71
+ return self.sp.current_playback()
72
+ except Exception:
73
+ return None
74
+
75
+ def current_song(self) -> str:
76
+ playback = self._current_playback()
77
+ if not playback or not playback.get("item"):
78
+ return "Nothing playing"
79
+
80
+ item = playback["item"]
81
+ artist_names = ", ".join(artist["name"] for artist in item.get("artists", []))
82
+ return f"{item.get('name', 'Unknown')} - {artist_names or 'Unknown artist'}"
83
+
84
+ def play_specific_track(self, track_name: str) -> str:
85
+ if not track_name:
86
+ return "Tell me what to play, for example: play Numb by Linkin Park."
87
+
88
+ try:
89
+ results = self.sp.search(q=track_name, limit=1, type="track")
90
+ except Exception as exc:
91
+ return f"Spotify search failed: {exc}"
92
+
93
+ items = results.get("tracks", {}).get("items", [])
94
+ if not items:
95
+ return f"Could not find '{track_name}'."
96
+
97
+ track = items[0]
98
+ track_uri = track["uri"]
99
+ track_name = track.get("name", "Unknown song")
100
+ artist_name = track.get("artists", [{}])[0].get("name", "Unknown artist")
101
+
102
+ device_id = self._active_device_id()
103
+ if not device_id:
104
+ return "No Spotify device found. Open Spotify on phone/desktop and try again."
105
+
106
+ try:
107
+ self.sp.start_playback(device_id=device_id, uris=[track_uri])
108
+ except Exception as exc:
109
+ return f"Could not start playback: {exc}"
110
+
111
+ return f"Playing {track_name} by {artist_name}."
112
+
113
+ def toggle_playback(self, action: str) -> str:
114
+ device_id = self._active_device_id()
115
+ if not device_id:
116
+ return "No Spotify device found. Open Spotify and start a device first."
117
+
118
+ if action == "pause":
119
+ try:
120
+ self.sp.pause_playback(device_id=device_id)
121
+ except Exception as exc:
122
+ return f"Could not pause playback: {exc}"
123
+ return "Paused playback."
124
+ if action == "resume":
125
+ try:
126
+ self.sp.start_playback(device_id=device_id)
127
+ except Exception as exc:
128
+ return f"Could not resume playback: {exc}"
129
+ return "Resumed playback."
130
+ return "Unknown playback action."
131
+
132
+ def skip_track(self) -> str:
133
+ device_id = self._active_device_id()
134
+ if not device_id:
135
+ return "No active Spotify device."
136
+ try:
137
+ self.sp.next_track(device_id=device_id)
138
+ except Exception as exc:
139
+ return f"Could not skip track: {exc}"
140
+ return "Skipped to next track."
141
+
142
+ def previous_track(self) -> str:
143
+ device_id = self._active_device_id()
144
+ if not device_id:
145
+ return "No active Spotify device."
146
+ try:
147
+ self.sp.previous_track(device_id=device_id)
148
+ except Exception as exc:
149
+ return f"Could not go to previous track: {exc}"
150
+ return "Went to previous track."
151
+
152
+ def set_volume(self, volume_percent: int) -> str:
153
+ volume_percent = max(0, min(100, int(volume_percent)))
154
+ device_id = self._active_device_id()
155
+ if not device_id:
156
+ return "No active Spotify device."
157
+ try:
158
+ self.sp.volume(volume_percent, device_id=device_id)
159
+ except Exception as exc:
160
+ return f"Could not set volume: {exc}"
161
+ return f"Volume set to {volume_percent}%."
162
+
163
+ def adjust_volume(self, delta: int) -> str:
164
+ playback = self._current_playback()
165
+ if not playback:
166
+ return "Nothing is playing, so I cannot adjust volume."
167
+ current = playback.get("device", {}).get("volume_percent")
168
+ if current is None:
169
+ return "Could not read current volume."
170
+ target = max(0, min(100, current + delta))
171
+ return self.set_volume(target)
File without changes
@@ -0,0 +1,126 @@
1
+ from rich import box
2
+ from rich.align import Align
3
+ from rich.console import Group
4
+ from rich.layout import Layout
5
+ from rich.panel import Panel
6
+ from rich.text import Text
7
+
8
+ from .theme import (
9
+ APP_LOGO,
10
+ APP_SUBTITLE,
11
+ APP_TITLE,
12
+ FOOTER_BORDER,
13
+ HEADER_BORDER,
14
+ HISTORY_BORDER,
15
+ STATUS_BORDER,
16
+ )
17
+
18
+
19
+ def _status_style(status: str) -> str:
20
+ lowered = status.lower()
21
+ if "error" in lowered or "failed" in lowered:
22
+ return "bold red3"
23
+ if "listening" in lowered or "wake word" in lowered:
24
+ return "bold yellow3"
25
+ if any(token in lowered for token in ("playing", "paused", "resumed", "volume", "skipped")):
26
+ return "bold spring_green3"
27
+ return "bold bright_white"
28
+
29
+
30
+ def generate_boot_banner() -> Panel:
31
+ logo = Text.from_markup(f"[spring_green3]{APP_LOGO}[/spring_green3]")
32
+ subtitle = Text.from_markup(f"[hot_pink2]{APP_SUBTITLE}[/hot_pink2]")
33
+ hints = Text.from_markup(
34
+ "[bright_cyan]Wake:[/] [white]'hey spotify'[/] "
35
+ "[bright_cyan]Try:[/] [white]play after hours[/], [white]volume 40[/], [white]next[/]"
36
+ )
37
+
38
+ return Panel(
39
+ Group(
40
+ Align.center(logo),
41
+ Align.center(subtitle),
42
+ Align.center(Text.from_markup("[grey74]Voice-first music terminal[/grey74]")),
43
+ Align.center(hints),
44
+ ),
45
+ title="[bold hot_pink2]BOOT SEQUENCE[/bold hot_pink2]",
46
+ subtitle="[bold white]ready for command capture[/bold white]",
47
+ border_style=HEADER_BORDER,
48
+ box=box.DOUBLE,
49
+ padding=(1, 2),
50
+ )
51
+
52
+
53
+ def generate_layout(current_song: str = "None", status: str = "Waiting...", last_command: str = "N/A") -> Layout:
54
+ """
55
+ Creates a full-screen layout for the hey-spotify CLI.
56
+ """
57
+ layout = Layout()
58
+ layout.split_column(
59
+ Layout(name="header", size=8),
60
+ Layout(name="body", ratio=1),
61
+ Layout(name="footer", size=3),
62
+ )
63
+
64
+ header_group = Group(
65
+ Align.center(Text.from_markup(f"[bold hot_pink2]{APP_TITLE}[/bold hot_pink2]")),
66
+ Align.center(Text.from_markup("[spring_green3]| ||| ||||| ||| |[/spring_green3]")),
67
+ Align.center(Text.from_markup("[grey70]Hands-free Spotify control in your terminal[/grey70]")),
68
+ )
69
+ layout["header"].update(
70
+ Panel(
71
+ header_group,
72
+ border_style=HEADER_BORDER,
73
+ box=box.HEAVY,
74
+ )
75
+ )
76
+
77
+ layout["body"].split_row(
78
+ Layout(name="status_panel", ratio=5),
79
+ Layout(name="log_panel", ratio=4),
80
+ )
81
+
82
+ playback_text = Text()
83
+ playback_text.append("NOW PLAYING\n", style="bold bright_white")
84
+ playback_text.append(f"{current_song}\n\n", style="spring_green3")
85
+ playback_text.append("SYSTEM STATUS\n", style="bold bright_white")
86
+ playback_text.append(status, style=_status_style(status))
87
+
88
+ layout["status_panel"].update(
89
+ Panel(
90
+ playback_text,
91
+ title="[bold spring_green3]Playback Deck[/bold spring_green3]",
92
+ border_style=STATUS_BORDER,
93
+ box=box.ROUNDED,
94
+ padding=(1, 2),
95
+ )
96
+ )
97
+
98
+ history_text = Text()
99
+ history_text.append("LAST HEARD\n", style="bold bright_white")
100
+ history_text.append(f"'{last_command}'\n\n", style="deep_sky_blue2")
101
+ history_text.append("COMMANDS\n", style="bold bright_white")
102
+ history_text.append("play <song>\n", style="white")
103
+ history_text.append("pause | resume\n", style="white")
104
+ history_text.append("next | previous\n", style="white")
105
+ history_text.append("volume up/down | volume 40\n", style="white")
106
+ history_text.append("now playing", style="white")
107
+
108
+ layout["log_panel"].update(
109
+ Panel(
110
+ history_text,
111
+ title="[bold deep_sky_blue2]Voice Log[/bold deep_sky_blue2]",
112
+ border_style=HISTORY_BORDER,
113
+ box=box.ROUNDED,
114
+ padding=(1, 2),
115
+ )
116
+ )
117
+
118
+ layout["footer"].update(
119
+ Panel(
120
+ "[bold yellow3]Wake:[/] 'hey spotify' [bold orchid]Exit:[/] Ctrl+C",
121
+ border_style=FOOTER_BORDER,
122
+ box=box.SQUARE,
123
+ )
124
+ )
125
+
126
+ return layout
@@ -0,0 +1,23 @@
1
+ APP_TITLE = "hey-spotify"
2
+ APP_SUBTITLE = "Voice Control for Spotify"
3
+
4
+ APP_LOGO = r"""
5
+ :'######::'########:::'#######::'########:'####:'########:'##:::'##:
6
+ '##... ##: ##.... ##:'##.... ##:... ##..::. ##:: ##.....::. ##:'##::
7
+ ##:::..:: ##:::: ##: ##:::: ##:::: ##::::: ##:: ##::::::::. ####:::
8
+ . ######:: ########:: ##:::: ##:::: ##::::: ##:: ######:::::. ##::::
9
+ :..... ##: ##.....::: ##:::: ##:::: ##::::: ##:: ##...::::::: ##::::
10
+ '##::: ##: ##:::::::: ##:::: ##:::: ##::::: ##:: ##:::::::::: ##::::
11
+ . ######:: ##::::::::. #######::::: ##::::'####: ##:::::::::: ##::::
12
+ :......:::..::::::::::.......::::::..:::::....::..:::::::::::..:::::
13
+ """
14
+
15
+ ACCENT = "bright_cyan"
16
+ SUCCESS = "spring_green3"
17
+ WARNING = "yellow3"
18
+ ERROR = "red3"
19
+
20
+ HEADER_BORDER = "hot_pink2"
21
+ STATUS_BORDER = "spring_green3"
22
+ HISTORY_BORDER = "deep_sky_blue3"
23
+ FOOTER_BORDER = "orchid"
@@ -0,0 +1,51 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Union
4
+
5
+
6
+ @dataclass
7
+ class ParsedCommand:
8
+ action: str
9
+ value: Optional[Union[str, int]] = None
10
+
11
+
12
+ def parse_voice_command(raw_command: str) -> ParsedCommand:
13
+ command = raw_command.strip().lower()
14
+ if not command:
15
+ return ParsedCommand(action="unknown")
16
+
17
+ if command.startswith("play "):
18
+ query = command.replace("play", "", 1).strip()
19
+ return ParsedCommand(action="play", value=query)
20
+ if command == "play":
21
+ return ParsedCommand(action="play", value="")
22
+
23
+ if any(word in command for word in ("exit", "quit", "goodbye")):
24
+ return ParsedCommand(action="exit")
25
+
26
+ if any(word in command for word in ("pause", "stop")):
27
+ return ParsedCommand(action="pause")
28
+
29
+ if any(word in command for word in ("resume", "continue", "unpause")):
30
+ return ParsedCommand(action="resume")
31
+
32
+ if any(word in command for word in ("next", "skip")):
33
+ return ParsedCommand(action="next")
34
+
35
+ if any(word in command for word in ("previous", "go back", "back track")):
36
+ return ParsedCommand(action="previous")
37
+
38
+ if "volume up" in command or "louder" in command:
39
+ return ParsedCommand(action="volume_delta", value=10)
40
+
41
+ if "volume down" in command or "quieter" in command:
42
+ return ParsedCommand(action="volume_delta", value=-10)
43
+
44
+ volume_match = re.search(r"(?:set )?volume(?: to)?\s+(\d{1,3})", command)
45
+ if volume_match:
46
+ return ParsedCommand(action="volume_set", value=int(volume_match.group(1)))
47
+
48
+ if any(word in command for word in ("what is playing", "now playing", "current song")):
49
+ return ParsedCommand(action="now_playing")
50
+
51
+ return ParsedCommand(action="unknown")
@@ -0,0 +1,183 @@
1
+ import os
2
+ import re
3
+ from difflib import SequenceMatcher
4
+ from typing import Optional
5
+
6
+ try:
7
+ import speech_recognition as sr
8
+ except ModuleNotFoundError:
9
+ sr = None
10
+
11
+
12
+ class VoiceEngine:
13
+ def __init__(
14
+ self,
15
+ microphone_index: Optional[int] = None,
16
+ debug: bool = False,
17
+ language: Optional[str] = None,
18
+ ) -> None:
19
+ if sr is None:
20
+ raise RuntimeError(
21
+ "Missing dependency 'SpeechRecognition'. Run: pip install -r requirements.txt"
22
+ )
23
+
24
+ self.debug = debug
25
+ self.language = (language or os.getenv("VOICE_LANGUAGE") or "en-US").strip() or "en-US"
26
+ self.wake_words = (
27
+ "hey spotify",
28
+ "hello spotify",
29
+ "ok spotify",
30
+ "okay spotify",
31
+ "hey spotty",
32
+ )
33
+
34
+ mic_names = self.list_microphones()
35
+ if not mic_names:
36
+ raise RuntimeError(
37
+ "No input device detected. Connect a microphone, grant terminal mic access, "
38
+ "then run: python main.py --list-mics"
39
+ )
40
+
41
+ if microphone_index is None:
42
+ env_index = os.getenv("MIC_DEVICE_INDEX")
43
+ if env_index:
44
+ try:
45
+ microphone_index = int(env_index)
46
+ except ValueError as exc:
47
+ raise RuntimeError(
48
+ f"Invalid MIC_DEVICE_INDEX='{env_index}'. It must be an integer."
49
+ ) from exc
50
+ else:
51
+ microphone_index = self._choose_default_mic_index(mic_names)
52
+
53
+ if microphone_index is not None:
54
+ if microphone_index < 0 or microphone_index >= len(mic_names):
55
+ raise RuntimeError(
56
+ f"Invalid microphone index {microphone_index}. Run: python main.py --list-mics"
57
+ )
58
+
59
+ self.recognizer = sr.Recognizer()
60
+ self.recognizer.dynamic_energy_threshold = True
61
+ self.recognizer.pause_threshold = 0.8
62
+ self.recognizer.non_speaking_duration = 0.5
63
+ self.recognizer.phrase_threshold = 0.25
64
+
65
+ selected_index = microphone_index
66
+ self.selected_mic_name = (
67
+ mic_names[selected_index] if selected_index is not None else "system default"
68
+ )
69
+
70
+ try:
71
+ self.microphone = sr.Microphone(device_index=selected_index)
72
+ except Exception as exc:
73
+ raise RuntimeError(f"No usable microphone found: {exc}") from exc
74
+
75
+ with self.microphone as source:
76
+ self.recognizer.adjust_for_ambient_noise(source, duration=1)
77
+
78
+ @staticmethod
79
+ def list_microphones() -> list[str]:
80
+ if sr is None:
81
+ return []
82
+ try:
83
+ return sr.Microphone.list_microphone_names()
84
+ except Exception:
85
+ return []
86
+
87
+ @staticmethod
88
+ def _choose_default_mic_index(mic_names: list[str]) -> int:
89
+ ranked_keywords = ("microphone", "mic", "input", "headset", "airpods")
90
+ for index, name in enumerate(mic_names):
91
+ lowered = name.lower()
92
+ if any(keyword in lowered for keyword in ranked_keywords):
93
+ return index
94
+ return 0
95
+
96
+ @staticmethod
97
+ def _normalize(text: str) -> str:
98
+ cleaned = re.sub(r"[^a-z0-9\s]", " ", text.lower())
99
+ return " ".join(cleaned.split())
100
+
101
+ @staticmethod
102
+ def _is_spotify_token(token: str) -> bool:
103
+ known_aliases = {"spotify", "spotty", "spotifly", "spotifi", "spotfy"}
104
+ if token in known_aliases:
105
+ return True
106
+ return SequenceMatcher(None, token, "spotify").ratio() >= 0.72
107
+
108
+ def _extract_wake_remainder(self, transcript: str) -> tuple[bool, Optional[str]]:
109
+ normalized = self._normalize(transcript)
110
+ if not normalized:
111
+ return False, None
112
+
113
+ for wake_word in self.wake_words:
114
+ wake_normalized = self._normalize(wake_word)
115
+ if wake_normalized in normalized:
116
+ remainder = normalized.split(wake_normalized, 1)[1].strip()
117
+ return True, remainder or None
118
+
119
+ tokens = normalized.split()
120
+ if not tokens:
121
+ return False, None
122
+
123
+ greeting_tokens = {"hey", "hello", "hi", "ok", "okay"}
124
+ spotify_index: Optional[int] = None
125
+ for index, token in enumerate(tokens):
126
+ if self._is_spotify_token(token):
127
+ spotify_index = index
128
+ break
129
+
130
+ if spotify_index is None:
131
+ return False, None
132
+
133
+ has_greeting = any(token in greeting_tokens for token in tokens[: spotify_index + 1])
134
+ starts_with_spotify = spotify_index == 0
135
+ if not has_greeting and not starts_with_spotify:
136
+ return False, None
137
+
138
+ remainder_tokens = tokens[spotify_index + 1 :]
139
+ remainder = " ".join(remainder_tokens).strip()
140
+ return True, remainder or None
141
+
142
+ def _transcribe_once(
143
+ self,
144
+ timeout: Optional[float] = None,
145
+ phrase_time_limit: float = 4,
146
+ ) -> Optional[str]:
147
+ with self.microphone as source:
148
+ try:
149
+ audio = self.recognizer.listen(
150
+ source,
151
+ timeout=timeout,
152
+ phrase_time_limit=phrase_time_limit,
153
+ )
154
+ except sr.WaitTimeoutError:
155
+ return None
156
+
157
+ try:
158
+ text = self.recognizer.recognize_google(audio, language=self.language)
159
+ except sr.UnknownValueError:
160
+ return None
161
+ except sr.RequestError:
162
+ return None
163
+
164
+ return text.lower().strip()
165
+
166
+ def listen_for_wake_event(self) -> tuple[bool, Optional[str]]:
167
+ transcript = self._transcribe_once(timeout=None, phrase_time_limit=4.5)
168
+ if self.debug and transcript:
169
+ print(f"[voice-debug] wake-listen heard: {transcript}")
170
+ if not transcript:
171
+ return False, None
172
+
173
+ return self._extract_wake_remainder(transcript)
174
+
175
+ def listen_for_wake_word(self) -> bool:
176
+ detected, _ = self.listen_for_wake_event()
177
+ return detected
178
+
179
+ def get_command(self) -> Optional[str]:
180
+ transcript = self._transcribe_once(timeout=5, phrase_time_limit=8)
181
+ if self.debug and transcript:
182
+ print(f"[voice-debug] command heard: {transcript}")
183
+ return transcript
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: hey-spotify-cli
3
+ Version: 1.0.0
4
+ Summary: A premium, voice-controlled terminal interface for Spotify playback.
5
+ Author-email: Prayag Tushar <prayagtushar@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/prayagtushar/hey-spotify-cli
8
+ Project-URL: Documentation, https://prayagtushar.github.io/hey-spotify-cli/
9
+ Project-URL: Repository, https://github.com/prayagtushar/hey-spotify-cli.git
10
+ Project-URL: BugTracker, https://github.com/prayagtushar/hey-spotify-cli/issues
11
+ Keywords: spotify,cli,voice-control,voice-command,hands-free,music
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Players
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: License :: OSI Approved :: MIT License
24
+ Classifier: Operating System :: OS Independent
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: spotipy>=2.24.0
29
+ Requires-Dist: python-dotenv>=1.0.1
30
+ Requires-Dist: PyAudio>=0.2.14
31
+ Requires-Dist: SpeechRecognition>=3.10.4
32
+ Requires-Dist: rich>=13.7.1
33
+ Dynamic: license-file
34
+
35
+ # hey-spotify 🎤 🎵
36
+
37
+ [![PyPI version](https://img.shields.io/pypi/v/hey-spotify-cli.svg)](https://pypi.org/project/hey-spotify-cli/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ A premium, voice-controlled terminal interface for Spotify playback. Control your music without leaving your terminal or touching your keyboard.
41
+
42
+ ## 📋 Prerequisites
43
+
44
+ - **Spotify Premium**: A Premium subscription is **required** for playback control via the Spotify API.
45
+ - **Python 3.8+**: Ensure you have a modern Python version installed.
46
+
47
+ ## 🚀 Quick Start
48
+
49
+ ### 1. Install
50
+ Install the package directly via pip:
51
+
52
+ ```bash
53
+ pip install hey-spotify-cli
54
+ ```
55
+
56
+ *Note: Depending on your OS, you might need to install system dependencies for `PyAudio` (e.g., `portaudio` on macOS or `python3-pyaudio` on Linux).*
57
+
58
+ ### 2. Spotify API Setup
59
+ 1. Go to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
60
+ 2. Create a new app and name it (e.g., `hey-spotify`).
61
+ 3. Add `http://127.0.0.1:8888/callback` as a **Redirect URI** in your app settings.
62
+ 4. Set up your environment variables (see [Environment Variables](#environment-variables) section).
63
+
64
+ ### 3. Run
65
+ Launch the CLI:
66
+
67
+ ```bash
68
+ hey-spotify
69
+ ```
70
+
71
+ On the first run, it will open your browser for authentication.
72
+
73
+ ## 🎙️ Voice Commands
74
+
75
+ Once active, just say **"Hey Spotify"** followed by:
76
+
77
+ - `play <song name>` — Plays a specific track.
78
+ - `pause` / `stop` — Pauses current playback.
79
+ - `resume` / `continue` — Resumes playback.
80
+ - `next` / `skip` — Skips to the next track.
81
+ - `previous` — Goes back to the previous track.
82
+ - `volume <0-100>` — Sets volume to a specific level.
83
+ - `volume up` / `volume down` — Adjusts volume by 10%.
84
+ - `now playing` — Shows information about the current track.
85
+ - `exit` / `quit` — Closes the application.
86
+
87
+ ## ⚙️ Configuration
88
+
89
+ ### Environment Variables
90
+ You can create a `.env` file in your working directory or set them in your shell:
91
+
92
+ ```bash
93
+ SPOTIPY_CLIENT_ID='your_client_id'
94
+ SPOTIPY_CLIENT_SECRET='your_client_secret'
95
+ SPOTIPY_REDIRECT_URI='http://127.0.0.1:8888/callback'
96
+ ```
97
+
98
+ ### CLI Options
99
+ - `--list-mics`: List all detected microphone devices.
100
+ - `--mic-index <index>`: Use a non-default microphone.
101
+ - `--no-wake-word`: Disable "Hey Spotify" trigger and listen continuously.
102
+ - `--language <lang>`: Change recognition language (e.g., `en-US`, `es-ES`).
103
+ - `--voice-debug`: See what the engine is hearing in real-time.
104
+
105
+ ## 🛠️ Troubleshooting
106
+ - **Microphone not found**: Ensure you've granted terminal permissions for the microphone and use `--list-mics` to find the correct index.
107
+ - **Spotify Authentication**: Ensure your Redirect URI in the Dashboard exactly matches the one in your environment.
108
+
109
+ ## 📄 License
110
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ hey_spotify/__init__.py
5
+ hey_spotify/__main__.py
6
+ hey_spotify/py.typed
7
+ hey_spotify/spotify_client.py
8
+ hey_spotify/utils.py
9
+ hey_spotify/voice_engine.py
10
+ hey_spotify/ui/__init__.py
11
+ hey_spotify/ui/cli.py
12
+ hey_spotify/ui/theme.py
13
+ hey_spotify_cli.egg-info/PKG-INFO
14
+ hey_spotify_cli.egg-info/SOURCES.txt
15
+ hey_spotify_cli.egg-info/dependency_links.txt
16
+ hey_spotify_cli.egg-info/entry_points.txt
17
+ hey_spotify_cli.egg-info/requires.txt
18
+ hey_spotify_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hey-spotify = hey_spotify.__main__:cli
@@ -0,0 +1,5 @@
1
+ spotipy>=2.24.0
2
+ python-dotenv>=1.0.1
3
+ PyAudio>=0.2.14
4
+ SpeechRecognition>=3.10.4
5
+ rich>=13.7.1
@@ -0,0 +1 @@
1
+ hey_spotify
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hey-spotify-cli"
7
+ version = "1.0.0"
8
+ authors = [
9
+ { name="Prayag Tushar", email="prayagtushar@gmail.com" },
10
+ ]
11
+ description = "A premium, voice-controlled terminal interface for Spotify playback."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ keywords = ["spotify", "cli", "voice-control", "voice-command", "hands-free", "music"]
15
+ license = { text = "MIT" }
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Environment :: Console",
19
+ "Intended Audience :: End Users/Desktop",
20
+ "Intended Audience :: Developers",
21
+ "Topic :: Multimedia :: Sound/Audio :: Players",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.8",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ ]
31
+ dependencies = [
32
+ "spotipy>=2.24.0",
33
+ "python-dotenv>=1.0.1",
34
+ "PyAudio>=0.2.14",
35
+ "SpeechRecognition>=3.10.4",
36
+ "rich>=13.7.1",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/prayagtushar/hey-spotify-cli"
41
+ Documentation = "https://prayagtushar.github.io/hey-spotify-cli/"
42
+ Repository = "https://github.com/prayagtushar/hey-spotify-cli.git"
43
+ BugTracker = "https://github.com/prayagtushar/hey-spotify-cli/issues"
44
+
45
+ [project.scripts]
46
+ hey-spotify = "hey_spotify.__main__:cli"
47
+
48
+ [tool.setuptools]
49
+ packages = ["hey_spotify", "hey_spotify.ui"]
50
+
51
+ [tool.setuptools.package-data]
52
+ hey_spotify = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+