ytm-cli 0.7.1__tar.gz → 0.7.3__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.
- {ytm_cli-0.7.1/ytm_cli.egg-info → ytm_cli-0.7.3}/PKG-INFO +34 -38
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/README.md +26 -35
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/pyproject.toml +19 -4
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_config.py +6 -22
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_dislikes.py +4 -2
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_integration.py +2 -168
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_main.py +13 -7
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_playlists.py +8 -8
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/__init__.py +1 -1
- ytm_cli-0.7.3/ytm_cli/config.py +49 -0
- ytm_cli-0.7.3/ytm_cli/demo.py +120 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/dislikes.py +12 -3
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/ffmpeg_player.py +1 -1
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/hybrid_player.py +1 -1
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/llm_client.py +5 -3
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/main.py +19 -5
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/player.py +31 -11
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/playlists.py +18 -9
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/spectrum.py +1 -1
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/ui.py +5 -5
- {ytm_cli-0.7.1 → ytm_cli-0.7.3/ytm_cli.egg-info}/PKG-INFO +34 -38
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli.egg-info/SOURCES.txt +1 -1
- ytm_cli-0.7.1/ytm_cli/auth.py +0 -697
- ytm_cli-0.7.1/ytm_cli/config.py +0 -29
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/LICENSE +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/setup.cfg +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_cache.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_ffmpeg_player.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_hybrid_player.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_lyrics_service.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_runner.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_spectrum.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_ui.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/tests/test_utils.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/__main__.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/cache.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/lyrics_service.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/utils.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli/verbose_logger.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli.egg-info/dependency_links.txt +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli.egg-info/entry_points.txt +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/ytm_cli.egg-info/requires.txt +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.3}/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.
|
|
3
|
+
Version: 0.7.3
|
|
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,11 +11,16 @@ 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 ::
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
15
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
22
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
|
|
18
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
19
24
|
Description-Content-Type: text/markdown
|
|
20
25
|
License-File: LICENSE
|
|
21
26
|
Requires-Dist: ytmusicapi
|
|
@@ -32,9 +37,16 @@ Dynamic: license-file
|
|
|
32
37
|
|
|
33
38
|
Stream music directly from YouTube Music in your terminal with intuitive controls, playlist management, and smart filtering.
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
<p align="center">
|
|
41
|
+
<a href="https://github.com/thieuluan1618/ytm/raw/main/screenshots/player.webm">
|
|
42
|
+
<img src="screenshots/player.gif" alt="ytm-cli player demo" width="800">
|
|
43
|
+
</a>
|
|
44
|
+
</p>
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
<p align="center">
|
|
47
|
+
<em>Player UI with the real-time FFT spectrum visualizer (synthetic demo capture).
|
|
48
|
+
Click for the higher-quality .webm.</em>
|
|
49
|
+
</p>
|
|
38
50
|
|
|
39
51
|
## ✨ Features
|
|
40
52
|
|
|
@@ -45,21 +57,23 @@ Stream music directly from YouTube Music in your terminal with intuitive control
|
|
|
45
57
|
- **👎 Smart Filtering**: Dislike songs to filter them from future results
|
|
46
58
|
- **📜 Synced Lyrics**: Live-highlighted lyrics with auto-scroll (press `l`)
|
|
47
59
|
- **🎯 Radio Mode**: Automatic playlist generation based on your selection
|
|
60
|
+
- **📊 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
61
|
- **🤖 AI-Powered**: Natural language music requests and AI-generated playlists
|
|
49
62
|
|
|
50
63
|
## 🚀 Quick Start
|
|
51
64
|
|
|
52
65
|
### Requirements
|
|
53
66
|
|
|
54
|
-
- Python 3.
|
|
67
|
+
- Python **3.10+** (set in `pyproject.toml`)
|
|
55
68
|
- [mpv media player](https://mpv.io/installation/) (must be installed system-wide)
|
|
69
|
+
- [`ffmpeg`](https://ffmpeg.org/) on `PATH` for the real-time spectrum visualizer (optional — falls back to a stereo oscilloscope if missing)
|
|
56
70
|
|
|
57
71
|
### Installation
|
|
58
72
|
|
|
59
73
|
**One command with [uv](https://docs.astral.sh/uv/) (recommended):**
|
|
60
74
|
|
|
61
75
|
```bash
|
|
62
|
-
# Install from PyPI
|
|
76
|
+
# Install from PyPI — gets latest version automatically
|
|
63
77
|
uvx ytm-cli "song name"
|
|
64
78
|
```
|
|
65
79
|
|
|
@@ -75,36 +89,16 @@ uv run ytm-cli "song name"
|
|
|
75
89
|
</details>
|
|
76
90
|
|
|
77
91
|
<details>
|
|
78
|
-
<summary>Alternative: one-shot setup
|
|
92
|
+
<summary>Alternative: one-shot setup script (Linux / macOS)</summary>
|
|
79
93
|
|
|
80
94
|
```bash
|
|
81
95
|
git clone https://github.com/thieuluan1618/ytm.git
|
|
82
96
|
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
97
|
./setup.sh
|
|
91
|
-
source ~/.zshrc
|
|
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
|
|
98
|
+
source ~/.zshrc # or ~/.bashrc for bash
|
|
105
99
|
```
|
|
106
100
|
|
|
107
|
-
|
|
101
|
+
This creates the venv, installs runtime deps from `pyproject.toml`, and adds a `ytm` shell alias.
|
|
108
102
|
|
|
109
103
|
</details>
|
|
110
104
|
|
|
@@ -114,14 +108,15 @@ After setup, use `ytm` (alias for `ytm-cli`) from anywhere.
|
|
|
114
108
|
```bash
|
|
115
109
|
git clone https://github.com/thieuluan1618/ytm.git
|
|
116
110
|
cd ytm
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
./setup_alias.sh # or setup_alias.ps1 / setup_alias.bat
|
|
111
|
+
uv sync # installs runtime deps from pyproject.toml + uv.lock
|
|
112
|
+
./setup_alias.sh # optional: registers the `ytm` alias
|
|
113
|
+
# Run with: uv run ytm-cli "song"
|
|
121
114
|
```
|
|
122
115
|
|
|
123
116
|
</details>
|
|
124
117
|
|
|
118
|
+
> **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.
|
|
119
|
+
|
|
125
120
|
### Basic Usage
|
|
126
121
|
|
|
127
122
|
**Interactive search:**
|
|
@@ -140,9 +135,10 @@ ytm-cli "your favorite song"
|
|
|
140
135
|
**Non-interactive mode (automation/scripting):**
|
|
141
136
|
|
|
142
137
|
```bash
|
|
143
|
-
ytm-cli search "song name" --select 1
|
|
144
|
-
ytm-cli search "song" -s 1
|
|
145
|
-
ytm-cli search "song" -s 1
|
|
138
|
+
ytm-cli search "song name" --select 1 # Auto-select first result
|
|
139
|
+
ytm-cli search "song" -s 1 --verbose # With verbose output
|
|
140
|
+
ytm-cli search "song" -s 1 --verbose --log-file debug.log # Save debug logs
|
|
141
|
+
ytm-cli -v # Print version
|
|
146
142
|
```
|
|
147
143
|
|
|
148
144
|
> **Tip:** Set up the shell alias to use `ytm` as a shortcut for `ytm-cli`. See the setup scripts above.
|
|
@@ -249,7 +245,7 @@ Having issues? Check out the [**Troubleshooting Guide**](TROUBLESHOOTING.md) for
|
|
|
249
245
|
|
|
250
246
|
```bash
|
|
251
247
|
# Enable verbose logging to see what's happening
|
|
252
|
-
ytm-cli search "test" -s 1
|
|
248
|
+
ytm-cli search "test" -s 1 --verbose --log-file debug.log
|
|
253
249
|
|
|
254
250
|
# Check versions
|
|
255
251
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
34
|
+
- Python **3.10+** (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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
116
|
-
ytm-cli search "song" -s 1
|
|
117
|
-
ytm-cli search "song" -s 1
|
|
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
|
|
215
|
+
ytm-cli search "test" -s 1 --verbose --log-file debug.log
|
|
225
216
|
|
|
226
217
|
# Check versions
|
|
227
218
|
mpv --version
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ytm-cli"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.3"
|
|
8
8
|
description = "YouTube Music CLI tool with interactive terminal interface"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "thieuluan", email = "thieuluan.dark@gmail.com" },
|
|
14
14
|
]
|
|
@@ -17,8 +17,13 @@ classifiers = [
|
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
18
18
|
"Environment :: Console",
|
|
19
19
|
"Intended Audience :: End Users/Desktop",
|
|
20
|
-
"Operating System ::
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Operating System :: MacOS",
|
|
21
22
|
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
22
27
|
"Programming Language :: Python :: 3.14",
|
|
23
28
|
"Topic :: Multimedia :: Sound/Audio :: Players",
|
|
24
29
|
]
|
|
@@ -31,6 +36,16 @@ dependencies = [
|
|
|
31
36
|
"numpy",
|
|
32
37
|
]
|
|
33
38
|
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"ruff>=0.14.0",
|
|
42
|
+
"pytest>=9.0.3",
|
|
43
|
+
"pytest-cov>=6.0.0",
|
|
44
|
+
"pytest-mock>=3.15.0",
|
|
45
|
+
"coverage>=7.0.0",
|
|
46
|
+
"yt-dlp>=2024.0.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
34
49
|
[project.urls]
|
|
35
50
|
Homepage = "https://github.com/thieuluan1618/ytm"
|
|
36
51
|
Repository = "https://github.com/thieuluan1618/ytm"
|
|
@@ -45,7 +60,7 @@ include = ["ytm_cli*"]
|
|
|
45
60
|
[tool.ruff]
|
|
46
61
|
# Set line length to match pylint (100 characters)
|
|
47
62
|
line-length = 100
|
|
48
|
-
target-version = "
|
|
63
|
+
target-version = "py310"
|
|
49
64
|
|
|
50
65
|
[tool.ruff.lint]
|
|
51
66
|
# Enable flake8 rules and other important checks
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|