ytm-cli 0.7.0__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.0 → ytm_cli-0.7.2}/PKG-INFO +29 -40
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/README.md +26 -38
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/pyproject.toml +13 -2
- ytm_cli-0.7.2/tests/test_cache.py +111 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_config.py +6 -22
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_dislikes.py +4 -2
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_integration.py +2 -168
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_main.py +13 -7
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_playlists.py +8 -8
- ytm_cli-0.7.2/tests/test_spectrum.py +167 -0
- {ytm_cli-0.7.0 → 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.0 → ytm_cli-0.7.2}/ytm_cli/dislikes.py +11 -2
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/llm_client.py +5 -3
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/main.py +24 -9
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/player.py +27 -7
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/playlists.py +17 -8
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/ui.py +3 -3
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli.egg-info/PKG-INFO +29 -40
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli.egg-info/SOURCES.txt +3 -1
- ytm_cli-0.7.0/ytm_cli/auth.py +0 -697
- ytm_cli-0.7.0/ytm_cli/config.py +0 -29
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/LICENSE +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/setup.cfg +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_ffmpeg_player.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_hybrid_player.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_lyrics_service.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_runner.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_ui.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/tests/test_utils.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/__main__.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/cache.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/ffmpeg_player.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/hybrid_player.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/lyrics_service.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/spectrum.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/utils.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli/verbose_logger.py +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli.egg-info/dependency_links.txt +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli.egg-info/entry_points.txt +0 -0
- {ytm_cli-0.7.0 → ytm_cli-0.7.2}/ytm_cli.egg-info/requires.txt +0 -0
- {ytm_cli-0.7.0 → 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,25 +53,24 @@ 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
|
-
|
|
65
|
-
# Or from source (no clone needed)
|
|
66
|
-
uvx --from git+https://github.com/thieuluan1618/ytm.git ytm-cli "song name"
|
|
67
74
|
```
|
|
68
75
|
|
|
69
76
|
<details>
|
|
@@ -78,36 +85,16 @@ uv run ytm-cli "song name"
|
|
|
78
85
|
</details>
|
|
79
86
|
|
|
80
87
|
<details>
|
|
81
|
-
<summary>Alternative: one-shot setup
|
|
88
|
+
<summary>Alternative: one-shot setup script (Linux / macOS)</summary>
|
|
82
89
|
|
|
83
90
|
```bash
|
|
84
91
|
git clone https://github.com/thieuluan1618/ytm.git
|
|
85
92
|
cd ytm
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
These create the virtual environment, install dependencies, and configure the `ytm` command globally.
|
|
89
|
-
|
|
90
|
-
**Linux/macOS:**
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
93
|
./setup.sh
|
|
94
|
-
source ~/.zshrc
|
|
94
|
+
source ~/.zshrc # or ~/.bashrc for bash
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```powershell
|
|
100
|
-
.\setup.ps1
|
|
101
|
-
. $PROFILE
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
**Windows Command Prompt:**
|
|
105
|
-
|
|
106
|
-
```cmd
|
|
107
|
-
setup.bat
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
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.
|
|
111
98
|
|
|
112
99
|
</details>
|
|
113
100
|
|
|
@@ -117,14 +104,15 @@ After setup, use `ytm` (alias for `ytm-cli`) from anywhere.
|
|
|
117
104
|
```bash
|
|
118
105
|
git clone https://github.com/thieuluan1618/ytm.git
|
|
119
106
|
cd ytm
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
./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"
|
|
124
110
|
```
|
|
125
111
|
|
|
126
112
|
</details>
|
|
127
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
|
+
|
|
128
116
|
### Basic Usage
|
|
129
117
|
|
|
130
118
|
**Interactive search:**
|
|
@@ -143,9 +131,10 @@ ytm-cli "your favorite song"
|
|
|
143
131
|
**Non-interactive mode (automation/scripting):**
|
|
144
132
|
|
|
145
133
|
```bash
|
|
146
|
-
ytm-cli search "song name" --select 1
|
|
147
|
-
ytm-cli search "song" -s 1
|
|
148
|
-
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
|
|
149
138
|
```
|
|
150
139
|
|
|
151
140
|
> **Tip:** Set up the shell alias to use `ytm` as a shortcut for `ytm-cli`. See the setup scripts above.
|
|
@@ -252,7 +241,7 @@ Having issues? Check out the [**Troubleshooting Guide**](TROUBLESHOOTING.md) for
|
|
|
252
241
|
|
|
253
242
|
```bash
|
|
254
243
|
# Enable verbose logging to see what's happening
|
|
255
|
-
ytm-cli search "test" -s 1
|
|
244
|
+
ytm-cli search "test" -s 1 --verbose --log-file debug.log
|
|
256
245
|
|
|
257
246
|
# Check versions
|
|
258
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,25 +24,24 @@ 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
|
-
|
|
37
|
-
# Or from source (no clone needed)
|
|
38
|
-
uvx --from git+https://github.com/thieuluan1618/ytm.git ytm-cli "song name"
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
<details>
|
|
@@ -50,36 +56,16 @@ uv run ytm-cli "song name"
|
|
|
50
56
|
</details>
|
|
51
57
|
|
|
52
58
|
<details>
|
|
53
|
-
<summary>Alternative: one-shot setup
|
|
59
|
+
<summary>Alternative: one-shot setup script (Linux / macOS)</summary>
|
|
54
60
|
|
|
55
61
|
```bash
|
|
56
62
|
git clone https://github.com/thieuluan1618/ytm.git
|
|
57
63
|
cd ytm
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
These create the virtual environment, install dependencies, and configure the `ytm` command globally.
|
|
61
|
-
|
|
62
|
-
**Linux/macOS:**
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
64
|
./setup.sh
|
|
66
|
-
source ~/.zshrc
|
|
65
|
+
source ~/.zshrc # or ~/.bashrc for bash
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```powershell
|
|
72
|
-
.\setup.ps1
|
|
73
|
-
. $PROFILE
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**Windows Command Prompt:**
|
|
77
|
-
|
|
78
|
-
```cmd
|
|
79
|
-
setup.bat
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
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.
|
|
83
69
|
|
|
84
70
|
</details>
|
|
85
71
|
|
|
@@ -89,14 +75,15 @@ After setup, use `ytm` (alias for `ytm-cli`) from anywhere.
|
|
|
89
75
|
```bash
|
|
90
76
|
git clone https://github.com/thieuluan1618/ytm.git
|
|
91
77
|
cd ytm
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
./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"
|
|
96
81
|
```
|
|
97
82
|
|
|
98
83
|
</details>
|
|
99
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
|
+
|
|
100
87
|
### Basic Usage
|
|
101
88
|
|
|
102
89
|
**Interactive search:**
|
|
@@ -115,9 +102,10 @@ ytm-cli "your favorite song"
|
|
|
115
102
|
**Non-interactive mode (automation/scripting):**
|
|
116
103
|
|
|
117
104
|
```bash
|
|
118
|
-
ytm-cli search "song name" --select 1
|
|
119
|
-
ytm-cli search "song" -s 1
|
|
120
|
-
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
|
|
121
109
|
```
|
|
122
110
|
|
|
123
111
|
> **Tip:** Set up the shell alias to use `ytm` as a shortcut for `ytm-cli`. See the setup scripts above.
|
|
@@ -224,7 +212,7 @@ Having issues? Check out the [**Troubleshooting Guide**](TROUBLESHOOTING.md) for
|
|
|
224
212
|
|
|
225
213
|
```bash
|
|
226
214
|
# Enable verbose logging to see what's happening
|
|
227
|
-
ytm-cli search "test" -s 1
|
|
215
|
+
ytm-cli search "test" -s 1 --verbose --log-file debug.log
|
|
228
216
|
|
|
229
217
|
# Check versions
|
|
230
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"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Tests for ytm_cli.cache."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from ytm_cli import cache as cache_module
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def temp_cache_file(tmp_path, monkeypatch):
|
|
14
|
+
"""Redirect CACHE_FILE to an isolated temp path for each test."""
|
|
15
|
+
path = tmp_path / "ytm_cache.json"
|
|
16
|
+
monkeypatch.setattr(cache_module, "CACHE_FILE", str(path))
|
|
17
|
+
return path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestLoadSaveCache:
|
|
21
|
+
def test_load_cache_returns_empty_dict_when_missing(self, temp_cache_file):
|
|
22
|
+
assert cache_module.load_cache() == {}
|
|
23
|
+
|
|
24
|
+
def test_load_cache_returns_empty_dict_on_invalid_json(self, temp_cache_file):
|
|
25
|
+
temp_cache_file.write_text("not json {{{")
|
|
26
|
+
assert cache_module.load_cache() == {}
|
|
27
|
+
|
|
28
|
+
def test_save_cache_then_load_round_trip(self, temp_cache_file):
|
|
29
|
+
payload = {"k": {"data": "v", "timestamp": 1234.0}}
|
|
30
|
+
cache_module.save_cache(payload)
|
|
31
|
+
assert json.loads(temp_cache_file.read_text()) == payload
|
|
32
|
+
assert cache_module.load_cache() == payload
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestGetSetCached:
|
|
36
|
+
def test_get_cached_missing_key_returns_none(self, temp_cache_file):
|
|
37
|
+
assert cache_module.get_cached("nope") is None
|
|
38
|
+
|
|
39
|
+
def test_set_then_get_returns_value(self, temp_cache_file):
|
|
40
|
+
cache_module.set_cached("query1", "result1")
|
|
41
|
+
assert cache_module.get_cached("query1") == "result1"
|
|
42
|
+
|
|
43
|
+
def test_get_cached_expired_entry_removed_and_returns_none(self, temp_cache_file):
|
|
44
|
+
# Write an entry with timestamp older than the TTL.
|
|
45
|
+
old_ts = time.time() - cache_module.CACHE_TTL - 100
|
|
46
|
+
cache_module.save_cache({"old": {"data": "stale", "timestamp": old_ts}})
|
|
47
|
+
|
|
48
|
+
assert cache_module.get_cached("old") is None
|
|
49
|
+
# Expired entry should be evicted from disk on read.
|
|
50
|
+
assert "old" not in cache_module.load_cache()
|
|
51
|
+
|
|
52
|
+
def test_set_cached_overwrites_existing(self, temp_cache_file):
|
|
53
|
+
cache_module.set_cached("k", "v1")
|
|
54
|
+
cache_module.set_cached("k", "v2")
|
|
55
|
+
assert cache_module.get_cached("k") == "v2"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestClearExpired:
|
|
59
|
+
def test_clear_expired_removes_only_old_entries(self, temp_cache_file):
|
|
60
|
+
now = time.time()
|
|
61
|
+
cache_module.save_cache(
|
|
62
|
+
{
|
|
63
|
+
"fresh": {"data": "a", "timestamp": now},
|
|
64
|
+
"stale": {"data": "b", "timestamp": now - cache_module.CACHE_TTL - 10},
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
cache_module.clear_expired()
|
|
69
|
+
|
|
70
|
+
remaining = cache_module.load_cache()
|
|
71
|
+
assert "fresh" in remaining
|
|
72
|
+
assert "stale" not in remaining
|
|
73
|
+
|
|
74
|
+
def test_clear_expired_on_empty_cache_is_noop(self, temp_cache_file):
|
|
75
|
+
cache_module.clear_expired()
|
|
76
|
+
assert cache_module.load_cache() == {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestSearchWithCache:
|
|
80
|
+
def test_search_with_cache_calls_search_func_on_miss(self, temp_cache_file):
|
|
81
|
+
search_func = lambda q: [{"title": q}] # noqa: E731
|
|
82
|
+
|
|
83
|
+
result = cache_module.search_with_cache("song", search_func)
|
|
84
|
+
|
|
85
|
+
assert result == [{"title": "song"}]
|
|
86
|
+
# Underlying entry is JSON-serialized.
|
|
87
|
+
stored = cache_module.get_cached("song")
|
|
88
|
+
assert json.loads(stored) == [{"title": "song"}]
|
|
89
|
+
|
|
90
|
+
def test_search_with_cache_uses_cached_value_on_hit(self, temp_cache_file):
|
|
91
|
+
calls = {"n": 0}
|
|
92
|
+
|
|
93
|
+
def search_func(q):
|
|
94
|
+
calls["n"] += 1
|
|
95
|
+
return [{"title": q}]
|
|
96
|
+
|
|
97
|
+
cache_module.search_with_cache("song", search_func)
|
|
98
|
+
cache_module.search_with_cache("song", search_func)
|
|
99
|
+
|
|
100
|
+
assert calls["n"] == 1 # second call served from cache
|
|
101
|
+
|
|
102
|
+
def test_search_with_cache_handles_non_string_cached_value(self, temp_cache_file):
|
|
103
|
+
# Pre-populate cache with a non-string (already-deserialized) value.
|
|
104
|
+
cache_module.set_cached("song", [{"title": "song"}])
|
|
105
|
+
|
|
106
|
+
# search_func should NOT be called.
|
|
107
|
+
with patch.object(cache_module, "save_cache") as mock_save:
|
|
108
|
+
result = cache_module.search_with_cache("song", lambda q: pytest.fail("must not call"))
|
|
109
|
+
|
|
110
|
+
assert result == [{"title": "song"}]
|
|
111
|
+
mock_save.assert_not_called()
|
|
@@ -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):
|