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.
Files changed (37) hide show
  1. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/PKG-INFO +31 -8
  2. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/README.md +29 -7
  3. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/PKG-INFO +31 -8
  4. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/SOURCES.txt +4 -0
  5. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/__init__.py +1 -1
  6. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/cli.py +72 -12
  7. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/client.py +15 -13
  8. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/config.py +15 -6
  9. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/daemon.py +43 -30
  10. coder_music_cli-0.4.1/music_cli/platform/__init__.py +140 -0
  11. coder_music_cli-0.4.1/music_cli/platform/ipc.py +315 -0
  12. coder_music_cli-0.4.1/music_cli/platform/paths.py +149 -0
  13. coder_music_cli-0.4.1/music_cli/platform/player_control.py +197 -0
  14. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/player/ffplay.py +34 -13
  15. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/pyproject.toml +2 -1
  16. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/tests/test_ai_tracks.py +1 -4
  17. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/LICENSE +0 -0
  18. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/dependency_links.txt +0 -0
  19. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/entry_points.txt +0 -0
  20. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/requires.txt +0 -0
  21. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/coder_music_cli.egg-info/top_level.txt +0 -0
  22. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/__main__.py +0 -0
  23. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/ai_tracks.py +0 -0
  24. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/context/__init__.py +0 -0
  25. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/context/mood.py +0 -0
  26. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/context/temporal.py +0 -0
  27. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/history.py +0 -0
  28. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/player/__init__.py +0 -0
  29. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/player/base.py +0 -0
  30. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/__init__.py +0 -0
  31. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/ai_generator.py +0 -0
  32. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/local.py +0 -0
  33. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/music_cli/sources/radio.py +0 -0
  34. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/setup.cfg +0 -0
  35. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/tests/test_config.py +0 -0
  36. {coder_music_cli-0.4.0 → coder_music_cli-0.4.1}/tests/test_context.py +0 -0
  37. {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.0
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
- # music-cli
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
- [![PyPI version](https://img.shields.io/pypi/v/coder-music-cli.svg)](https://pypi.org/project/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
+ <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 # macOS
72
- sudo apt install ffmpeg # Ubuntu/Debian
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
- Files in `~/.config/music-cli/`:
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
- # music-cli
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
- [![PyPI version](https://img.shields.io/pypi/v/coder-music-cli.svg)](https://pypi.org/project/coder-music-cli/)
4
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
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 # macOS
27
- sudo apt install ffmpeg # Ubuntu/Debian
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
- Files in `~/.config/music-cli/`:
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.0
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
- # music-cli
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
- [![PyPI version](https://img.shields.io/pypi/v/coder-music-cli.svg)](https://pypi.org/project/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
+ <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 # macOS
72
- sudo apt install ffmpeg # Ubuntu/Debian
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
- Files in `~/.config/music-cli/`:
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
@@ -1,4 +1,4 @@
1
1
  """music-cli: A command-line music application for coders."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.1"
4
4
  __github_url__ = "https://github.com/luongnv89/music-cli"
@@ -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
- # Start daemon as subprocess
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
- subprocess.Popen(
124
- [python, "-m", "music_cli.daemon"],
125
- stdout=subprocess.DEVNULL,
126
- stderr=subprocess.DEVNULL,
127
- start_new_session=True,
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
- click.echo(" macOS: brew install ffmpeg", err=True)
178
- click.echo(" Linux: apt install ffmpeg", err=True)
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
- os.kill(pid, signal.SIGTERM)
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
- os.kill(pid, signal.SIGTERM)
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 = str(self.config.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
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
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 = Path("~/.config/music-cli").expanduser()
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 / "history.jsonl"
130
- self.socket_path = self.config_dir / "music-cli.sock"
131
- self.pid_file = self.config_dir / "music-cli.pid"
132
- self.ai_music_dir = self.config_dir / "ai_music"
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
- self._server: asyncio.Server | None = None
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
- # Clean up stale socket
44
- if socket_path.exists():
45
- socket_path.unlink()
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
- loop = asyncio.get_event_loop()
51
- for sig in (signal.SIGTERM, signal.SIGINT):
52
- loop.add_signal_handler(sig, lambda: asyncio.create_task(self.stop()))
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
- # Set socket permissions
61
- socket_path.chmod(0o600)
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
- logger.info(f"Daemon started, listening on {socket_path}")
65
+ address_display = self._ipc_server.get_address_display(socket_path)
66
+ logger.info(f"Daemon started, listening on {address_display}")
67
67
 
68
- async with self._server:
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
- if self._server:
79
- self._server.close()
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 files
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
- self.config.pid_file.unlink()
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
- if config.socket_path.exists():
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