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.
- {ytm_cli-0.7.1/ytm_cli.egg-info → ytm_cli-0.7.2}/PKG-INFO +29 -37
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/README.md +26 -35
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/pyproject.toml +13 -2
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_config.py +6 -22
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_dislikes.py +4 -2
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_integration.py +2 -168
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_main.py +13 -7
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_playlists.py +8 -8
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/__init__.py +1 -1
- ytm_cli-0.7.2/ytm_cli/config.py +49 -0
- ytm_cli-0.7.2/ytm_cli/demo.py +120 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/dislikes.py +11 -2
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/llm_client.py +5 -3
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/main.py +15 -1
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/player.py +27 -7
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/playlists.py +17 -8
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/ui.py +3 -3
- {ytm_cli-0.7.1 → ytm_cli-0.7.2/ytm_cli.egg-info}/PKG-INFO +29 -37
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/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.2}/LICENSE +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/setup.cfg +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_cache.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_ffmpeg_player.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_hybrid_player.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_lyrics_service.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_runner.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_spectrum.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_ui.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/tests/test_utils.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/__main__.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/cache.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/ffmpeg_player.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/hybrid_player.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/lyrics_service.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/spectrum.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/utils.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli/verbose_logger.py +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli.egg-info/dependency_links.txt +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli.egg-info/entry_points.txt +0 -0
- {ytm_cli-0.7.1 → ytm_cli-0.7.2}/ytm_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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 ::
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
144
|
-
ytm-cli search "song" -s 1
|
|
145
|
-
ytm-cli search "song" -s 1
|
|
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
|
|
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
|
-
|
|
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.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
|
|
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,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ytm-cli"
|
|
7
|
-
version = "0.7.
|
|
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 ::
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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")
|