ytm-cli 0.7.1__tar.gz → 0.7.2__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 (43) hide show
  1. {ytm_cli-0.7.1/ytm_cli.egg-info → ytm_cli-0.7.2}/PKG-INFO +29 -37
  2. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/README.md +26 -35
  3. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/pyproject.toml +13 -2
  4. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_config.py +6 -22
  5. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_dislikes.py +4 -2
  6. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_integration.py +2 -168
  7. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_main.py +13 -7
  8. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_playlists.py +8 -8
  9. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/__init__.py +1 -1
  10. ytm_cli-0.7.2/ytm_cli/config.py +49 -0
  11. ytm_cli-0.7.2/ytm_cli/demo.py +120 -0
  12. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/dislikes.py +11 -2
  13. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/llm_client.py +5 -3
  14. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/main.py +15 -1
  15. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/player.py +27 -7
  16. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/playlists.py +17 -8
  17. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/ui.py +3 -3
  18. {ytm_cli-0.7.1 → ytm_cli-0.7.2/ytm_cli.egg-info}/PKG-INFO +29 -37
  19. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli.egg-info/SOURCES.txt +1 -1
  20. ytm_cli-0.7.1/ytm_cli/auth.py +0 -697
  21. ytm_cli-0.7.1/ytm_cli/config.py +0 -29
  22. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/LICENSE +0 -0
  23. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/setup.cfg +0 -0
  24. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_cache.py +0 -0
  25. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_ffmpeg_player.py +0 -0
  26. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_hybrid_player.py +0 -0
  27. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_lyrics_service.py +0 -0
  28. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_runner.py +0 -0
  29. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_spectrum.py +0 -0
  30. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_ui.py +0 -0
  31. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_utils.py +0 -0
  32. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/__main__.py +0 -0
  33. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/cache.py +0 -0
  34. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/ffmpeg_player.py +0 -0
  35. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/hybrid_player.py +0 -0
  36. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/lyrics_service.py +0 -0
  37. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/spectrum.py +0 -0
  38. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/utils.py +0 -0
  39. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/verbose_logger.py +0 -0
  40. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli.egg-info/dependency_links.txt +0 -0
  41. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli.egg-info/entry_points.txt +0 -0
  42. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli.egg-info/requires.txt +0 -0
  43. {ytm_cli-0.7.1 → ytm_cli-0.7.2}/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.7.1
3
+ Version: 0.7.2
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
@@ -11,7 +11,8 @@ Keywords: youtube-music,cli,terminal,music-player,mpv
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Intended Audience :: End Users/Desktop
14
- Classifier: Operating System :: OS Independent
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: MacOS
15
16
  Classifier: Programming Language :: Python :: 3
16
17
  Classifier: Programming Language :: Python :: 3.14
17
18
  Classifier: Topic :: Multimedia :: Sound/Audio :: Players
@@ -32,9 +33,16 @@ Dynamic: license-file
32
33
 
33
34
  Stream music directly from YouTube Music in your terminal with intuitive controls, playlist management, and smart filtering.
34
35
 
35
- ![YTM CLI Player](image.png)
36
+ <p align="center">
37
+ <a href="https://github.com/thieuluan1618/ytm/raw/main/screenshots/player.webm">
38
+ <img src="screenshots/player.gif" alt="ytm-cli player demo" width="800">
39
+ </a>
40
+ </p>
36
41
 
37
- ![YTM CLI Lyrics](image-lyric.png)
42
+ <p align="center">
43
+ <em>Player UI with the real-time FFT spectrum visualizer (synthetic demo capture).
44
+ Click for the higher-quality .webm.</em>
45
+ </p>
38
46
 
39
47
  ## ✨ Features
40
48
 
@@ -45,21 +53,23 @@ Stream music directly from YouTube Music in your terminal with intuitive control
45
53
  - **👎 Smart Filtering**: Dislike songs to filter them from future results
46
54
  - **📜 Synced Lyrics**: Live-highlighted lyrics with auto-scroll (press `l`)
47
55
  - **🎯 Radio Mode**: Automatic playlist generation based on your selection
56
+ - **📊 Real-time Spectrum Visualizer**: 24-band FFT bars driven by an `ffmpeg` sidecar — bass left, treble right (stereo oscilloscope fallback when `ffmpeg` is unavailable)
48
57
  - **🤖 AI-Powered**: Natural language music requests and AI-generated playlists
49
58
 
50
59
  ## 🚀 Quick Start
51
60
 
52
61
  ### Requirements
53
62
 
54
- - Python 3.8+
63
+ - Python **3.14+** (set in `pyproject.toml`)
55
64
  - [mpv media player](https://mpv.io/installation/) (must be installed system-wide)
65
+ - [`ffmpeg`](https://ffmpeg.org/) on `PATH` for the real-time spectrum visualizer (optional — falls back to a stereo oscilloscope if missing)
56
66
 
57
67
  ### Installation
58
68
 
59
69
  **One command with [uv](https://docs.astral.sh/uv/) (recommended):**
60
70
 
61
71
  ```bash
62
- # Install from PyPI
72
+ # Install from PyPI — gets latest version automatically
63
73
  uvx ytm-cli "song name"
64
74
  ```
65
75
 
@@ -75,36 +85,16 @@ uv run ytm-cli "song name"
75
85
  </details>
76
86
 
77
87
  <details>
78
- <summary>Alternative: one-shot setup scripts</summary>
88
+ <summary>Alternative: one-shot setup script (Linux / macOS)</summary>
79
89
 
80
90
  ```bash
81
91
  git clone https://github.com/thieuluan1618/ytm.git
82
92
  cd ytm
83
- ```
84
-
85
- These create the virtual environment, install dependencies, and configure the `ytm` command globally.
86
-
87
- **Linux/macOS:**
88
-
89
- ```bash
90
93
  ./setup.sh
91
- source ~/.zshrc # or ~/.bashrc for bash
92
- ```
93
-
94
- **Windows PowerShell:**
95
-
96
- ```powershell
97
- .\setup.ps1
98
- . $PROFILE
99
- ```
100
-
101
- **Windows Command Prompt:**
102
-
103
- ```cmd
104
- setup.bat
94
+ source ~/.zshrc # or ~/.bashrc for bash
105
95
  ```
106
96
 
107
- After setup, use `ytm` (alias for `ytm-cli`) from anywhere.
97
+ This creates the venv, installs runtime deps from `pyproject.toml`, and adds a `ytm` shell alias.
108
98
 
109
99
  </details>
110
100
 
@@ -114,14 +104,15 @@ After setup, use `ytm` (alias for `ytm-cli`) from anywhere.
114
104
  ```bash
115
105
  git clone https://github.com/thieuluan1618/ytm.git
116
106
  cd ytm
117
- python -m venv venv
118
- source venv/bin/activate # Windows: venv\Scripts\activate
119
- pip install -r requirements.txt
120
- ./setup_alias.sh # or setup_alias.ps1 / setup_alias.bat
107
+ uv sync # installs runtime deps from pyproject.toml + uv.lock
108
+ ./setup_alias.sh # optional: registers the `ytm` alias
109
+ # Run with: uv run ytm-cli "song"
121
110
  ```
122
111
 
123
112
  </details>
124
113
 
114
+ > **Windows users:** native Windows is not supported (the codebase uses POSIX-only `termios`/`tty`/`fcntl`). Use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) and follow the Linux instructions above.
115
+
125
116
  ### Basic Usage
126
117
 
127
118
  **Interactive search:**
@@ -140,9 +131,10 @@ ytm-cli "your favorite song"
140
131
  **Non-interactive mode (automation/scripting):**
141
132
 
142
133
  ```bash
143
- ytm-cli search "song name" --select 1 # Auto-select first result
144
- ytm-cli search "song" -s 1 -v # With verbose output
145
- ytm-cli search "song" -s 1 -v --log-file debug.log # Save debug logs
134
+ ytm-cli search "song name" --select 1 # Auto-select first result
135
+ ytm-cli search "song" -s 1 --verbose # With verbose output
136
+ ytm-cli search "song" -s 1 --verbose --log-file debug.log # Save debug logs
137
+ ytm-cli -v # Print version
146
138
  ```
147
139
 
148
140
  > **Tip:** Set up the shell alias to use `ytm` as a shortcut for `ytm-cli`. See the setup scripts above.
@@ -249,7 +241,7 @@ Having issues? Check out the [**Troubleshooting Guide**](TROUBLESHOOTING.md) for
249
241
 
250
242
  ```bash
251
243
  # Enable verbose logging to see what's happening
252
- ytm-cli search "test" -s 1 -v --log-file debug.log
244
+ ytm-cli search "test" -s 1 --verbose --log-file debug.log
253
245
 
254
246
  # Check versions
255
247
  mpv --version
@@ -4,9 +4,16 @@
4
4
 
5
5
  Stream music directly from YouTube Music in your terminal with intuitive controls, playlist management, and smart filtering.
6
6
 
7
- ![YTM CLI Player](image.png)
7
+ <p align="center">
8
+ <a href="https://github.com/thieuluan1618/ytm/raw/main/screenshots/player.webm">
9
+ <img src="screenshots/player.gif" alt="ytm-cli player demo" width="800">
10
+ </a>
11
+ </p>
8
12
 
9
- ![YTM CLI Lyrics](image-lyric.png)
13
+ <p align="center">
14
+ <em>Player UI with the real-time FFT spectrum visualizer (synthetic demo capture).
15
+ Click for the higher-quality .webm.</em>
16
+ </p>
10
17
 
11
18
  ## ✨ Features
12
19
 
@@ -17,21 +24,23 @@ Stream music directly from YouTube Music in your terminal with intuitive control
17
24
  - **👎 Smart Filtering**: Dislike songs to filter them from future results
18
25
  - **📜 Synced Lyrics**: Live-highlighted lyrics with auto-scroll (press `l`)
19
26
  - **🎯 Radio Mode**: Automatic playlist generation based on your selection
27
+ - **📊 Real-time Spectrum Visualizer**: 24-band FFT bars driven by an `ffmpeg` sidecar — bass left, treble right (stereo oscilloscope fallback when `ffmpeg` is unavailable)
20
28
  - **🤖 AI-Powered**: Natural language music requests and AI-generated playlists
21
29
 
22
30
  ## 🚀 Quick Start
23
31
 
24
32
  ### Requirements
25
33
 
26
- - Python 3.8+
34
+ - Python **3.14+** (set in `pyproject.toml`)
27
35
  - [mpv media player](https://mpv.io/installation/) (must be installed system-wide)
36
+ - [`ffmpeg`](https://ffmpeg.org/) on `PATH` for the real-time spectrum visualizer (optional — falls back to a stereo oscilloscope if missing)
28
37
 
29
38
  ### Installation
30
39
 
31
40
  **One command with [uv](https://docs.astral.sh/uv/) (recommended):**
32
41
 
33
42
  ```bash
34
- # Install from PyPI
43
+ # Install from PyPI — gets latest version automatically
35
44
  uvx ytm-cli "song name"
36
45
  ```
37
46
 
@@ -47,36 +56,16 @@ uv run ytm-cli "song name"
47
56
  </details>
48
57
 
49
58
  <details>
50
- <summary>Alternative: one-shot setup scripts</summary>
59
+ <summary>Alternative: one-shot setup script (Linux / macOS)</summary>
51
60
 
52
61
  ```bash
53
62
  git clone https://github.com/thieuluan1618/ytm.git
54
63
  cd ytm
55
- ```
56
-
57
- These create the virtual environment, install dependencies, and configure the `ytm` command globally.
58
-
59
- **Linux/macOS:**
60
-
61
- ```bash
62
64
  ./setup.sh
63
- source ~/.zshrc # or ~/.bashrc for bash
64
- ```
65
-
66
- **Windows PowerShell:**
67
-
68
- ```powershell
69
- .\setup.ps1
70
- . $PROFILE
71
- ```
72
-
73
- **Windows Command Prompt:**
74
-
75
- ```cmd
76
- setup.bat
65
+ source ~/.zshrc # or ~/.bashrc for bash
77
66
  ```
78
67
 
79
- After setup, use `ytm` (alias for `ytm-cli`) from anywhere.
68
+ This creates the venv, installs runtime deps from `pyproject.toml`, and adds a `ytm` shell alias.
80
69
 
81
70
  </details>
82
71
 
@@ -86,14 +75,15 @@ After setup, use `ytm` (alias for `ytm-cli`) from anywhere.
86
75
  ```bash
87
76
  git clone https://github.com/thieuluan1618/ytm.git
88
77
  cd ytm
89
- python -m venv venv
90
- source venv/bin/activate # Windows: venv\Scripts\activate
91
- pip install -r requirements.txt
92
- ./setup_alias.sh # or setup_alias.ps1 / setup_alias.bat
78
+ uv sync # installs runtime deps from pyproject.toml + uv.lock
79
+ ./setup_alias.sh # optional: registers the `ytm` alias
80
+ # Run with: uv run ytm-cli "song"
93
81
  ```
94
82
 
95
83
  </details>
96
84
 
85
+ > **Windows users:** native Windows is not supported (the codebase uses POSIX-only `termios`/`tty`/`fcntl`). Use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) and follow the Linux instructions above.
86
+
97
87
  ### Basic Usage
98
88
 
99
89
  **Interactive search:**
@@ -112,9 +102,10 @@ ytm-cli "your favorite song"
112
102
  **Non-interactive mode (automation/scripting):**
113
103
 
114
104
  ```bash
115
- ytm-cli search "song name" --select 1 # Auto-select first result
116
- ytm-cli search "song" -s 1 -v # With verbose output
117
- ytm-cli search "song" -s 1 -v --log-file debug.log # Save debug logs
105
+ ytm-cli search "song name" --select 1 # Auto-select first result
106
+ ytm-cli search "song" -s 1 --verbose # With verbose output
107
+ ytm-cli search "song" -s 1 --verbose --log-file debug.log # Save debug logs
108
+ ytm-cli -v # Print version
118
109
  ```
119
110
 
120
111
  > **Tip:** Set up the shell alias to use `ytm` as a shortcut for `ytm-cli`. See the setup scripts above.
@@ -221,7 +212,7 @@ Having issues? Check out the [**Troubleshooting Guide**](TROUBLESHOOTING.md) for
221
212
 
222
213
  ```bash
223
214
  # Enable verbose logging to see what's happening
224
- ytm-cli search "test" -s 1 -v --log-file debug.log
215
+ ytm-cli search "test" -s 1 --verbose --log-file debug.log
225
216
 
226
217
  # Check versions
227
218
  mpv --version
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ytm-cli"
7
- version = "0.7.1"
7
+ version = "0.7.2"
8
8
  description = "YouTube Music CLI tool with interactive terminal interface"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -17,7 +17,8 @@ classifiers = [
17
17
  "Development Status :: 4 - Beta",
18
18
  "Environment :: Console",
19
19
  "Intended Audience :: End Users/Desktop",
20
- "Operating System :: OS Independent",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Operating System :: MacOS",
21
22
  "Programming Language :: Python :: 3",
22
23
  "Programming Language :: Python :: 3.14",
23
24
  "Topic :: Multimedia :: Sound/Audio :: Players",
@@ -31,6 +32,16 @@ dependencies = [
31
32
  "numpy",
32
33
  ]
33
34
 
35
+ [dependency-groups]
36
+ dev = [
37
+ "ruff>=0.14.0",
38
+ "pytest>=9.0.3",
39
+ "pytest-cov>=6.0.0",
40
+ "pytest-mock>=3.15.0",
41
+ "coverage>=7.0.0",
42
+ "yt-dlp>=2024.0.0",
43
+ ]
44
+
34
45
  [project.urls]
35
46
  Homepage = "https://github.com/thieuluan1618/ytm"
36
47
  Repository = "https://github.com/thieuluan1618/ytm"
@@ -8,7 +8,7 @@ import pytest
8
8
 
9
9
  # We need to mock the imports before importing the config module
10
10
  # since it initializes global objects on import
11
- with patch("ytm_cli.config.AuthManager"), patch("ytm_cli.config.configparser.ConfigParser"):
11
+ with patch("ytm_cli.config.configparser.ConfigParser"):
12
12
  from ytm_cli.config import get_mpv_flags, get_songs_to_display
13
13
 
14
14
 
@@ -139,10 +139,9 @@ class TestConfigModule:
139
139
  """Tests for config module initialization and behavior"""
140
140
 
141
141
  def test_config_reads_file(self):
142
- """Test that config reads from config.ini file"""
142
+ """Test that config reads from ~/.config/ytm-cli/config.ini"""
143
143
  with (
144
144
  patch("ytm_cli.config.configparser.ConfigParser") as mock_parser_class,
145
- patch("ytm_cli.config.AuthManager"),
146
145
  ):
147
146
  mock_parser = Mock()
148
147
  mock_parser_class.return_value = mock_parser
@@ -154,25 +153,10 @@ class TestConfigModule:
154
153
 
155
154
  importlib.reload(ytm_cli.config)
156
155
 
157
- mock_parser.read.assert_called_with("config.ini")
158
-
159
- def test_auth_manager_initialization(self):
160
- """Test that AuthManager is properly initialized"""
161
- mock_auth_manager = Mock()
162
- mock_auth_manager.get_ytmusic_instance.return_value = Mock()
163
-
164
- with patch(
165
- "ytm_cli.auth.AuthManager", return_value=mock_auth_manager
166
- ) as mock_auth_manager_class:
167
- # Re-import to trigger initialization
168
- import importlib
169
-
170
- import ytm_cli.config
171
-
172
- importlib.reload(ytm_cli.config)
173
-
174
- mock_auth_manager_class.assert_called_once()
175
- mock_auth_manager.get_ytmusic_instance.assert_called_once()
156
+ # Config now lives under ~/.config/ytm-cli/
157
+ mock_parser.read.assert_called_once()
158
+ called_path = mock_parser.read.call_args[0][0]
159
+ assert called_path.endswith(os.path.join(".config", "ytm-cli", "config.ini"))
176
160
 
177
161
 
178
162
  class TestConfigIntegration:
@@ -11,11 +11,13 @@ class TestDislikeManagerInit:
11
11
  """Tests for DislikeManager initialization"""
12
12
 
13
13
  def test_init_default_file(self):
14
- """Test initialization with default file"""
14
+ """Test initialization with default file (~/.config/ytm-cli/dislikes.json)"""
15
15
  with patch("os.path.exists", return_value=False):
16
16
  manager = DislikeManager()
17
17
 
18
- assert manager.dislikes_file == "dislikes.json"
18
+ assert manager.dislikes_file.endswith(
19
+ os.path.join(".config", "ytm-cli", "dislikes.json")
20
+ )
19
21
  assert manager._disliked_ids == set()
20
22
 
21
23
  def test_init_custom_file(self):
@@ -1,10 +1,8 @@
1
1
  """Integration tests for YTM CLI components"""
2
2
 
3
- import json
4
3
  import os
5
- from unittest.mock import Mock, patch
4
+ from unittest.mock import patch
6
5
 
7
- from ytm_cli.auth import AuthManager
8
6
  from ytm_cli.dislikes import DislikeManager
9
7
  from ytm_cli.playlists import PlaylistManager
10
8
 
@@ -48,121 +46,6 @@ class TestPlaylistDislikeIntegration:
48
46
 
49
47
  # Should have 2 songs left (song1 and song3)
50
48
  assert len(filtered_songs) == 2
51
- video_ids = [song["videoId"] for song in filtered_songs]
52
- assert "song1" in video_ids
53
- assert "song3" in video_ids
54
- assert "song2" not in video_ids
55
-
56
- def test_playlist_creation_with_auth_enabled(self, temp_dir):
57
- """Test playlist creation when authentication is enabled"""
58
- # Setup auth manager with enabled auth
59
- config_file = os.path.join(temp_dir, "config.ini")
60
- auth_manager = AuthManager(config_path=config_file)
61
-
62
- auth_manager._update_auth_config("oauth")
63
-
64
- # Setup playlist manager
65
- playlist_manager = PlaylistManager(temp_dir)
66
-
67
- # Create playlist - should work regardless of auth status
68
- result = playlist_manager.create_playlist("Auth Test Playlist")
69
-
70
- assert result is True
71
- assert auth_manager.is_auth_enabled() is True
72
-
73
- # Verify playlist exists
74
- playlists = playlist_manager.list_playlists()
75
- assert len(playlists) == 1
76
- assert playlists[0]["name"] == "Auth Test Playlist"
77
-
78
- def test_dislike_persistence_across_playlist_operations(self, temp_dir, sample_songs):
79
- """Test that dislikes persist across playlist operations"""
80
- dislikes_file = os.path.join(temp_dir, "dislikes.json")
81
- dislike_manager = DislikeManager(dislikes_file)
82
- playlist_manager = PlaylistManager(temp_dir)
83
-
84
- # Dislike a song first
85
- with patch("builtins.print"):
86
- dislike_manager.dislike_song(sample_songs[0])
87
-
88
- # Create playlist and add songs
89
- with patch("builtins.print"):
90
- playlist_manager.create_playlist("Test Playlist")
91
- for song in sample_songs:
92
- playlist_manager.add_song_to_playlist("Test Playlist", song)
93
-
94
- # Create new dislike manager instance to test persistence
95
- dislike_manager2 = DislikeManager(dislikes_file)
96
-
97
- # Should still have the disliked song
98
- assert dislike_manager2.is_disliked("song1")
99
- assert dislike_manager2.get_dislike_count() == 1
100
-
101
-
102
- class TestAuthConfigIntegration:
103
- """Integration tests between auth and config management"""
104
-
105
- def test_auth_config_file_management(self, temp_dir):
106
- """Test auth configuration file management"""
107
- config_file = os.path.join(temp_dir, "config.ini")
108
-
109
- auth_manager = AuthManager(config_path=config_file)
110
-
111
- # Initially no auth
112
- assert auth_manager.is_auth_enabled() is False
113
-
114
- # Enable OAuth
115
- auth_manager._update_auth_config("oauth")
116
-
117
- assert auth_manager.is_auth_enabled() is True
118
- assert auth_manager.get_auth_method() == "oauth"
119
-
120
- # Create new instance to test persistence
121
- auth_manager2 = AuthManager(config_path=config_file)
122
- assert auth_manager2.is_auth_enabled() is True
123
- assert auth_manager2.get_auth_method() == "oauth"
124
-
125
- # Disable auth
126
- auth_manager2._update_auth_config("none", enabled=False)
127
-
128
- # Create third instance to verify disable persisted
129
- auth_manager3 = AuthManager(config_path=config_file)
130
- assert auth_manager3.is_auth_enabled() is False
131
-
132
- def test_auth_with_ytmusic_instance_creation(self, temp_dir):
133
- """Test YTMusic instance creation with different auth states"""
134
- config_file = os.path.join(temp_dir, "config.ini")
135
- oauth_file = os.path.join(temp_dir, "oauth.json")
136
-
137
- auth_manager = AuthManager(config_path=config_file)
138
- auth_manager.oauth_file = oauth_file
139
-
140
- # Test unauthenticated instance
141
- with patch("ytm_cli.auth.YTMusic") as mock_ytmusic, patch("ytm_cli.auth.print"):
142
- mock_instance = Mock()
143
- mock_ytmusic.return_value = mock_instance
144
-
145
- instance = auth_manager.get_ytmusic_instance()
146
-
147
- assert instance == mock_instance
148
- mock_ytmusic.assert_called_once_with()
149
-
150
- # Enable auth and create OAuth file
151
- oauth_data = {"access_token": "test_token"}
152
- with open(oauth_file, "w") as f:
153
- json.dump(oauth_data, f)
154
-
155
- auth_manager._update_auth_config("oauth")
156
-
157
- # Test authenticated instance
158
- with patch("ytm_cli.auth.YTMusic") as mock_ytmusic, patch("ytm_cli.auth.print"):
159
- mock_instance = Mock()
160
- mock_ytmusic.return_value = mock_instance
161
-
162
- instance = auth_manager.get_ytmusic_instance()
163
-
164
- assert instance == mock_instance
165
- mock_ytmusic.assert_called_once_with(oauth_file)
166
49
 
167
50
 
168
51
  class TestFullWorkflowIntegration:
@@ -224,55 +107,6 @@ class TestFullWorkflowIntegration:
224
107
  deleted_playlist = playlist_manager.get_playlist("My Workflow Playlist")
225
108
  assert deleted_playlist is None
226
109
 
227
- def test_auth_setup_to_usage_workflow(self, temp_dir):
228
- """Test complete auth workflow from setup to usage"""
229
- config_file = os.path.join(temp_dir, "config.ini")
230
- oauth_file = os.path.join(temp_dir, "oauth.json")
231
-
232
- auth_manager = AuthManager(config_path=config_file)
233
- auth_manager.oauth_file = oauth_file
234
-
235
- with patch("ytm_cli.auth.print"):
236
- # Setup OAuth
237
- mock_oauth_data = {
238
- "access_token": "test_token",
239
- "refresh_token": "refresh_token",
240
- }
241
-
242
- with patch("ytm_cli.auth.setup_oauth"):
243
- result = auth_manager.setup_oauth_auth("client_id", "client_secret")
244
- assert result is True
245
-
246
- # Verify auth is enabled
247
- assert auth_manager.is_auth_enabled() is True
248
- assert auth_manager.get_auth_method() == "oauth"
249
-
250
- # Verify OAuth file was created by setup_oauth_auth
251
- # (setup_oauth is mocked, so we create the file manually for downstream tests)
252
- with open(oauth_file, "w") as f:
253
- json.dump(mock_oauth_data, f)
254
-
255
- assert os.path.exists(oauth_file)
256
- with open(oauth_file) as f:
257
- saved_data = json.load(f)
258
- assert saved_data == mock_oauth_data
259
-
260
- # Test YTMusic instance creation
261
- with patch("ytm_cli.auth.YTMusic") as mock_ytmusic:
262
- mock_instance = Mock()
263
- mock_ytmusic.return_value = mock_instance
264
-
265
- instance = auth_manager.get_ytmusic_instance()
266
-
267
- assert instance == mock_instance
268
- mock_ytmusic.assert_called_once_with(oauth_file)
269
-
270
- # Disable auth
271
- with patch("builtins.input", return_value="n"):
272
- result = auth_manager.disable_auth()
273
- assert result is True
274
- assert auth_manager.is_auth_enabled() is False
275
-
276
110
  def test_error_recovery_workflow(self, temp_dir, sample_songs):
277
111
  """Test error recovery in integrated workflows"""
278
112
  playlist_manager = PlaylistManager(temp_dir)
@@ -285,7 +119,7 @@ class TestFullWorkflowIntegration:
285
119
 
286
120
  # Try to add song with missing data
287
121
  incomplete_song = {"title": "Incomplete Song"} # Missing videoId
288
- result = playlist_manager.add_song_to_playlist("Error Test Playlist", incomplete_song)
122
+ playlist_manager.add_song_to_playlist("Error Test Playlist", incomplete_song)
289
123
  # Should handle gracefully (behavior depends on implementation)
290
124
 
291
125
  # Add valid songs
@@ -7,7 +7,7 @@ import pytest
7
7
  # Mock the imports before importing main to avoid initialization issues
8
8
  with (
9
9
  patch("ytm_cli.main.get_songs_to_display"),
10
- patch("ytm_cli.main.ytmusic"),
10
+ patch("ytm_cli.main.get_ytmusic"),
11
11
  patch("ytm_cli.main.dislike_manager"),
12
12
  patch("ytm_cli.main.playlist_manager"),
13
13
  patch("ytm_cli.main.setup_signal_handler"),
@@ -29,13 +29,14 @@ class TestSearchAndPlay:
29
29
  def test_search_and_play_with_query(self, sample_songs):
30
30
  """Test search and play with provided query"""
31
31
  with (
32
- patch("ytm_cli.main.ytmusic") as mock_ytmusic,
32
+ patch("ytm_cli.main.get_ytmusic") as mock_get_ytmusic,
33
33
  patch("ytm_cli.main.dislike_manager") as mock_dislike_manager,
34
34
  patch("ytm_cli.main.get_songs_to_display", return_value=5),
35
35
  patch("ytm_cli.main.wrapper", return_value=0),
36
36
  patch("ytm_cli.main.play_music_with_controls"),
37
37
  patch("ytm_cli.main.print") as mock_print,
38
38
  ):
39
+ mock_ytmusic = mock_get_ytmusic.return_value
39
40
  mock_ytmusic.search.return_value = sample_songs
40
41
  mock_dislike_manager.filter_disliked_songs.return_value = sample_songs
41
42
  mock_ytmusic.get_watch_playlist.return_value = {"tracks": []}
@@ -48,13 +49,14 @@ class TestSearchAndPlay:
48
49
  def test_search_and_play_no_query_prompts_input(self, sample_songs):
49
50
  """Test search and play without query prompts for input"""
50
51
  with (
51
- patch("ytm_cli.main.ytmusic") as mock_ytmusic,
52
+ patch("ytm_cli.main.get_ytmusic") as mock_get_ytmusic,
52
53
  patch("ytm_cli.main.dislike_manager") as mock_dislike_manager,
53
54
  patch("ytm_cli.main.get_songs_to_display", return_value=5),
54
55
  patch("ytm_cli.main.wrapper", return_value=0),
55
56
  patch("ytm_cli.main.play_music_with_controls"),
56
57
  patch("builtins.input", return_value="user input query"),
57
58
  ):
59
+ mock_ytmusic = mock_get_ytmusic.return_value
58
60
  mock_ytmusic.search.return_value = sample_songs
59
61
  mock_dislike_manager.filter_disliked_songs.return_value = sample_songs
60
62
  mock_ytmusic.get_watch_playlist.return_value = {"tracks": []}
@@ -66,9 +68,10 @@ class TestSearchAndPlay:
66
68
  def test_search_and_play_no_results(self):
67
69
  """Test search and play when no results found"""
68
70
  with (
69
- patch("ytm_cli.main.ytmusic") as mock_ytmusic,
71
+ patch("ytm_cli.main.get_ytmusic") as mock_get_ytmusic,
70
72
  patch("ytm_cli.main.print") as mock_print,
71
73
  ):
74
+ mock_ytmusic = mock_get_ytmusic.return_value
72
75
  mock_ytmusic.search.return_value = []
73
76
 
74
77
  search_and_play("no results query")
@@ -78,10 +81,11 @@ class TestSearchAndPlay:
78
81
  def test_search_and_play_all_filtered_out(self, sample_songs):
79
82
  """Test search and play when all results are filtered out"""
80
83
  with (
81
- patch("ytm_cli.main.ytmusic") as mock_ytmusic,
84
+ patch("ytm_cli.main.get_ytmusic") as mock_get_ytmusic,
82
85
  patch("ytm_cli.main.dislike_manager") as mock_dislike_manager,
83
86
  patch("ytm_cli.main.print") as mock_print,
84
87
  ):
88
+ mock_ytmusic = mock_get_ytmusic.return_value
85
89
  mock_ytmusic.search.return_value = sample_songs
86
90
  mock_dislike_manager.filter_disliked_songs.return_value = []
87
91
 
@@ -92,12 +96,13 @@ class TestSearchAndPlay:
92
96
  def test_search_and_play_user_quits(self, sample_songs):
93
97
  """Test search and play when user quits selection"""
94
98
  with (
95
- patch("ytm_cli.main.ytmusic") as mock_ytmusic,
99
+ patch("ytm_cli.main.get_ytmusic") as mock_get_ytmusic,
96
100
  patch("ytm_cli.main.dislike_manager") as mock_dislike_manager,
97
101
  patch("ytm_cli.main.get_songs_to_display", return_value=5),
98
102
  patch("ytm_cli.main.wrapper", return_value=None),
99
103
  patch("ytm_cli.main.play_music_with_controls") as mock_play,
100
104
  ):
105
+ mock_ytmusic = mock_get_ytmusic.return_value
101
106
  mock_ytmusic.search.return_value = sample_songs
102
107
  mock_dislike_manager.filter_disliked_songs.return_value = sample_songs
103
108
 
@@ -109,13 +114,14 @@ class TestSearchAndPlay:
109
114
  def test_search_and_play_radio_fetch_error(self, sample_songs):
110
115
  """Test search and play when radio fetch fails"""
111
116
  with (
112
- patch("ytm_cli.main.ytmusic") as mock_ytmusic,
117
+ patch("ytm_cli.main.get_ytmusic") as mock_get_ytmusic,
113
118
  patch("ytm_cli.main.dislike_manager") as mock_dislike_manager,
114
119
  patch("ytm_cli.main.get_songs_to_display", return_value=5),
115
120
  patch("ytm_cli.main.wrapper", return_value=0),
116
121
  patch("ytm_cli.main.play_music_with_controls") as mock_play,
117
122
  patch("ytm_cli.main.print"),
118
123
  ):
124
+ mock_ytmusic = mock_get_ytmusic.return_value
119
125
  mock_ytmusic.search.return_value = sample_songs
120
126
  mock_dislike_manager.filter_disliked_songs.return_value = sample_songs
121
127
  mock_ytmusic.get_watch_playlist.side_effect = Exception("Radio error")