ytm-cli 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/PKG-INFO +12 -3
  2. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/README.md +10 -2
  3. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/pyproject.toml +2 -1
  4. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_dislikes.py +2 -7
  5. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_lyrics_service.py +6 -12
  6. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/__init__.py +1 -1
  7. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/dislikes.py +0 -3
  8. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/hybrid_player.py +6 -1
  9. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/lyrics_service.py +4 -8
  10. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/player.py +264 -181
  11. ytm_cli-0.7.0/ytm_cli/spectrum.py +149 -0
  12. ytm_cli-0.7.0/ytm_cli/ui.py +764 -0
  13. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli.egg-info/PKG-INFO +12 -3
  14. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli.egg-info/SOURCES.txt +1 -0
  15. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli.egg-info/requires.txt +1 -0
  16. ytm_cli-0.6.0/ytm_cli/ui.py +0 -451
  17. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/LICENSE +0 -0
  18. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/setup.cfg +0 -0
  19. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_config.py +0 -0
  20. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_ffmpeg_player.py +0 -0
  21. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_hybrid_player.py +0 -0
  22. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_integration.py +0 -0
  23. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_main.py +0 -0
  24. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_playlists.py +0 -0
  25. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_runner.py +0 -0
  26. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_ui.py +0 -0
  27. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/tests/test_utils.py +0 -0
  28. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/__main__.py +0 -0
  29. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/auth.py +0 -0
  30. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/cache.py +0 -0
  31. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/config.py +0 -0
  32. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/ffmpeg_player.py +0 -0
  33. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/llm_client.py +0 -0
  34. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/main.py +0 -0
  35. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/playlists.py +0 -0
  36. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/utils.py +0 -0
  37. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli/verbose_logger.py +0 -0
  38. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli.egg-info/dependency_links.txt +0 -0
  39. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli.egg-info/entry_points.txt +0 -0
  40. {ytm_cli-0.6.0 → ytm_cli-0.7.0}/ytm_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ytm-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: YouTube Music CLI tool with interactive terminal interface
5
5
  Author-email: thieuluan <thieuluan.dark@gmail.com>
6
6
  License-Expression: MIT
@@ -23,6 +23,7 @@ Requires-Dist: rich
23
23
  Requires-Dist: mpv
24
24
  Requires-Dist: requests
25
25
  Requires-Dist: pyperclip
26
+ Requires-Dist: numpy
26
27
  Dynamic: license-file
27
28
 
28
29
  # YTM - YouTube Music CLI
@@ -42,7 +43,7 @@ Stream music directly from YouTube Music in your terminal with intuitive control
42
43
  - **📱 Vim-like Navigation**: Use `j/k` keys or arrow keys to navigate
43
44
  - **📋 Local Playlists**: Create and manage personal playlists
44
45
  - **👎 Smart Filtering**: Dislike songs to filter them from future results
45
- - **📜 Lyrics Display**: View lyrics while listening (press `l`)
46
+ - **📜 Synced Lyrics**: Live-highlighted lyrics with auto-scroll (press `l`)
46
47
  - **🎯 Radio Mode**: Automatic playlist generation based on your selection
47
48
  - **🤖 AI-Powered**: Natural language music requests and AI-generated playlists
48
49
 
@@ -162,11 +163,19 @@ ytm-cli search "song" -s 1 -v --log-file debug.log # Save debug logs
162
163
  - `Space` - Play/pause
163
164
  - `n` - Next song
164
165
  - `b` - Previous song
165
- - `l` - Show lyrics
166
+ - `l` - Show lyrics (synced highlighting with auto-scroll)
166
167
  - `a` - Add to playlist
167
168
  - `d` - Dislike song (skip and filter from future results)
168
169
  - `q` - Quit
169
170
 
171
+ ### During Lyrics View
172
+
173
+ - `j/k` or `↑/↓` - Manual scroll
174
+ - `Space` - Re-enable auto-scroll
175
+ - `q` or `Esc` - Back to player
176
+ - `PgUp/PgDn` - Page scroll
177
+ - `Home/End` - Jump to top/bottom
178
+
170
179
  ## 📋 Playlist Management
171
180
 
172
181
  **Create and manage playlists:**
@@ -15,7 +15,7 @@ Stream music directly from YouTube Music in your terminal with intuitive control
15
15
  - **📱 Vim-like Navigation**: Use `j/k` keys or arrow keys to navigate
16
16
  - **📋 Local Playlists**: Create and manage personal playlists
17
17
  - **👎 Smart Filtering**: Dislike songs to filter them from future results
18
- - **📜 Lyrics Display**: View lyrics while listening (press `l`)
18
+ - **📜 Synced Lyrics**: Live-highlighted lyrics with auto-scroll (press `l`)
19
19
  - **🎯 Radio Mode**: Automatic playlist generation based on your selection
20
20
  - **🤖 AI-Powered**: Natural language music requests and AI-generated playlists
21
21
 
@@ -135,11 +135,19 @@ ytm-cli search "song" -s 1 -v --log-file debug.log # Save debug logs
135
135
  - `Space` - Play/pause
136
136
  - `n` - Next song
137
137
  - `b` - Previous song
138
- - `l` - Show lyrics
138
+ - `l` - Show lyrics (synced highlighting with auto-scroll)
139
139
  - `a` - Add to playlist
140
140
  - `d` - Dislike song (skip and filter from future results)
141
141
  - `q` - Quit
142
142
 
143
+ ### During Lyrics View
144
+
145
+ - `j/k` or `↑/↓` - Manual scroll
146
+ - `Space` - Re-enable auto-scroll
147
+ - `q` or `Esc` - Back to player
148
+ - `PgUp/PgDn` - Page scroll
149
+ - `Home/End` - Jump to top/bottom
150
+
143
151
  ## 📋 Playlist Management
144
152
 
145
153
  **Create and manage playlists:**
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ytm-cli"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "YouTube Music CLI tool with interactive terminal interface"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "mpv",
29
29
  "requests",
30
30
  "pyperclip",
31
+ "numpy",
31
32
  ]
32
33
 
33
34
  [project.urls]
@@ -216,8 +216,7 @@ class TestFilterDislikedSongs:
216
216
 
217
217
  manager = DislikeManager(dislikes_file)
218
218
 
219
- with patch("ytm_cli.dislikes.print") as mock_print:
220
- result = manager.filter_disliked_songs(sample_songs)
219
+ result = manager.filter_disliked_songs(sample_songs)
221
220
 
222
221
  # Should filter out song1
223
222
  assert len(result) == 2
@@ -226,8 +225,6 @@ class TestFilterDislikedSongs:
226
225
  assert "song2" in video_ids
227
226
  assert "song3" in video_ids
228
227
 
229
- mock_print.assert_called_with("[yellow]Filtered out 1 disliked song(s)[/yellow]")
230
-
231
228
  def test_filter_disliked_songs_all_disliked(self, temp_dir, sample_songs):
232
229
  """Test filtering when all songs are disliked"""
233
230
  dislikes_data = {
@@ -244,11 +241,9 @@ class TestFilterDislikedSongs:
244
241
 
245
242
  manager = DislikeManager(dislikes_file)
246
243
 
247
- with patch("ytm_cli.dislikes.print") as mock_print:
248
- result = manager.filter_disliked_songs(sample_songs)
244
+ result = manager.filter_disliked_songs(sample_songs)
249
245
 
250
246
  assert len(result) == 0
251
- mock_print.assert_called_with("[yellow]Filtered out 3 disliked song(s)[/yellow]")
252
247
 
253
248
  def test_filter_disliked_songs_missing_video_id(self, temp_dir):
254
249
  """Test filtering songs with missing videoId"""
@@ -50,7 +50,7 @@ class TestLyricsService:
50
50
  "album_name": "Test Album",
51
51
  "duration": 225,
52
52
  },
53
- timeout=10,
53
+ timeout=5,
54
54
  )
55
55
 
56
56
  def test_get_lyrics_without_optional_params(self, sample_lyrics_response):
@@ -69,7 +69,7 @@ class TestLyricsService:
69
69
  mock_get.assert_called_once_with(
70
70
  f"{service.base_url}/get",
71
71
  params={"track_name": "Test Song", "artist_name": "Test Artist"},
72
- timeout=10,
72
+ timeout=5,
73
73
  )
74
74
 
75
75
  def test_get_lyrics_not_found(self):
@@ -94,11 +94,9 @@ class TestLyricsService:
94
94
  "get",
95
95
  side_effect=requests.RequestException("Network error"),
96
96
  ):
97
- with patch("builtins.print") as mock_print:
98
- result = service.get_lyrics("Test Song", "Test Artist")
97
+ result = service.get_lyrics("Test Song", "Test Artist")
99
98
 
100
99
  assert result is None
101
- mock_print.assert_called_with("Error fetching lyrics from LRCLIB: Network error")
102
100
 
103
101
  def test_search_lyrics_success(self):
104
102
  """Test successful lyrics search"""
@@ -120,7 +118,7 @@ class TestLyricsService:
120
118
  mock_get.assert_called_once_with(
121
119
  f"{service.base_url}/search",
122
120
  params={"track_name": "Test Song"},
123
- timeout=10,
121
+ timeout=5,
124
122
  )
125
123
 
126
124
  def test_search_lyrics_no_results(self):
@@ -146,11 +144,9 @@ class TestLyricsService:
146
144
  mock_response.status_code = 500
147
145
  mock_get.return_value = mock_response
148
146
 
149
- with patch("builtins.print") as mock_print:
150
- result = service.search_lyrics("Test Song")
147
+ result = service.search_lyrics("Test Song")
151
148
 
152
149
  assert result == []
153
- mock_print.assert_called_with("LRCLIB search error: 500")
154
150
 
155
151
  def test_search_lyrics_network_error(self):
156
152
  """Test lyrics search with network error"""
@@ -161,11 +157,9 @@ class TestLyricsService:
161
157
  "get",
162
158
  side_effect=requests.RequestException("Network error"),
163
159
  ):
164
- with patch("builtins.print") as mock_print:
165
- result = service.search_lyrics("Test Song")
160
+ result = service.search_lyrics("Test Song")
166
161
 
167
162
  assert result == []
168
- mock_print.assert_called_with("Error searching lyrics from LRCLIB: Network error")
169
163
 
170
164
 
171
165
  class TestLRCParser:
@@ -1,3 +1,3 @@
1
1
  """YouTube Music CLI - A terminal interface for YouTube Music"""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
@@ -116,9 +116,6 @@ class DislikeManager:
116
116
  continue
117
117
  filtered.append(song)
118
118
 
119
- if filtered_count > 0:
120
- print(f"[yellow]Filtered out {filtered_count} disliked song(s)[/yellow]")
121
-
122
119
  return filtered
123
120
 
124
121
  def get_disliked_songs(self) -> list[dict[str, Any]]:
@@ -79,7 +79,12 @@ class CLIHybridPlayerService:
79
79
 
80
80
  url = f"https://music.youtube.com/watch?v={video_id}"
81
81
  mpv_flags = get_mpv_flags()
82
- mpv_flags.extend([f"--input-ipc-server={self.socket_path}"])
82
+ mpv_flags.extend(
83
+ [
84
+ f"--input-ipc-server={self.socket_path}",
85
+ "--af-append=@vstats:lavfi=[astats=metadata=1:reset=1:length=0.1]",
86
+ ]
87
+ )
83
88
 
84
89
  log_info(f"Starting MPV playback: {title or video_id}")
85
90
  self.mpv_process = subprocess.Popen(
@@ -43,16 +43,14 @@ class LyricsService:
43
43
  params["duration"] = duration
44
44
 
45
45
  try:
46
- response = self.session.get(f"{self.base_url}/get", params=params, timeout=10)
46
+ response = self.session.get(f"{self.base_url}/get", params=params, timeout=5)
47
47
  if response.status_code == 200:
48
48
  return response.json()
49
49
  elif response.status_code == 404:
50
50
  return None
51
51
  else:
52
- print(f"LRCLIB API error: {response.status_code}")
53
52
  return None
54
- except requests.RequestException as e:
55
- print(f"Error fetching lyrics from LRCLIB: {e}")
53
+ except requests.RequestException:
56
54
  return None
57
55
 
58
56
  def search_lyrics(self, track_name: str) -> list[dict]:
@@ -68,14 +66,12 @@ class LyricsService:
68
66
  params = {"track_name": track_name}
69
67
 
70
68
  try:
71
- response = self.session.get(f"{self.base_url}/search", params=params, timeout=10)
69
+ response = self.session.get(f"{self.base_url}/search", params=params, timeout=5)
72
70
  if response.status_code == 200:
73
71
  return response.json()
74
72
  else:
75
- print(f"LRCLIB search error: {response.status_code}")
76
73
  return []
77
- except requests.RequestException as e:
78
- print(f"Error searching lyrics from LRCLIB: {e}")
74
+ except requests.RequestException:
79
75
  return []
80
76
 
81
77