coder-music-cli 0.4.0__tar.gz → 0.4.1__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.
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/PKG-INFO +31 -8
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/README.md +29 -7
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/PKG-INFO +31 -8
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/SOURCES.txt +4 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/__init__.py +1 -1
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/cli.py +72 -12
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/client.py +15 -13
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/config.py +15 -6
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/daemon.py +43 -30
- coder_music_cli-0.4.1/music_cli/platform/__init__.py +140 -0
- coder_music_cli-0.4.1/music_cli/platform/ipc.py +315 -0
- coder_music_cli-0.4.1/music_cli/platform/paths.py +149 -0
- coder_music_cli-0.4.1/music_cli/platform/player_control.py +197 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/player/ffplay.py +34 -13
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/pyproject.toml +2 -1
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/tests/test_ai_tracks.py +1 -4
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/LICENSE +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/dependency_links.txt +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/entry_points.txt +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/requires.txt +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/top_level.txt +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/__main__.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/ai_tracks.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/context/__init__.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/context/mood.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/context/temporal.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/history.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/player/__init__.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/player/base.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/__init__.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/ai_generator.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/local.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/radio.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/setup.cfg +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/tests/test_config.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/tests/test_context.py +0 -0
- {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/tests/test_history.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coder-music-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A command-line music application for coders with daemon support, radio streaming, and AI-generated music
|
|
5
5
|
Author-email: Luong Nguyen <luongnv89@gmail.com>
|
|
6
6
|
Maintainer-email: Luong Nguyen <luongnv89@gmail.com>
|
|
@@ -16,6 +16,7 @@ Classifier: Environment :: Console
|
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
20
|
Classifier: Programming Language :: Python :: 3
|
|
20
21
|
Classifier: Programming Language :: Python :: 3.9
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -43,11 +44,21 @@ Requires-Dist: bandit>=1.7; extra == "dev"
|
|
|
43
44
|
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
44
45
|
Dynamic: license-file
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="assets/logo/logo-mark.svg" alt="music-cli logo" width="80" height="80">
|
|
49
|
+
</p>
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
<h1 align="center">music-cli</h1>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://pypi.org/project/coder-music-cli/"><img src="https://img.shields.io/pypi/v/coder-music-cli.svg" alt="PyPI version"></a>
|
|
55
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+"></a>
|
|
56
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<p align="center">
|
|
60
|
+
<img src="music-cli-ai.gif" alt="music-cli AI demo" width="600">
|
|
61
|
+
</p>
|
|
51
62
|
|
|
52
63
|
A command-line music player for coders. Background daemon with radio streaming, local MP3s, and AI-generated music.
|
|
53
64
|
|
|
@@ -68,8 +79,9 @@ pip install coder-music-cli
|
|
|
68
79
|
uv pip install coder-music-cli
|
|
69
80
|
|
|
70
81
|
# Install FFmpeg (required)
|
|
71
|
-
brew install ffmpeg
|
|
72
|
-
sudo apt install ffmpeg
|
|
82
|
+
brew install ffmpeg # macOS
|
|
83
|
+
sudo apt install ffmpeg # Ubuntu/Debian
|
|
84
|
+
choco install ffmpeg # Windows (or: winget install ffmpeg)
|
|
73
85
|
```
|
|
74
86
|
|
|
75
87
|
### Optional: AI Music Generation
|
|
@@ -212,7 +224,9 @@ Features:
|
|
|
212
224
|
|
|
213
225
|
## Configuration
|
|
214
226
|
|
|
215
|
-
|
|
227
|
+
Configuration files location:
|
|
228
|
+
- **Linux/macOS**: `~/.config/music-cli/`
|
|
229
|
+
- **Windows**: `%LOCALAPPDATA%\music-cli\`
|
|
216
230
|
|
|
217
231
|
| File | Purpose |
|
|
218
232
|
|------|---------|
|
|
@@ -276,9 +290,18 @@ GitHub: https://github.com/luongnv89/music-cli
|
|
|
276
290
|
|
|
277
291
|
- Python 3.9+
|
|
278
292
|
- FFmpeg
|
|
293
|
+
- **Supported Platforms**: Linux, macOS, Windows 10+
|
|
279
294
|
|
|
280
295
|
## Changelog
|
|
281
296
|
|
|
297
|
+
### v0.4.1
|
|
298
|
+
- Add Windows 10+ support
|
|
299
|
+
- Platform abstraction layer for cross-platform compatibility
|
|
300
|
+
- TCP localhost IPC on Windows (Unix sockets on Linux/macOS)
|
|
301
|
+
- stdin-based pause/resume on Windows (signals on Linux/macOS)
|
|
302
|
+
- Windows-specific config directory (`%LOCALAPPDATA%\music-cli\`)
|
|
303
|
+
- Add Windows to CI test matrix
|
|
304
|
+
|
|
282
305
|
### v0.4.0
|
|
283
306
|
- Add `music-cli ai` command suite for AI track management
|
|
284
307
|
- `ai list` - Display all AI tracks with prompts
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo/logo-mark.svg" alt="music-cli logo" width="80" height="80">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
<h1 align="center">music-cli</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://pypi.org/project/coder-music-cli/"><img src="https://img.shields.io/pypi/v/coder-music-cli.svg" alt="PyPI version"></a>
|
|
9
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+"></a>
|
|
10
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="music-cli-ai.gif" alt="music-cli AI demo" width="600">
|
|
15
|
+
</p>
|
|
6
16
|
|
|
7
17
|
A command-line music player for coders. Background daemon with radio streaming, local MP3s, and AI-generated music.
|
|
8
18
|
|
|
@@ -23,8 +33,9 @@ pip install coder-music-cli
|
|
|
23
33
|
uv pip install coder-music-cli
|
|
24
34
|
|
|
25
35
|
# Install FFmpeg (required)
|
|
26
|
-
brew install ffmpeg
|
|
27
|
-
sudo apt install ffmpeg
|
|
36
|
+
brew install ffmpeg # macOS
|
|
37
|
+
sudo apt install ffmpeg # Ubuntu/Debian
|
|
38
|
+
choco install ffmpeg # Windows (or: winget install ffmpeg)
|
|
28
39
|
```
|
|
29
40
|
|
|
30
41
|
### Optional: AI Music Generation
|
|
@@ -167,7 +178,9 @@ Features:
|
|
|
167
178
|
|
|
168
179
|
## Configuration
|
|
169
180
|
|
|
170
|
-
|
|
181
|
+
Configuration files location:
|
|
182
|
+
- **Linux/macOS**: `~/.config/music-cli/`
|
|
183
|
+
- **Windows**: `%LOCALAPPDATA%\music-cli\`
|
|
171
184
|
|
|
172
185
|
| File | Purpose |
|
|
173
186
|
|------|---------|
|
|
@@ -231,9 +244,18 @@ GitHub: https://github.com/luongnv89/music-cli
|
|
|
231
244
|
|
|
232
245
|
- Python 3.9+
|
|
233
246
|
- FFmpeg
|
|
247
|
+
- **Supported Platforms**: Linux, macOS, Windows 10+
|
|
234
248
|
|
|
235
249
|
## Changelog
|
|
236
250
|
|
|
251
|
+
### v0.4.1
|
|
252
|
+
- Add Windows 10+ support
|
|
253
|
+
- Platform abstraction layer for cross-platform compatibility
|
|
254
|
+
- TCP localhost IPC on Windows (Unix sockets on Linux/macOS)
|
|
255
|
+
- stdin-based pause/resume on Windows (signals on Linux/macOS)
|
|
256
|
+
- Windows-specific config directory (`%LOCALAPPDATA%\music-cli\`)
|
|
257
|
+
- Add Windows to CI test matrix
|
|
258
|
+
|
|
237
259
|
### v0.4.0
|
|
238
260
|
- Add `music-cli ai` command suite for AI track management
|
|
239
261
|
- `ai list` - Display all AI tracks with prompts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coder-music-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A command-line music application for coders with daemon support, radio streaming, and AI-generated music
|
|
5
5
|
Author-email: Luong Nguyen <luongnv89@gmail.com>
|
|
6
6
|
Maintainer-email: Luong Nguyen <luongnv89@gmail.com>
|
|
@@ -16,6 +16,7 @@ Classifier: Environment :: Console
|
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
20
|
Classifier: Programming Language :: Python :: 3
|
|
20
21
|
Classifier: Programming Language :: Python :: 3.9
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -43,11 +44,21 @@ Requires-Dist: bandit>=1.7; extra == "dev"
|
|
|
43
44
|
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
44
45
|
Dynamic: license-file
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="assets/logo/logo-mark.svg" alt="music-cli logo" width="80" height="80">
|
|
49
|
+
</p>
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
<h1 align="center">music-cli</h1>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://pypi.org/project/coder-music-cli/"><img src="https://img.shields.io/pypi/v/coder-music-cli.svg" alt="PyPI version"></a>
|
|
55
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+"></a>
|
|
56
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<p align="center">
|
|
60
|
+
<img src="music-cli-ai.gif" alt="music-cli AI demo" width="600">
|
|
61
|
+
</p>
|
|
51
62
|
|
|
52
63
|
A command-line music player for coders. Background daemon with radio streaming, local MP3s, and AI-generated music.
|
|
53
64
|
|
|
@@ -68,8 +79,9 @@ pip install coder-music-cli
|
|
|
68
79
|
uv pip install coder-music-cli
|
|
69
80
|
|
|
70
81
|
# Install FFmpeg (required)
|
|
71
|
-
brew install ffmpeg
|
|
72
|
-
sudo apt install ffmpeg
|
|
82
|
+
brew install ffmpeg # macOS
|
|
83
|
+
sudo apt install ffmpeg # Ubuntu/Debian
|
|
84
|
+
choco install ffmpeg # Windows (or: winget install ffmpeg)
|
|
73
85
|
```
|
|
74
86
|
|
|
75
87
|
### Optional: AI Music Generation
|
|
@@ -212,7 +224,9 @@ Features:
|
|
|
212
224
|
|
|
213
225
|
## Configuration
|
|
214
226
|
|
|
215
|
-
|
|
227
|
+
Configuration files location:
|
|
228
|
+
- **Linux/macOS**: `~/.config/music-cli/`
|
|
229
|
+
- **Windows**: `%LOCALAPPDATA%\music-cli\`
|
|
216
230
|
|
|
217
231
|
| File | Purpose |
|
|
218
232
|
|------|---------|
|
|
@@ -276,9 +290,18 @@ GitHub: https://github.com/luongnv89/music-cli
|
|
|
276
290
|
|
|
277
291
|
- Python 3.9+
|
|
278
292
|
- FFmpeg
|
|
293
|
+
- **Supported Platforms**: Linux, macOS, Windows 10+
|
|
279
294
|
|
|
280
295
|
## Changelog
|
|
281
296
|
|
|
297
|
+
### v0.4.1
|
|
298
|
+
- Add Windows 10+ support
|
|
299
|
+
- Platform abstraction layer for cross-platform compatibility
|
|
300
|
+
- TCP localhost IPC on Windows (Unix sockets on Linux/macOS)
|
|
301
|
+
- stdin-based pause/resume on Windows (signals on Linux/macOS)
|
|
302
|
+
- Windows-specific config directory (`%LOCALAPPDATA%\music-cli\`)
|
|
303
|
+
- Add Windows to CI test matrix
|
|
304
|
+
|
|
282
305
|
### v0.4.0
|
|
283
306
|
- Add `music-cli ai` command suite for AI track management
|
|
284
307
|
- `ai list` - Display all AI tracks with prompts
|
|
@@ -18,6 +18,10 @@ music_cli/history.py
|
|
|
18
18
|
music_cli/context/__init__.py
|
|
19
19
|
music_cli/context/mood.py
|
|
20
20
|
music_cli/context/temporal.py
|
|
21
|
+
music_cli/platform/__init__.py
|
|
22
|
+
music_cli/platform/ipc.py
|
|
23
|
+
music_cli/platform/paths.py
|
|
24
|
+
music_cli/platform/player_control.py
|
|
21
25
|
music_cli/player/__init__.py
|
|
22
26
|
music_cli/player/base.py
|
|
23
27
|
music_cli/player/ffplay.py
|
|
@@ -14,6 +14,7 @@ from . import __github_url__, __version__
|
|
|
14
14
|
from .client import DaemonClient
|
|
15
15
|
from .config import get_config
|
|
16
16
|
from .daemon import get_daemon_pid, is_daemon_running
|
|
17
|
+
from .platform import is_windows
|
|
17
18
|
from .player.ffplay import check_ffplay_available
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
@@ -117,15 +118,35 @@ def ensure_daemon() -> DaemonClient:
|
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
def start_daemon_background() -> None:
|
|
120
|
-
"""Start the daemon in background.
|
|
121
|
-
|
|
121
|
+
"""Start the daemon in background.
|
|
122
|
+
|
|
123
|
+
Uses platform-appropriate process creation:
|
|
124
|
+
- Linux/macOS: start_new_session=True
|
|
125
|
+
- Windows: CREATE_NEW_PROCESS_GROUP flag
|
|
126
|
+
"""
|
|
122
127
|
python = sys.executable
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
cmd = [python, "-m", "music_cli.daemon"]
|
|
129
|
+
|
|
130
|
+
if is_windows():
|
|
131
|
+
# Windows: Use CREATE_NEW_PROCESS_GROUP to detach from console
|
|
132
|
+
# These are Windows API constants
|
|
133
|
+
create_new_process_group = 0x00000200
|
|
134
|
+
detached_process = 0x00000008
|
|
135
|
+
subprocess.Popen(
|
|
136
|
+
cmd,
|
|
137
|
+
stdout=subprocess.DEVNULL,
|
|
138
|
+
stderr=subprocess.DEVNULL,
|
|
139
|
+
stdin=subprocess.DEVNULL,
|
|
140
|
+
creationflags=create_new_process_group | detached_process,
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
# Unix: Use start_new_session to create a new session
|
|
144
|
+
subprocess.Popen(
|
|
145
|
+
cmd,
|
|
146
|
+
stdout=subprocess.DEVNULL,
|
|
147
|
+
stderr=subprocess.DEVNULL,
|
|
148
|
+
start_new_session=True,
|
|
149
|
+
)
|
|
129
150
|
|
|
130
151
|
|
|
131
152
|
@click.group(invoke_without_command=True)
|
|
@@ -174,8 +195,13 @@ def play(mode, source, mood, auto, duration, index):
|
|
|
174
195
|
"""
|
|
175
196
|
if not check_ffplay_available():
|
|
176
197
|
click.echo("Error: ffplay not found. Please install FFmpeg.", err=True)
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
if is_windows():
|
|
199
|
+
click.echo(" Windows: choco install ffmpeg", err=True)
|
|
200
|
+
click.echo(" or: winget install ffmpeg", err=True)
|
|
201
|
+
click.echo(" or: scoop install ffmpeg", err=True)
|
|
202
|
+
else:
|
|
203
|
+
click.echo(" macOS: brew install ffmpeg", err=True)
|
|
204
|
+
click.echo(" Linux: apt install ffmpeg", err=True)
|
|
179
205
|
sys.exit(1)
|
|
180
206
|
|
|
181
207
|
client = ensure_daemon()
|
|
@@ -555,7 +581,7 @@ def daemon_control(action):
|
|
|
555
581
|
elif action == "stop":
|
|
556
582
|
pid = get_daemon_pid()
|
|
557
583
|
if pid:
|
|
558
|
-
|
|
584
|
+
_terminate_daemon(pid)
|
|
559
585
|
click.echo("Daemon stopped")
|
|
560
586
|
else:
|
|
561
587
|
click.echo("Daemon is not running")
|
|
@@ -563,12 +589,46 @@ def daemon_control(action):
|
|
|
563
589
|
elif action == "restart":
|
|
564
590
|
pid = get_daemon_pid()
|
|
565
591
|
if pid:
|
|
566
|
-
|
|
592
|
+
_terminate_daemon(pid)
|
|
567
593
|
time.sleep(0.5)
|
|
568
594
|
start_daemon_background()
|
|
569
595
|
click.echo("Daemon restarted")
|
|
570
596
|
|
|
571
597
|
|
|
598
|
+
def _terminate_daemon(pid: int) -> None:
|
|
599
|
+
"""Terminate the daemon process.
|
|
600
|
+
|
|
601
|
+
Uses platform-appropriate method:
|
|
602
|
+
- Unix: SIGTERM signal (allows graceful shutdown)
|
|
603
|
+
- Windows: Send stop command via IPC, then terminate
|
|
604
|
+
"""
|
|
605
|
+
if is_windows():
|
|
606
|
+
# On Windows, try to send stop command via IPC for graceful shutdown
|
|
607
|
+
# TerminateProcess doesn't give the daemon a chance to cleanup
|
|
608
|
+
try:
|
|
609
|
+
from .client import DaemonClient
|
|
610
|
+
|
|
611
|
+
client = DaemonClient()
|
|
612
|
+
# Try to send stop command - this triggers graceful shutdown
|
|
613
|
+
client.send_command("shutdown", timeout=2.0)
|
|
614
|
+
# Wait a moment for cleanup
|
|
615
|
+
time.sleep(0.3)
|
|
616
|
+
except Exception: # noqa: S110 # nosec B110
|
|
617
|
+
pass # If IPC fails, fall through to forceful termination
|
|
618
|
+
|
|
619
|
+
# Force terminate if still running
|
|
620
|
+
try:
|
|
621
|
+
os.kill(pid, signal.SIGTERM)
|
|
622
|
+
except (ProcessLookupError, OSError):
|
|
623
|
+
pass # Process already stopped
|
|
624
|
+
else:
|
|
625
|
+
# Unix: SIGTERM triggers graceful shutdown via signal handler
|
|
626
|
+
try:
|
|
627
|
+
os.kill(pid, signal.SIGTERM)
|
|
628
|
+
except (ProcessLookupError, OSError):
|
|
629
|
+
pass # Process already stopped
|
|
630
|
+
|
|
631
|
+
|
|
572
632
|
@main.command("config")
|
|
573
633
|
def show_config():
|
|
574
634
|
"""Show configuration file locations."""
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
import socket
|
|
6
5
|
from typing import Any
|
|
7
6
|
|
|
8
7
|
from .config import get_config
|
|
8
|
+
from .platform import get_ipc_client
|
|
9
|
+
from .platform.ipc import IPCClient
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
11
12
|
|
|
@@ -17,11 +18,18 @@ AI_TIMEOUT = 300.0 # 5 minutes for AI generation
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class DaemonClient:
|
|
20
|
-
"""Client for sending commands to the daemon.
|
|
21
|
+
"""Client for sending commands to the daemon.
|
|
22
|
+
|
|
23
|
+
Uses platform-appropriate IPC:
|
|
24
|
+
- Linux/macOS: Unix domain sockets
|
|
25
|
+
- Windows: TCP localhost
|
|
26
|
+
"""
|
|
21
27
|
|
|
22
28
|
def __init__(self):
|
|
23
29
|
self.config = get_config()
|
|
24
|
-
self.socket_path =
|
|
30
|
+
self.socket_path = self.config.socket_path
|
|
31
|
+
# Platform-specific IPC client (Unix sockets or TCP)
|
|
32
|
+
self._ipc_client: IPCClient = get_ipc_client()
|
|
25
33
|
|
|
26
34
|
def send_command(
|
|
27
35
|
self, command: str, args: dict | None = None, timeout: float | None = None
|
|
@@ -46,6 +54,8 @@ class DaemonClient:
|
|
|
46
54
|
if timeout is None:
|
|
47
55
|
if command == "play" and args.get("mode") == "ai":
|
|
48
56
|
timeout = AI_TIMEOUT
|
|
57
|
+
elif command == "ai_play":
|
|
58
|
+
timeout = AI_TIMEOUT
|
|
49
59
|
else:
|
|
50
60
|
timeout = DEFAULT_TIMEOUT
|
|
51
61
|
|
|
@@ -54,11 +64,9 @@ class DaemonClient:
|
|
|
54
64
|
"args": args,
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
# Use platform-specific IPC client
|
|
68
|
+
sock = self._ipc_client.connect(self.socket_path, timeout)
|
|
58
69
|
try:
|
|
59
|
-
sock.settimeout(timeout)
|
|
60
|
-
sock.connect(self.socket_path)
|
|
61
|
-
|
|
62
70
|
sock.sendall(json.dumps(request).encode())
|
|
63
71
|
|
|
64
72
|
# Receive response with size limit
|
|
@@ -78,12 +86,6 @@ class DaemonClient:
|
|
|
78
86
|
else:
|
|
79
87
|
return {"error": "Empty response from daemon"}
|
|
80
88
|
|
|
81
|
-
except FileNotFoundError as e:
|
|
82
|
-
raise ConnectionError("Daemon not running (socket not found)") from e
|
|
83
|
-
except ConnectionRefusedError as e:
|
|
84
|
-
raise ConnectionError("Daemon not running (connection refused)") from e
|
|
85
|
-
except socket.timeout as e:
|
|
86
|
-
raise ConnectionError("Daemon not responding (timeout)") from e
|
|
87
89
|
except json.JSONDecodeError as e:
|
|
88
90
|
logger.warning(f"Invalid JSON response from daemon: {e}")
|
|
89
91
|
return {"error": "Invalid response from daemon"}
|
|
@@ -15,6 +15,7 @@ else:
|
|
|
15
15
|
import tomli_w
|
|
16
16
|
|
|
17
17
|
from . import __version__
|
|
18
|
+
from .platform import get_path_provider
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
@@ -120,16 +121,24 @@ Radio Capital|https://icecast.unitedradio.it/Capital.mp3
|
|
|
120
121
|
"""
|
|
121
122
|
|
|
122
123
|
def __init__(self, config_dir: Path | None = None):
|
|
123
|
-
"""Initialize config with optional custom directory.
|
|
124
|
+
"""Initialize config with optional custom directory.
|
|
125
|
+
|
|
126
|
+
Uses platform-appropriate paths:
|
|
127
|
+
- Linux/macOS: ~/.config/music-cli/
|
|
128
|
+
- Windows: %LOCALAPPDATA%\\music-cli\\
|
|
129
|
+
"""
|
|
130
|
+
# Get platform-specific path provider
|
|
131
|
+
self._path_provider = get_path_provider()
|
|
132
|
+
|
|
124
133
|
if config_dir is None:
|
|
125
|
-
config_dir =
|
|
134
|
+
config_dir = self._path_provider.get_config_dir()
|
|
126
135
|
self.config_dir = config_dir
|
|
127
136
|
self.config_file = self.config_dir / "config.toml"
|
|
128
137
|
self.radios_file = self.config_dir / "radios.txt"
|
|
129
|
-
self.history_file = self.config_dir
|
|
130
|
-
self.socket_path = self.config_dir
|
|
131
|
-
self.pid_file = self.config_dir
|
|
132
|
-
self.ai_music_dir = self.config_dir
|
|
138
|
+
self.history_file = self._path_provider.get_history_file(self.config_dir)
|
|
139
|
+
self.socket_path = self._path_provider.get_socket_path(self.config_dir)
|
|
140
|
+
self.pid_file = self._path_provider.get_pid_file(self.config_dir)
|
|
141
|
+
self.ai_music_dir = self._path_provider.get_ai_music_dir(self.config_dir)
|
|
133
142
|
self.ai_tracks_file = self.config_dir / "ai_tracks.json"
|
|
134
143
|
self._config: dict[str, Any] = {}
|
|
135
144
|
self._ensure_config_dir()
|
|
@@ -11,6 +11,8 @@ from .config import get_config
|
|
|
11
11
|
from .context.mood import Mood, MoodContext
|
|
12
12
|
from .context.temporal import TemporalContext
|
|
13
13
|
from .history import get_history
|
|
14
|
+
from .platform import get_ipc_server, supports_unix_signals
|
|
15
|
+
from .platform.ipc import IPCServer
|
|
14
16
|
from .player.base import TrackInfo
|
|
15
17
|
from .player.ffplay import FFplayPlayer
|
|
16
18
|
from .sources.local import LocalSource
|
|
@@ -31,42 +33,39 @@ class MusicDaemon:
|
|
|
31
33
|
self.temporal = TemporalContext()
|
|
32
34
|
self.ai_tracks = get_ai_tracks()
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
# Platform-specific IPC server (Unix sockets or TCP)
|
|
37
|
+
self._ipc_server: IPCServer = get_ipc_server()
|
|
35
38
|
self._running = False
|
|
36
39
|
self._current_mood: Mood | None = None
|
|
37
40
|
self._auto_play = False # For infinite/context-aware mode
|
|
38
41
|
|
|
39
42
|
async def start(self) -> None:
|
|
40
|
-
"""Start the daemon server.
|
|
41
|
-
socket_path = self.config.socket_path
|
|
43
|
+
"""Start the daemon server.
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
Uses platform-appropriate IPC:
|
|
46
|
+
- Linux/macOS: Unix domain sockets
|
|
47
|
+
- Windows: TCP localhost
|
|
48
|
+
"""
|
|
49
|
+
socket_path = self.config.socket_path
|
|
46
50
|
|
|
47
51
|
self._running = True
|
|
48
52
|
|
|
49
|
-
# Set up signal handlers
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# Start Unix socket server
|
|
55
|
-
self._server = await asyncio.start_unix_server(
|
|
56
|
-
self._handle_client,
|
|
57
|
-
path=str(socket_path),
|
|
58
|
-
)
|
|
53
|
+
# Set up signal handlers (Unix only - not supported on Windows asyncio)
|
|
54
|
+
if supports_unix_signals():
|
|
55
|
+
loop = asyncio.get_event_loop()
|
|
56
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
57
|
+
loop.add_signal_handler(sig, lambda: asyncio.create_task(self.stop()))
|
|
59
58
|
|
|
60
|
-
#
|
|
61
|
-
|
|
59
|
+
# Start IPC server (platform-specific)
|
|
60
|
+
await self._ipc_server.start(self._handle_client, socket_path)
|
|
62
61
|
|
|
63
62
|
# Write PID file
|
|
64
63
|
self.config.pid_file.write_text(str(os.getpid()))
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
address_display = self._ipc_server.get_address_display(socket_path)
|
|
66
|
+
logger.info(f"Daemon started, listening on {address_display}")
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
await self._server.serve_forever()
|
|
68
|
+
await self._ipc_server.serve_forever()
|
|
70
69
|
|
|
71
70
|
async def stop(self) -> None:
|
|
72
71
|
"""Stop the daemon."""
|
|
@@ -75,15 +74,15 @@ class MusicDaemon:
|
|
|
75
74
|
|
|
76
75
|
await self.player.stop()
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await self._server.wait_closed()
|
|
77
|
+
# Stop IPC server (handles socket cleanup on Unix)
|
|
78
|
+
await self._ipc_server.stop()
|
|
81
79
|
|
|
82
|
-
# Clean up
|
|
83
|
-
if self.config.socket_path.exists():
|
|
84
|
-
self.config.socket_path.unlink()
|
|
80
|
+
# Clean up PID file
|
|
85
81
|
if self.config.pid_file.exists():
|
|
86
|
-
|
|
82
|
+
try:
|
|
83
|
+
self.config.pid_file.unlink()
|
|
84
|
+
except OSError:
|
|
85
|
+
pass # Best effort cleanup
|
|
87
86
|
|
|
88
87
|
logger.info("Daemon stopped")
|
|
89
88
|
|
|
@@ -137,6 +136,7 @@ class MusicDaemon:
|
|
|
137
136
|
"ai_play": self._cmd_ai_play,
|
|
138
137
|
"ai_replay": self._cmd_ai_replay,
|
|
139
138
|
"ai_remove": self._cmd_ai_remove,
|
|
139
|
+
"shutdown": self._cmd_shutdown,
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
handler = handlers.get(command)
|
|
@@ -538,6 +538,16 @@ class MusicDaemon:
|
|
|
538
538
|
else:
|
|
539
539
|
return {"error": "Failed to remove track"}
|
|
540
540
|
|
|
541
|
+
async def _cmd_shutdown(self, args: dict) -> dict:
|
|
542
|
+
"""Shutdown the daemon gracefully.
|
|
543
|
+
|
|
544
|
+
Used on Windows where signal handlers aren't supported.
|
|
545
|
+
"""
|
|
546
|
+
logger.info("Shutdown command received")
|
|
547
|
+
# Schedule stop in a separate task so we can respond first
|
|
548
|
+
asyncio.create_task(self.stop())
|
|
549
|
+
return {"status": "shutting_down"}
|
|
550
|
+
|
|
541
551
|
|
|
542
552
|
def run_daemon() -> None:
|
|
543
553
|
"""Run the daemon (entry point)."""
|
|
@@ -556,6 +566,8 @@ def get_daemon_pid() -> int | None:
|
|
|
556
566
|
Returns the PID if daemon is running, None otherwise.
|
|
557
567
|
Also cleans up stale PID/socket files if the daemon is not running.
|
|
558
568
|
"""
|
|
569
|
+
from .platform import is_unix
|
|
570
|
+
|
|
559
571
|
config = get_config()
|
|
560
572
|
|
|
561
573
|
if not config.pid_file.exists():
|
|
@@ -565,12 +577,13 @@ def get_daemon_pid() -> int | None:
|
|
|
565
577
|
pid = int(config.pid_file.read_text().strip())
|
|
566
578
|
os.kill(pid, 0) # Check if running
|
|
567
579
|
return pid
|
|
568
|
-
except (ValueError, ProcessLookupError, PermissionError):
|
|
580
|
+
except (ValueError, ProcessLookupError, PermissionError, OSError):
|
|
569
581
|
# PID file is stale, clean up
|
|
570
582
|
try:
|
|
571
583
|
if config.pid_file.exists():
|
|
572
584
|
config.pid_file.unlink()
|
|
573
|
-
|
|
585
|
+
# Only clean up socket file on Unix (Windows uses TCP)
|
|
586
|
+
if is_unix() and config.socket_path.exists():
|
|
574
587
|
config.socket_path.unlink()
|
|
575
588
|
except OSError:
|
|
576
589
|
pass # Best effort cleanup
|