coder-music-cli 0.9.0__tar.gz → 0.9.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 (53) hide show
  1. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/PKG-INFO +3 -3
  2. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/README.md +2 -2
  3. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/coder_music_cli.egg-info/PKG-INFO +3 -3
  4. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/coder_music_cli.egg-info/SOURCES.txt +2 -1
  5. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/__init__.py +1 -1
  6. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/cli.py +24 -0
  7. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/local.py +8 -2
  8. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/pyproject.toml +1 -1
  9. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/tests/test_cli.py +86 -1
  10. coder_music_cli-0.9.1/tests/test_local_source.py +88 -0
  11. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/LICENSE +0 -0
  12. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/coder_music_cli.egg-info/dependency_links.txt +0 -0
  13. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/coder_music_cli.egg-info/entry_points.txt +0 -0
  14. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/coder_music_cli.egg-info/requires.txt +0 -0
  15. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/coder_music_cli.egg-info/top_level.txt +0 -0
  16. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/__main__.py +0 -0
  17. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/ai_tracks.py +0 -0
  18. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/client.py +0 -0
  19. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/config.py +0 -0
  20. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/context/__init__.py +0 -0
  21. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/context/mood.py +0 -0
  22. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/context/temporal.py +0 -0
  23. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/daemon.py +0 -0
  24. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/hf_cache.py +0 -0
  25. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/history.py +0 -0
  26. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/model_manager.py +0 -0
  27. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/platform/__init__.py +0 -0
  28. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/platform/ipc.py +0 -0
  29. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/platform/paths.py +0 -0
  30. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/platform/player_control.py +0 -0
  31. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/player/__init__.py +0 -0
  32. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/player/base.py +0 -0
  33. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/player/ffplay.py +0 -0
  34. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/__init__.py +0 -0
  35. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_generator.py +0 -0
  36. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/__init__.py +0 -0
  37. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/audioldm_strategy.py +0 -0
  38. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/bark_strategy.py +0 -0
  39. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/model_config.py +0 -0
  40. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/model_registry.py +0 -0
  41. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/model_strategy.py +0 -0
  42. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/musicgen_strategy.py +0 -0
  43. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/progress_callback.py +0 -0
  44. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/ai_models/strategy_cache.py +0 -0
  45. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/radio.py +0 -0
  46. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/sources/youtube.py +0 -0
  47. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/music_cli/youtube_history.py +0 -0
  48. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/setup.cfg +0 -0
  49. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/tests/test_ai_tracks.py +0 -0
  50. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/tests/test_config.py +0 -0
  51. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/tests/test_context.py +0 -0
  52. {coder_music_cli-0.9.0 → coder_music_cli-0.9.1}/tests/test_e2e.py +0 -0
  53. {coder_music_cli-0.9.0 → coder_music_cli-0.9.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.9.0
3
+ Version: 0.9.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>
@@ -203,7 +203,7 @@ The base install is tiny. The `[ai]` extra downloads ~5GB (PyTorch + HuggingFace
203
203
  music-cli is not a replacement for your music library. It's a lightweight, terminal-native player for background music while coding. No browser tabs, no electron apps, no accounts.
204
204
 
205
205
  **Is it actively maintained?**
206
- Yes. The latest release is v0.9.0. Check the [changelog](CHANGELOG.md) for recent updates.
206
+ Yes. The latest release is v0.9.1. Check the [changelog](CHANGELOG.md) for recent updates.
207
207
 
208
208
  **Can I add my own radio stations?**
209
209
  Absolutely. Run `mc radio add` or edit `~/.config/music-cli/radios.txt` directly. Format: `Station Name|stream-url`.
@@ -552,7 +552,7 @@ Context: morning / weekday
552
552
 
553
553
  "Music gives a soul to the universe, wings to the mind, flight to the imagination." - Plato
554
554
 
555
- Version: 0.9.0
555
+ Version: 0.9.1
556
556
  GitHub: https://github.com/luongnv89/music-cli
557
557
  ```
558
558
 
@@ -150,7 +150,7 @@ The base install is tiny. The `[ai]` extra downloads ~5GB (PyTorch + HuggingFace
150
150
  music-cli is not a replacement for your music library. It's a lightweight, terminal-native player for background music while coding. No browser tabs, no electron apps, no accounts.
151
151
 
152
152
  **Is it actively maintained?**
153
- Yes. The latest release is v0.9.0. Check the [changelog](CHANGELOG.md) for recent updates.
153
+ Yes. The latest release is v0.9.1. Check the [changelog](CHANGELOG.md) for recent updates.
154
154
 
155
155
  **Can I add my own radio stations?**
156
156
  Absolutely. Run `mc radio add` or edit `~/.config/music-cli/radios.txt` directly. Format: `Station Name|stream-url`.
@@ -499,7 +499,7 @@ Context: morning / weekday
499
499
 
500
500
  "Music gives a soul to the universe, wings to the mind, flight to the imagination." - Plato
501
501
 
502
- Version: 0.9.0
502
+ Version: 0.9.1
503
503
  GitHub: https://github.com/luongnv89/music-cli
504
504
  ```
505
505
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coder-music-cli
3
- Version: 0.9.0
3
+ Version: 0.9.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>
@@ -203,7 +203,7 @@ The base install is tiny. The `[ai]` extra downloads ~5GB (PyTorch + HuggingFace
203
203
  music-cli is not a replacement for your music library. It's a lightweight, terminal-native player for background music while coding. No browser tabs, no electron apps, no accounts.
204
204
 
205
205
  **Is it actively maintained?**
206
- Yes. The latest release is v0.9.0. Check the [changelog](CHANGELOG.md) for recent updates.
206
+ Yes. The latest release is v0.9.1. Check the [changelog](CHANGELOG.md) for recent updates.
207
207
 
208
208
  **Can I add my own radio stations?**
209
209
  Absolutely. Run `mc radio add` or edit `~/.config/music-cli/radios.txt` directly. Format: `Station Name|stream-url`.
@@ -552,7 +552,7 @@ Context: morning / weekday
552
552
 
553
553
  "Music gives a soul to the universe, wings to the mind, flight to the imagination." - Plato
554
554
 
555
- Version: 0.9.0
555
+ Version: 0.9.1
556
556
  GitHub: https://github.com/luongnv89/music-cli
557
557
  ```
558
558
 
@@ -47,4 +47,5 @@ tests/test_cli.py
47
47
  tests/test_config.py
48
48
  tests/test_context.py
49
49
  tests/test_e2e.py
50
- tests/test_history.py
50
+ tests/test_history.py
51
+ tests/test_local_source.py
@@ -1,4 +1,4 @@
1
1
  """music-cli: A command-line music application for coders."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.9.1"
4
4
  __github_url__ = "https://github.com/luongnv89/music-cli"
@@ -289,6 +289,25 @@ def _detect_play_mode(source_arg):
289
289
  return "radio", source_arg
290
290
 
291
291
 
292
+ def _resolve_local_path(source: str) -> str:
293
+ """Resolve a local playback source against the CLI's cwd.
294
+
295
+ The daemon is a long-running background process started once with its
296
+ own cwd (see start_daemon_background) — it is generally not the cwd of
297
+ the terminal a later `play` command runs in. A still-relative path
298
+ handed to the daemon therefore gets checked against the *daemon's* cwd,
299
+ not the user's, and silently misses even when the file is right there.
300
+ Resolve it to an absolute path here, while we still have the correct
301
+ cwd, so it survives the trip over IPC unchanged. Leave anything that
302
+ doesn't exist relative to cwd as-is so the daemon's fallback to the
303
+ configured music directory keeps working.
304
+ """
305
+ path = Path(source)
306
+ if not path.is_absolute() and path.exists():
307
+ return str(path.resolve())
308
+ return source
309
+
310
+
292
311
  @main.command()
293
312
  @click.argument("source", required=False, default=None)
294
313
  @click.option(
@@ -366,6 +385,11 @@ def play(source, mode, source_flag, mood, auto, duration, index):
366
385
  # If mode was explicitly given, keep the old behaviour
367
386
  # (effective_source from flag/positional is used as-is)
368
387
 
388
+ # Issue #18: a relative local path must be resolved before it crosses
389
+ # the IPC boundary to the daemon (see _resolve_local_path).
390
+ if mode == "local" and effective_source:
391
+ effective_source = _resolve_local_path(effective_source)
392
+
369
393
  client = ensure_daemon()
370
394
 
371
395
  # Show animation for AI generation
@@ -23,8 +23,14 @@ class LocalSource:
23
23
  file_path = Path(path)
24
24
 
25
25
  if not file_path.is_absolute():
26
- # Try relative to music dir
27
- file_path = self.music_dir / file_path
26
+ # Prefer a match relative to the current working directory;
27
+ # resolve it now so the result doesn't depend on cwd staying
28
+ # the same for the rest of the call chain (e.g. across the
29
+ # daemon's IPC boundary). Fall back to the configured music dir.
30
+ if file_path.exists():
31
+ file_path = file_path.resolve()
32
+ else:
33
+ file_path = self.music_dir / file_path
28
34
 
29
35
  if not file_path.exists():
30
36
  return None
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coder-music-cli"
7
- version = "0.9.0"
7
+ version = "0.9.1"
8
8
  description = "A command-line music application for coders with daemon support, radio streaming, and AI-generated music"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,12 +1,13 @@
1
1
  """Tests for CLI v2 Phase 1, Phase 2 & Phase 3."""
2
2
 
3
3
  import os
4
+ from pathlib import Path
4
5
  from unittest.mock import MagicMock, patch
5
6
 
6
7
  import pytest
7
8
  from click.testing import CliRunner
8
9
 
9
- from music_cli.cli import _detect_play_mode, icon, main
10
+ from music_cli.cli import _detect_play_mode, _resolve_local_path, icon, main
10
11
 
11
12
 
12
13
  @pytest.fixture
@@ -381,6 +382,90 @@ class TestSmartPlayDetection:
381
382
  assert call_kwargs[1]["mode"] == "context" or call_kwargs.kwargs["mode"] == "context"
382
383
 
383
384
 
385
+ class TestResolveLocalPath:
386
+ """Unit tests for _resolve_local_path (issue #18)."""
387
+
388
+ def test_relative_existing_path_resolved_to_absolute(self, tmp_path, monkeypatch):
389
+ monkeypatch.chdir(tmp_path)
390
+ f = Path("song.mp3")
391
+ f.touch()
392
+
393
+ resolved = _resolve_local_path("song.mp3")
394
+
395
+ assert resolved == str((tmp_path / "song.mp3").resolve())
396
+
397
+ def test_relative_missing_path_returned_unchanged(self, tmp_path, monkeypatch):
398
+ monkeypatch.chdir(tmp_path)
399
+
400
+ resolved = _resolve_local_path("missing.mp3")
401
+
402
+ assert resolved == "missing.mp3"
403
+
404
+ def test_absolute_path_returned_unchanged(self, tmp_path):
405
+ f = tmp_path / "song.mp3"
406
+ f.touch()
407
+
408
+ resolved = _resolve_local_path(str(f))
409
+
410
+ assert resolved == str(f)
411
+
412
+
413
+ class TestPlayRelativeLocalPath:
414
+ """`mc play <relative-file>` must send the daemon an absolute path.
415
+
416
+ The daemon is a separate long-running process — a relative path sent
417
+ as-is would be checked against the *daemon's* cwd, not the terminal's,
418
+ and would silently fail to be found (issue #18).
419
+ """
420
+
421
+ @patch("music_cli.cli.ensure_daemon")
422
+ @patch("music_cli.cli.check_ffplay_available", return_value=True)
423
+ def test_play_relative_file_sends_absolute_source(
424
+ self, mock_ffplay, mock_daemon, runner, mock_daemon_client, tmp_path, monkeypatch
425
+ ):
426
+ monkeypatch.chdir(tmp_path)
427
+ Path("song.mp3").touch()
428
+ mock_daemon.return_value = mock_daemon_client
429
+
430
+ result = runner.invoke(main, ["play", "song.mp3"])
431
+
432
+ assert result.exit_code == 0
433
+ call_kwargs = mock_daemon_client.play.call_args.kwargs
434
+ assert call_kwargs["mode"] == "local"
435
+ assert call_kwargs["source"] == str((tmp_path / "song.mp3").resolve())
436
+
437
+ @patch("music_cli.cli.ensure_daemon")
438
+ @patch("music_cli.cli.check_ffplay_available", return_value=True)
439
+ def test_play_explicit_mode_local_relative_file_sends_absolute_source(
440
+ self, mock_ffplay, mock_daemon, runner, mock_daemon_client, tmp_path, monkeypatch
441
+ ):
442
+ monkeypatch.chdir(tmp_path)
443
+ Path("song.mp3").touch()
444
+ mock_daemon.return_value = mock_daemon_client
445
+
446
+ result = runner.invoke(main, ["play", "song.mp3", "--mode", "local"])
447
+
448
+ assert result.exit_code == 0
449
+ call_kwargs = mock_daemon_client.play.call_args.kwargs
450
+ assert call_kwargs["source"] == str((tmp_path / "song.mp3").resolve())
451
+
452
+ @patch("music_cli.cli.ensure_daemon")
453
+ @patch("music_cli.cli.check_ffplay_available", return_value=True)
454
+ def test_play_explicit_mode_local_missing_file_kept_relative(
455
+ self, mock_ffplay, mock_daemon, runner, mock_daemon_client, tmp_path, monkeypatch
456
+ ):
457
+ # Not found in cwd -> left unchanged so the daemon still falls back
458
+ # to the configured music directory (today's behavior, preserved).
459
+ monkeypatch.chdir(tmp_path)
460
+ mock_daemon.return_value = mock_daemon_client
461
+
462
+ result = runner.invoke(main, ["play", "not-in-cwd.mp3", "--mode", "local"])
463
+
464
+ assert result.exit_code == 0
465
+ call_kwargs = mock_daemon_client.play.call_args.kwargs
466
+ assert call_kwargs["source"] == "not-in-cwd.mp3"
467
+
468
+
384
469
  # -------------------------------------------------------------------------
385
470
  # 2.2 — Deprecate play -m ai
386
471
  # -------------------------------------------------------------------------
@@ -0,0 +1,88 @@
1
+ """Tests for LocalSource relative-path resolution (issue #18)."""
2
+
3
+ from music_cli.sources.local import LocalSource
4
+
5
+
6
+ class TestGetTrackRelativePaths:
7
+ """get_track() should check the current working directory before
8
+ falling back to the configured music directory."""
9
+
10
+ def test_relative_path_found_in_cwd(self, tmp_path, monkeypatch):
11
+ cwd = tmp_path / "cwd"
12
+ music_dir = tmp_path / "music"
13
+ cwd.mkdir()
14
+ music_dir.mkdir()
15
+ (cwd / "song.mp3").touch()
16
+ monkeypatch.chdir(cwd)
17
+
18
+ source = LocalSource(music_dir=music_dir)
19
+ track = source.get_track("song.mp3")
20
+
21
+ assert track is not None
22
+ assert track.title == "song"
23
+ assert track.source == str((cwd / "song.mp3").resolve())
24
+
25
+ def test_relative_path_falls_back_to_music_dir(self, tmp_path, monkeypatch):
26
+ cwd = tmp_path / "cwd"
27
+ music_dir = tmp_path / "music"
28
+ cwd.mkdir()
29
+ music_dir.mkdir()
30
+ (music_dir / "song.mp3").touch()
31
+ monkeypatch.chdir(cwd)
32
+
33
+ source = LocalSource(music_dir=music_dir)
34
+ track = source.get_track("song.mp3")
35
+
36
+ assert track is not None
37
+ assert track.source == str(music_dir / "song.mp3")
38
+
39
+ def test_relative_path_not_found_anywhere_returns_none(self, tmp_path, monkeypatch):
40
+ cwd = tmp_path / "cwd"
41
+ music_dir = tmp_path / "music"
42
+ cwd.mkdir()
43
+ music_dir.mkdir()
44
+ monkeypatch.chdir(cwd)
45
+
46
+ source = LocalSource(music_dir=music_dir)
47
+ track = source.get_track("missing.mp3")
48
+
49
+ assert track is None
50
+
51
+ def test_cwd_match_takes_priority_over_music_dir(self, tmp_path, monkeypatch):
52
+ cwd = tmp_path / "cwd"
53
+ music_dir = tmp_path / "music"
54
+ cwd.mkdir()
55
+ music_dir.mkdir()
56
+ (cwd / "song.mp3").touch()
57
+ (music_dir / "song.mp3").touch()
58
+ monkeypatch.chdir(cwd)
59
+
60
+ source = LocalSource(music_dir=music_dir)
61
+ track = source.get_track("song.mp3")
62
+
63
+ assert track.source == str((cwd / "song.mp3").resolve())
64
+
65
+ def test_absolute_path_unaffected(self, tmp_path):
66
+ music_dir = tmp_path / "music"
67
+ music_dir.mkdir()
68
+ f = music_dir / "song.mp3"
69
+ f.touch()
70
+
71
+ source = LocalSource(music_dir=tmp_path / "other")
72
+ track = source.get_track(str(f))
73
+
74
+ assert track is not None
75
+ assert track.source == str(f)
76
+
77
+ def test_unsupported_extension_in_cwd_returns_none(self, tmp_path, monkeypatch):
78
+ cwd = tmp_path / "cwd"
79
+ music_dir = tmp_path / "music"
80
+ cwd.mkdir()
81
+ music_dir.mkdir()
82
+ (cwd / "notes.txt").touch()
83
+ monkeypatch.chdir(cwd)
84
+
85
+ source = LocalSource(music_dir=music_dir)
86
+ track = source.get_track("notes.txt")
87
+
88
+ assert track is None
File without changes