coder-music-cli 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.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: coder-music-cli
3
+ Version: 0.1.0
4
+ Summary: A command-line music application for coders with daemon support, radio streaming, and AI-generated music
5
+ Author-email: Luong Nguyen <luongnv89@gmail.com>
6
+ Maintainer-email: Luong Nguyen <luongnv89@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/luongnv89/music-cli
9
+ Project-URL: Repository, https://github.com/luongnv89/music-cli
10
+ Project-URL: Documentation, https://github.com/luongnv89/music-cli#readme
11
+ Project-URL: Issues, https://github.com/luongnv89/music-cli/issues
12
+ Project-URL: Changelog, https://github.com/luongnv89/music-cli/releases
13
+ Keywords: music,cli,daemon,radio,ai-music,focus,coding,background-music,lofi,productivity
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Players
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: click>=8.0
29
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
30
+ Requires-Dist: tomli-w>=1.0
31
+ Provides-Extra: ai
32
+ Requires-Dist: torch>=2.0; extra == "ai"
33
+ Requires-Dist: transformers>=4.30; extra == "ai"
34
+ Requires-Dist: audiocraft>=1.0; extra == "ai"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7.0; extra == "dev"
37
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
38
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
39
+ Requires-Dist: black>=23.0; extra == "dev"
40
+ Requires-Dist: ruff>=0.1; extra == "dev"
41
+ Requires-Dist: mypy>=1.0; extra == "dev"
42
+ Requires-Dist: bandit>=1.7; extra == "dev"
43
+ Requires-Dist: pre-commit>=3.0; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # music-cli
47
+
48
+ [![PyPI version](https://badge.fury.io/py/coder-music-cli.svg)](https://badge.fury.io/py/coder-music-cli)
49
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
+
52
+ A command-line music player for coders. Background daemon with radio streaming, local MP3s, and AI-generated music.
53
+
54
+ ```bash
55
+ music-cli play --mood focus # Start focus music
56
+ music-cli pause # Pause for meeting
57
+ music-cli resume # Back to coding
58
+ ```
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ # Install from PyPI
64
+ pip install coder-music-cli
65
+
66
+ # Or with uv (faster)
67
+ uv pip install coder-music-cli
68
+
69
+ # Install FFmpeg (required)
70
+ brew install ffmpeg # macOS
71
+ sudo apt install ffmpeg # Ubuntu/Debian
72
+ ```
73
+
74
+ ### Optional: AI Music Generation
75
+
76
+ ```bash
77
+ pip install 'coder-music-cli[ai]' # ~5GB (PyTorch + MusicGen)
78
+ ```
79
+
80
+ ## Features
81
+
82
+ - **Daemon-based** - Persistent background playback
83
+ - **Multiple sources** - Local files, radio streams, AI generation
84
+ - **Context-aware** - Selects music based on time of day and mood
85
+ - **Simple config** - Human-readable text files
86
+
87
+ ## Quick Start
88
+
89
+ ```bash
90
+ # Play
91
+ music-cli play # Context-aware radio
92
+ music-cli play --mood focus # Focus music
93
+ music-cli play -m local --auto # Shuffle local library
94
+
95
+ # Control
96
+ music-cli pause | resume | stop | status
97
+ ```
98
+
99
+ ## Commands
100
+
101
+ | Command | Description |
102
+ |---------|-------------|
103
+ | `play` | Start playing (radio/local/ai/history) |
104
+ | `stop` / `pause` / `resume` | Playback control |
105
+ | `status` | Current track and state |
106
+ | `next` | Skip track (auto-play mode) |
107
+ | `volume [0-100]` | Get/set volume |
108
+ | `radios` | List stations |
109
+ | `history` | Playback log |
110
+ | `moods` | Available mood tags |
111
+ | `daemon start\|stop\|status` | Daemon control |
112
+
113
+ ## Play Modes
114
+
115
+ ```bash
116
+ # Radio (default)
117
+ music-cli play # Time-based selection
118
+ music-cli play -s "deep house" # By station name
119
+ music-cli play --mood focus # By mood
120
+
121
+ # Local
122
+ music-cli play -m local -s song.mp3
123
+ music-cli play -m local --auto # Shuffle
124
+
125
+ # AI (requires [ai] extras)
126
+ music-cli play -m ai --mood happy -d 60
127
+
128
+ # History
129
+ music-cli play -m history -i 3 # Replay item #3
130
+ ```
131
+
132
+ ## Moods
133
+
134
+ `focus` `happy` `sad` `excited` `relaxed` `energetic` `melancholic` `peaceful`
135
+
136
+ ## Configuration
137
+
138
+ Files in `~/.config/music-cli/`:
139
+
140
+ | File | Purpose |
141
+ |------|---------|
142
+ | `config.toml` | Settings |
143
+ | `radios.txt` | Station URLs |
144
+ | `history.jsonl` | Play history |
145
+
146
+ Add stations to `radios.txt`:
147
+ ```
148
+ ChillHop|https://streams.example.com/chillhop.mp3
149
+ Jazz FM|https://streams.example.com/jazz.mp3
150
+ ```
151
+
152
+ ## Documentation
153
+
154
+ | Document | Description |
155
+ |----------|-------------|
156
+ | [User Guide](docs/user-guide.md) | Complete usage instructions |
157
+ | [Architecture](docs/architecture.md) | System design and diagrams |
158
+ | [Development](docs/development.md) | Contributing guide |
159
+
160
+ ## Requirements
161
+
162
+ - Python 3.9+
163
+ - FFmpeg
164
+
165
+ ## License
166
+
167
+ MIT
@@ -0,0 +1,23 @@
1
+ coder_music_cli-0.1.0.dist-info/licenses/LICENSE,sha256=iIY9i1SGhK_mpAtd2Nzdd67baxXIjlaqJiaJFL2d5o0,1069
2
+ music_cli/__init__.py,sha256=PCuhKa5Q9i4mQGNtS4eRZCuxBYz4BBX-LO4QSSP2s_o,85
3
+ music_cli/__main__.py,sha256=7GY6AI64Lg6R1c7yVpaYwP1U9LbnZPGXC1ROveNnuAg,115
4
+ music_cli/cli.py,sha256=fMWJ784KNvpwkI_k4wGb3Fd7sVDUWoVbUvyxEJ6d70s,10622
5
+ music_cli/client.py,sha256=9a_BEsvp1GRyQ6if_Tv_bwV4NBP08DXLTdo-LPN2wro,4999
6
+ music_cli/config.py,sha256=wbwDng-C4gDi3B1v-ZKvg3M-8syv2OjXHM-d8vMgVqc,5947
7
+ music_cli/daemon.py,sha256=2kVqsY8DTJ6DhUC0tWDMmTakdV1kT4DgkhEWXEEflD0,12327
8
+ music_cli/history.py,sha256=2v5dDMbnXuBbOhf2Ek2dD7Im2oS5R1L5hyNIz6tQp7o,5269
9
+ music_cli/context/__init__.py,sha256=uQvlGLyFxzJpqjhTf0oCbVd42bl_Ll0ytJgQqbIR7I0,166
10
+ music_cli/context/mood.py,sha256=iQhwIgwqRNDRsriZz_ILp5D9JLt__yZvTQPHIQwglCg,4104
11
+ music_cli/context/temporal.py,sha256=zWlOLg0zhF3yWETKEtmUYLw2hUqVMFxJzrrkZWy5VH8,4774
12
+ music_cli/player/__init__.py,sha256=EKI00MbZeug1ZtS0Ma3Ze-DrqCb0eNtSq8pghDAFyl8,160
13
+ music_cli/player/base.py,sha256=1DMk6p3xy3ghXx337b31bW2TmLrU14YbcTW8HaX35Uw,2951
14
+ music_cli/player/ffplay.py,sha256=H0jYK2-JAuua8dl4Ngh0tLJHbQf0_n0dErXT5biXTEI,5739
15
+ music_cli/sources/__init__.py,sha256=abbtRMuDv4hqXKS_lzSNXU7XE-w3_FgPYBeQ9VIBNzk,147
16
+ music_cli/sources/ai_generator.py,sha256=MD3zDwJNrC8kqsOaMBaHaDc02U-35QrrdailEz5bkF0,6342
17
+ music_cli/sources/local.py,sha256=dMVF7YioGVf_9ZueXx-6ka2MxLr-vqUBwFVZ684_llM,2218
18
+ music_cli/sources/radio.py,sha256=cy28BIQALOqWOveR1XajiixJ4_23Co96ft5MeKJMxbw,2924
19
+ coder_music_cli-0.1.0.dist-info/METADATA,sha256=xP8ETriaKuhY9rbtmU-exU_bd5M171fKOpVO-DLsCuY,5035
20
+ coder_music_cli-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ coder_music_cli-0.1.0.dist-info/entry_points.txt,sha256=JD1XHz39p9-26ucEEUxNIkFGzDoKaylWyWGVLgjy3z0,49
22
+ coder_music_cli-0.1.0.dist-info/top_level.txt,sha256=cdu7SF6N7YSi0428x8CBLwHVguKCpyKhvVDV9WRqnfo,10
23
+ coder_music_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ music-cli = music_cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Luong Nguyen
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 @@
1
+ music_cli
music_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """music-cli: A command-line music application for coders."""
2
+
3
+ __version__ = "0.1.0"
music_cli/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for running music-cli as a module."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
music_cli/cli.py ADDED
@@ -0,0 +1,378 @@
1
+ """Command-line interface for music-cli."""
2
+
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ import sys
7
+ import time
8
+
9
+ import click
10
+
11
+ from . import __version__
12
+ from .client import DaemonClient
13
+ from .config import get_config
14
+ from .daemon import get_daemon_pid, is_daemon_running
15
+ from .player.ffplay import check_ffplay_available
16
+
17
+
18
+ def ensure_daemon() -> DaemonClient:
19
+ """Ensure daemon is running and return client."""
20
+ if not is_daemon_running():
21
+ click.echo("Starting daemon...", err=True)
22
+ start_daemon_background()
23
+ # Wait a bit for daemon to start
24
+ for _ in range(10):
25
+ time.sleep(0.2)
26
+ if is_daemon_running():
27
+ break
28
+ else:
29
+ click.echo("Failed to start daemon", err=True)
30
+ sys.exit(1)
31
+
32
+ return DaemonClient()
33
+
34
+
35
+ def start_daemon_background() -> None:
36
+ """Start the daemon in background."""
37
+ # Start daemon as subprocess
38
+ python = sys.executable
39
+ subprocess.Popen(
40
+ [python, "-m", "music_cli.daemon"],
41
+ stdout=subprocess.DEVNULL,
42
+ stderr=subprocess.DEVNULL,
43
+ start_new_session=True,
44
+ )
45
+
46
+
47
+ @click.group()
48
+ @click.version_option(__version__)
49
+ def main():
50
+ """music-cli: A command-line music player for coders.
51
+
52
+ Play local MP3s, stream radio, or generate AI music based on your mood
53
+ and the time of day.
54
+ """
55
+ pass
56
+
57
+
58
+ @main.command()
59
+ @click.option(
60
+ "--mode",
61
+ "-m",
62
+ type=click.Choice(["local", "radio", "ai", "context", "history"]),
63
+ default="radio",
64
+ help="Playback mode",
65
+ )
66
+ @click.option("--source", "-s", help="Source file/URL/station name")
67
+ @click.option(
68
+ "--mood",
69
+ type=click.Choice(["happy", "sad", "excited", "focus", "relaxed", "energetic"]),
70
+ help="Mood for context-aware playback",
71
+ )
72
+ @click.option("--auto", "-a", is_flag=True, help="Enable auto-play (shuffle local files)")
73
+ @click.option("--duration", "-d", default=30, help="Duration for AI generation (seconds)")
74
+ @click.option("--index", "-i", type=int, help="History entry index to replay")
75
+ def play(mode, source, mood, auto, duration, index):
76
+ """Start playing music.
77
+
78
+ \b
79
+ Examples:
80
+ music-cli play # Play context-aware radio
81
+ music-cli play -m local -s song.mp3 # Play local file
82
+ music-cli play -m radio -s "chill" # Play radio station by name
83
+ music-cli play --mood focus # Play focus music
84
+ music-cli play -m ai --mood happy # Generate happy AI music
85
+ music-cli play -m history -i 3 # Replay 3rd item from history
86
+ music-cli play -m local --auto # Shuffle local library
87
+ """
88
+ if not check_ffplay_available():
89
+ click.echo("Error: ffplay not found. Please install FFmpeg.", err=True)
90
+ click.echo(" macOS: brew install ffmpeg", err=True)
91
+ click.echo(" Linux: apt install ffmpeg", err=True)
92
+ sys.exit(1)
93
+
94
+ client = ensure_daemon()
95
+
96
+ try:
97
+ response = client.play(
98
+ mode=mode,
99
+ source=source,
100
+ mood=mood,
101
+ auto=auto,
102
+ duration=duration,
103
+ index=index,
104
+ )
105
+
106
+ if "error" in response:
107
+ click.echo(f"Error: {response['error']}", err=True)
108
+ sys.exit(1)
109
+
110
+ track = response.get("track", {})
111
+ title = track.get("title", track.get("source", "Unknown"))
112
+ source_type = track.get("source_type", "unknown")
113
+
114
+ click.echo(f"▶ Playing: {title} [{source_type}]")
115
+ if auto:
116
+ click.echo(" Auto-play enabled (shuffle mode)")
117
+
118
+ except ConnectionError as e:
119
+ click.echo(f"Error: {e}", err=True)
120
+ sys.exit(1)
121
+
122
+
123
+ @main.command()
124
+ def stop():
125
+ """Stop playback."""
126
+ client = ensure_daemon()
127
+
128
+ try:
129
+ response = client.stop()
130
+ if "error" in response:
131
+ click.echo(f"Error: {response['error']}", err=True)
132
+ else:
133
+ click.echo("⏹ Stopped")
134
+ except ConnectionError as e:
135
+ click.echo(f"Error: {e}", err=True)
136
+ sys.exit(1)
137
+
138
+
139
+ @main.command()
140
+ def pause():
141
+ """Pause playback."""
142
+ client = ensure_daemon()
143
+
144
+ try:
145
+ response = client.pause()
146
+ if "error" in response:
147
+ click.echo(f"Error: {response['error']}", err=True)
148
+ else:
149
+ click.echo("⏸ Paused")
150
+ except ConnectionError as e:
151
+ click.echo(f"Error: {e}", err=True)
152
+ sys.exit(1)
153
+
154
+
155
+ @main.command()
156
+ def resume():
157
+ """Resume playback."""
158
+ client = ensure_daemon()
159
+
160
+ try:
161
+ response = client.resume()
162
+ if "error" in response:
163
+ click.echo(f"Error: {response['error']}", err=True)
164
+ else:
165
+ click.echo("▶ Resumed")
166
+ except ConnectionError as e:
167
+ click.echo(f"Error: {e}", err=True)
168
+ sys.exit(1)
169
+
170
+
171
+ @main.command()
172
+ def status():
173
+ """Show current playback status."""
174
+ client = ensure_daemon()
175
+
176
+ try:
177
+ response = client.status()
178
+
179
+ if "error" in response:
180
+ click.echo(f"Error: {response['error']}", err=True)
181
+ sys.exit(1)
182
+
183
+ state = response.get("state", "unknown")
184
+ state_icons = {
185
+ "playing": "▶",
186
+ "paused": "⏸",
187
+ "stopped": "⏹",
188
+ "loading": "⏳",
189
+ "error": "❌",
190
+ }
191
+
192
+ click.echo(f"Status: {state_icons.get(state, '?')} {state}")
193
+
194
+ track = response.get("track")
195
+ if track:
196
+ title = track.get("title", track.get("source", "Unknown"))
197
+ source_type = track.get("source_type", "unknown")
198
+ click.echo(f"Track: {title} [{source_type}]")
199
+
200
+ volume = response.get("volume", 80)
201
+ click.echo(f"Volume: {volume}%")
202
+
203
+ if response.get("auto_play"):
204
+ click.echo("Auto-play: enabled")
205
+
206
+ mood = response.get("mood")
207
+ if mood:
208
+ click.echo(f"Mood: {mood}")
209
+
210
+ context = response.get("context", {})
211
+ time_period = context.get("time_period", "")
212
+ if time_period:
213
+ click.echo(f"Context: {time_period} / {context.get('day_type', '')}")
214
+
215
+ except ConnectionError as e:
216
+ click.echo(f"Error: {e}", err=True)
217
+ sys.exit(1)
218
+
219
+
220
+ @main.command("next")
221
+ def next_track():
222
+ """Skip to next track (auto-play mode only)."""
223
+ client = ensure_daemon()
224
+
225
+ try:
226
+ response = client.next_track()
227
+ if "error" in response:
228
+ click.echo(f"Error: {response['error']}", err=True)
229
+ else:
230
+ click.echo("⏭ Skipped to next track")
231
+ except ConnectionError as e:
232
+ click.echo(f"Error: {e}", err=True)
233
+ sys.exit(1)
234
+
235
+
236
+ @main.command()
237
+ @click.argument("level", type=int, required=False)
238
+ def volume(level):
239
+ """Get or set volume (0-100).
240
+
241
+ \b
242
+ Examples:
243
+ music-cli volume # Show current volume
244
+ music-cli volume 50 # Set volume to 50%
245
+ """
246
+ client = ensure_daemon()
247
+
248
+ try:
249
+ if level is not None:
250
+ response = client.set_volume(level)
251
+ click.echo(f"Volume: {response.get('volume', level)}%")
252
+ else:
253
+ vol = client.get_volume()
254
+ click.echo(f"Volume: {vol}%")
255
+ except ConnectionError as e:
256
+ click.echo(f"Error: {e}", err=True)
257
+ sys.exit(1)
258
+
259
+
260
+ @main.command("radios")
261
+ def list_radios():
262
+ """List available radio stations."""
263
+ client = ensure_daemon()
264
+
265
+ try:
266
+ stations = client.list_radios()
267
+
268
+ if not stations:
269
+ config = get_config()
270
+ click.echo(f"No stations configured. Add stations to: {config.radios_file}")
271
+ return
272
+
273
+ click.echo("Available radio stations:")
274
+ for station in stations:
275
+ click.echo(f" {station['index']}. {station['name']}")
276
+
277
+ except ConnectionError as e:
278
+ click.echo(f"Error: {e}", err=True)
279
+ sys.exit(1)
280
+
281
+
282
+ @main.command("history")
283
+ @click.option("--limit", "-n", default=20, help="Number of entries to show")
284
+ def list_history(limit):
285
+ """Show playback history."""
286
+ client = ensure_daemon()
287
+
288
+ try:
289
+ history = client.list_history(limit=limit)
290
+
291
+ if not history:
292
+ click.echo("No playback history yet.")
293
+ return
294
+
295
+ click.echo("Recent playback history:")
296
+ for entry in history:
297
+ idx = entry.get("index", "?")
298
+ title = entry.get("title") or entry.get("source", "Unknown")[:40]
299
+ source_type = entry.get("source_type", "?")
300
+ timestamp = entry.get("timestamp", "")[:16] # Truncate to date/time
301
+ click.echo(f" {idx}. [{timestamp}] {title} ({source_type})")
302
+
303
+ click.echo("\nReplay with: music-cli play -m history -i <number>")
304
+
305
+ except ConnectionError as e:
306
+ click.echo(f"Error: {e}", err=True)
307
+ sys.exit(1)
308
+
309
+
310
+ @main.command("daemon")
311
+ @click.argument("action", type=click.Choice(["start", "stop", "restart", "status"]))
312
+ def daemon_control(action):
313
+ """Control the background daemon.
314
+
315
+ \b
316
+ Actions:
317
+ start - Start the daemon
318
+ stop - Stop the daemon
319
+ restart - Restart the daemon
320
+ status - Check daemon status
321
+ """
322
+ if action == "status":
323
+ pid = get_daemon_pid()
324
+ if pid:
325
+ click.echo(f"Daemon is running (PID: {pid})")
326
+ else:
327
+ click.echo("Daemon is not running")
328
+
329
+ elif action == "start":
330
+ if is_daemon_running():
331
+ click.echo("Daemon is already running")
332
+ else:
333
+ start_daemon_background()
334
+ click.echo("Daemon started")
335
+
336
+ elif action == "stop":
337
+ pid = get_daemon_pid()
338
+ if pid:
339
+ os.kill(pid, signal.SIGTERM)
340
+ click.echo("Daemon stopped")
341
+ else:
342
+ click.echo("Daemon is not running")
343
+
344
+ elif action == "restart":
345
+ pid = get_daemon_pid()
346
+ if pid:
347
+ os.kill(pid, signal.SIGTERM)
348
+ time.sleep(0.5)
349
+ start_daemon_background()
350
+ click.echo("Daemon restarted")
351
+
352
+
353
+ @main.command("config")
354
+ def show_config():
355
+ """Show configuration file locations."""
356
+ config = get_config()
357
+
358
+ click.echo("Configuration files:")
359
+ click.echo(f" Config: {config.config_file}")
360
+ click.echo(f" Radios: {config.radios_file}")
361
+ click.echo(f" History: {config.history_file}")
362
+ click.echo(f" Socket: {config.socket_path}")
363
+ click.echo(f" PID: {config.pid_file}")
364
+
365
+
366
+ @main.command("moods")
367
+ def list_moods():
368
+ """List available mood tags."""
369
+ from .context.mood import MoodContext
370
+
371
+ click.echo("Available moods:")
372
+ for mood in MoodContext.get_all_moods():
373
+ click.echo(f" - {mood}")
374
+ click.echo("\nUse with: music-cli play --mood <mood>")
375
+
376
+
377
+ if __name__ == "__main__":
378
+ main()