wrkmon 1.0.0__tar.gz → 1.0.1__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.
- {wrkmon-1.0.0/wrkmon.egg-info → wrkmon-1.0.1}/PKG-INFO +40 -67
- {wrkmon-1.0.0 → wrkmon-1.0.1}/README.md +37 -64
- {wrkmon-1.0.0 → wrkmon-1.0.1}/pyproject.toml +2 -2
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/app.py +26 -2
- wrkmon-1.0.1/wrkmon/ui/views/search.py +258 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/widgets/player_bar.py +14 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1/wrkmon.egg-info}/PKG-INFO +40 -67
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon.egg-info/SOURCES.txt +1 -1
- wrkmon-1.0.0/wrkmon/ui/views/search.py +0 -150
- /wrkmon-1.0.0/LICENSE.txt → /wrkmon-1.0.1/LICENSE +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/setup.cfg +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/tests/test_core.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/tests/test_utils.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/__main__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/cli.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/core/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/core/cache.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/core/player.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/core/queue.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/core/youtube.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/data/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/data/database.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/data/migrations.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/data/models.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/components.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/messages.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/screens/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/screens/history.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/screens/player.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/screens/playlist.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/screens/search.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/theme.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/views/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/views/history.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/views/playlists.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/views/queue.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/widgets/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/widgets/header.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/ui/widgets/result_item.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/utils/__init__.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/utils/config.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/utils/mpv_installer.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon/utils/stealth.py +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon.egg-info/dependency_links.txt +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon.egg-info/entry_points.txt +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon.egg-info/requires.txt +0 -0
- {wrkmon-1.0.0 → wrkmon-1.0.1}/wrkmon.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wrkmon
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Stealth TUI YouTube audio player - stream music while looking productive
|
|
5
|
-
Author-email: Umar Khan Yousafzai <
|
|
5
|
+
Author-email: Umar Khan Yousafzai <umerfarooqkhan325@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtube
|
|
8
8
|
Project-URL: Documentation, https://github.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtube#readme
|
|
@@ -24,7 +24,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
24
24
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
|
-
License-File: LICENSE
|
|
27
|
+
License-File: LICENSE
|
|
28
28
|
Requires-Dist: textual>=0.50.0
|
|
29
29
|
Requires-Dist: typer>=0.9.0
|
|
30
30
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
@@ -35,29 +35,40 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
|
35
35
|
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
|
-
# wrkmon
|
|
38
|
+
# wrkmon
|
|
39
39
|
|
|
40
|
-
**
|
|
40
|
+
**Terminal-based YouTube Music Player** - Listen to music right from your terminal!
|
|
41
41
|
|
|
42
|
-
A
|
|
42
|
+
A beautiful TUI (Terminal User Interface) for streaming YouTube audio. No browser needed, just your terminal.
|
|
43
43
|
|
|
44
44
|

|
|
45
45
|

|
|
46
46
|

|
|
47
|
+

|
|
47
48
|
|
|
48
49
|
## Features
|
|
49
50
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
- 🖥️ **Cross-Platform** - Works on Windows, macOS, and Linux
|
|
51
|
+
- Search and stream YouTube audio
|
|
52
|
+
- Beautiful terminal interface
|
|
53
|
+
- Queue management with shuffle/repeat
|
|
54
|
+
- Play history and playlists
|
|
55
|
+
- Keyboard-driven controls
|
|
56
|
+
- Cross-platform (Windows, macOS, Linux)
|
|
57
57
|
|
|
58
58
|
## Installation
|
|
59
59
|
|
|
60
|
-
###
|
|
60
|
+
### pip (Recommended)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install wrkmon
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Note:** You also need mpv installed:
|
|
67
|
+
> - Windows: `winget install mpv`
|
|
68
|
+
> - macOS: `brew install mpv`
|
|
69
|
+
> - Linux: `sudo apt install mpv`
|
|
70
|
+
|
|
71
|
+
### Quick Install Scripts
|
|
61
72
|
|
|
62
73
|
**Windows (PowerShell):**
|
|
63
74
|
```powershell
|
|
@@ -71,49 +82,24 @@ curl -sSL https://raw.githubusercontent.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtu
|
|
|
71
82
|
|
|
72
83
|
### Package Managers
|
|
73
84
|
|
|
74
|
-
**Windows (Chocolatey):**
|
|
75
85
|
```powershell
|
|
86
|
+
# Windows (Chocolatey)
|
|
76
87
|
choco install wrkmon
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**Windows (winget):**
|
|
80
|
-
```powershell
|
|
81
|
-
winget install wrkmon
|
|
82
|
-
```
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
```bash
|
|
89
|
+
# macOS (Homebrew) - coming soon
|
|
86
90
|
brew install wrkmon
|
|
87
|
-
```
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
```bash
|
|
92
|
+
# Linux (Snap) - coming soon
|
|
91
93
|
sudo snap install wrkmon
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
**Linux (apt):**
|
|
95
|
-
```bash
|
|
96
|
-
sudo apt install wrkmon
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### pip (All Platforms)
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
pip install wrkmon
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
> **Note:** If using pip, you need to install mpv separately:
|
|
106
|
-
> - Windows: `winget install mpv` or `choco install mpv`
|
|
107
|
-
> - macOS: `brew install mpv`
|
|
108
|
-
> - Linux: `sudo apt install mpv`
|
|
109
|
-
|
|
110
96
|
## Usage
|
|
111
97
|
|
|
112
98
|
```bash
|
|
113
99
|
wrkmon # Launch the TUI
|
|
114
|
-
wrkmon search "q" # Quick search
|
|
115
|
-
wrkmon play <id> # Play a
|
|
116
|
-
wrkmon history # View
|
|
100
|
+
wrkmon search "q" # Quick search
|
|
101
|
+
wrkmon play <id> # Play a video
|
|
102
|
+
wrkmon history # View history
|
|
117
103
|
```
|
|
118
104
|
|
|
119
105
|
## Keyboard Controls
|
|
@@ -132,7 +118,7 @@ wrkmon history # View play history
|
|
|
132
118
|
| `F10` | Add to queue |
|
|
133
119
|
| `/` | Focus search |
|
|
134
120
|
| `Enter` | Play selected |
|
|
135
|
-
| `a` | Add to queue
|
|
121
|
+
| `a` | Add to queue |
|
|
136
122
|
| `Ctrl+C` | Quit |
|
|
137
123
|
|
|
138
124
|
## Screenshots
|
|
@@ -141,38 +127,29 @@ wrkmon history # View play history
|
|
|
141
127
|
┌─────────────────────────────────────────────────────────┐
|
|
142
128
|
│ wrkmon [Search] │
|
|
143
129
|
├─────────────────────────────────────────────────────────┤
|
|
144
|
-
│ Search: lofi
|
|
130
|
+
│ Search: lofi beats │
|
|
145
131
|
├─────────────────────────────────────────────────────────┤
|
|
146
|
-
│ #
|
|
147
|
-
│ 1
|
|
148
|
-
│ 2
|
|
149
|
-
│ 3
|
|
132
|
+
│ # Title Channel Duration│
|
|
133
|
+
│ 1 Lofi Hip Hop Radio ChilledCow 3:24:15│
|
|
134
|
+
│ 2 Jazz Lofi Beats Lofi Girl 2:45:00│
|
|
135
|
+
│ 3 Study Music Playlist Study 1:30:22│
|
|
150
136
|
├─────────────────────────────────────────────────────────┤
|
|
151
|
-
│ ▶ Now Playing:
|
|
137
|
+
│ ▶ Now Playing: Lofi Beats advancement █████░░░░░ 1:23:45 │
|
|
152
138
|
│ F1 Search F2 Queue F5 Play/Pause F9 Stop │
|
|
153
139
|
└─────────────────────────────────────────────────────────┘
|
|
154
140
|
```
|
|
155
141
|
|
|
156
|
-
## Why wrkmon?
|
|
157
|
-
|
|
158
|
-
Ever wanted to listen to music at work but worried about monitoring software catching you? wrkmon disguises itself as a legitimate development process while streaming your favorite tunes in the background. The TUI looks like a process monitor, and the audio plays through mpv with no visible windows.
|
|
159
|
-
|
|
160
142
|
## Requirements
|
|
161
143
|
|
|
162
144
|
- Python 3.10+
|
|
163
|
-
- mpv
|
|
145
|
+
- mpv media player
|
|
164
146
|
|
|
165
147
|
## Development
|
|
166
148
|
|
|
167
149
|
```bash
|
|
168
|
-
# Clone the repo
|
|
169
150
|
git clone https://github.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtube.git
|
|
170
151
|
cd Wrkmon-TUI-Youtube
|
|
171
|
-
|
|
172
|
-
# Install in development mode
|
|
173
152
|
pip install -e ".[dev]"
|
|
174
|
-
|
|
175
|
-
# Run tests
|
|
176
153
|
pytest
|
|
177
154
|
```
|
|
178
155
|
|
|
@@ -180,14 +157,10 @@ pytest
|
|
|
180
157
|
|
|
181
158
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
182
159
|
|
|
183
|
-
## Contributing
|
|
184
|
-
|
|
185
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
186
|
-
|
|
187
160
|
## Author
|
|
188
161
|
|
|
189
162
|
**Umar Khan Yousafzai**
|
|
190
163
|
|
|
191
164
|
---
|
|
192
165
|
|
|
193
|
-
*
|
|
166
|
+
*Enjoy your music!*
|
|
@@ -1,26 +1,37 @@
|
|
|
1
|
-
# wrkmon
|
|
1
|
+
# wrkmon
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Terminal-based YouTube Music Player** - Listen to music right from your terminal!
|
|
4
4
|
|
|
5
|
-
A
|
|
5
|
+
A beautiful TUI (Terminal User Interface) for streaming YouTube audio. No browser needed, just your terminal.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
10
|
+

|
|
10
11
|
|
|
11
12
|
## Features
|
|
12
13
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- 🖥️ **Cross-Platform** - Works on Windows, macOS, and Linux
|
|
14
|
+
- Search and stream YouTube audio
|
|
15
|
+
- Beautiful terminal interface
|
|
16
|
+
- Queue management with shuffle/repeat
|
|
17
|
+
- Play history and playlists
|
|
18
|
+
- Keyboard-driven controls
|
|
19
|
+
- Cross-platform (Windows, macOS, Linux)
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
-
###
|
|
23
|
+
### pip (Recommended)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install wrkmon
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Note:** You also need mpv installed:
|
|
30
|
+
> - Windows: `winget install mpv`
|
|
31
|
+
> - macOS: `brew install mpv`
|
|
32
|
+
> - Linux: `sudo apt install mpv`
|
|
33
|
+
|
|
34
|
+
### Quick Install Scripts
|
|
24
35
|
|
|
25
36
|
**Windows (PowerShell):**
|
|
26
37
|
```powershell
|
|
@@ -34,49 +45,24 @@ curl -sSL https://raw.githubusercontent.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtu
|
|
|
34
45
|
|
|
35
46
|
### Package Managers
|
|
36
47
|
|
|
37
|
-
**Windows (Chocolatey):**
|
|
38
48
|
```powershell
|
|
49
|
+
# Windows (Chocolatey)
|
|
39
50
|
choco install wrkmon
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Windows (winget):**
|
|
43
|
-
```powershell
|
|
44
|
-
winget install wrkmon
|
|
45
|
-
```
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
```bash
|
|
52
|
+
# macOS (Homebrew) - coming soon
|
|
49
53
|
brew install wrkmon
|
|
50
|
-
```
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
```bash
|
|
55
|
+
# Linux (Snap) - coming soon
|
|
54
56
|
sudo snap install wrkmon
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
**Linux (apt):**
|
|
58
|
-
```bash
|
|
59
|
-
sudo apt install wrkmon
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### pip (All Platforms)
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
pip install wrkmon
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
> **Note:** If using pip, you need to install mpv separately:
|
|
69
|
-
> - Windows: `winget install mpv` or `choco install mpv`
|
|
70
|
-
> - macOS: `brew install mpv`
|
|
71
|
-
> - Linux: `sudo apt install mpv`
|
|
72
|
-
|
|
73
59
|
## Usage
|
|
74
60
|
|
|
75
61
|
```bash
|
|
76
62
|
wrkmon # Launch the TUI
|
|
77
|
-
wrkmon search "q" # Quick search
|
|
78
|
-
wrkmon play <id> # Play a
|
|
79
|
-
wrkmon history # View
|
|
63
|
+
wrkmon search "q" # Quick search
|
|
64
|
+
wrkmon play <id> # Play a video
|
|
65
|
+
wrkmon history # View history
|
|
80
66
|
```
|
|
81
67
|
|
|
82
68
|
## Keyboard Controls
|
|
@@ -95,7 +81,7 @@ wrkmon history # View play history
|
|
|
95
81
|
| `F10` | Add to queue |
|
|
96
82
|
| `/` | Focus search |
|
|
97
83
|
| `Enter` | Play selected |
|
|
98
|
-
| `a` | Add to queue
|
|
84
|
+
| `a` | Add to queue |
|
|
99
85
|
| `Ctrl+C` | Quit |
|
|
100
86
|
|
|
101
87
|
## Screenshots
|
|
@@ -104,38 +90,29 @@ wrkmon history # View play history
|
|
|
104
90
|
┌─────────────────────────────────────────────────────────┐
|
|
105
91
|
│ wrkmon [Search] │
|
|
106
92
|
├─────────────────────────────────────────────────────────┤
|
|
107
|
-
│ Search: lofi
|
|
93
|
+
│ Search: lofi beats │
|
|
108
94
|
├─────────────────────────────────────────────────────────┤
|
|
109
|
-
│ #
|
|
110
|
-
│ 1
|
|
111
|
-
│ 2
|
|
112
|
-
│ 3
|
|
95
|
+
│ # Title Channel Duration│
|
|
96
|
+
│ 1 Lofi Hip Hop Radio ChilledCow 3:24:15│
|
|
97
|
+
│ 2 Jazz Lofi Beats Lofi Girl 2:45:00│
|
|
98
|
+
│ 3 Study Music Playlist Study 1:30:22│
|
|
113
99
|
├─────────────────────────────────────────────────────────┤
|
|
114
|
-
│ ▶ Now Playing:
|
|
100
|
+
│ ▶ Now Playing: Lofi Beats advancement █████░░░░░ 1:23:45 │
|
|
115
101
|
│ F1 Search F2 Queue F5 Play/Pause F9 Stop │
|
|
116
102
|
└─────────────────────────────────────────────────────────┘
|
|
117
103
|
```
|
|
118
104
|
|
|
119
|
-
## Why wrkmon?
|
|
120
|
-
|
|
121
|
-
Ever wanted to listen to music at work but worried about monitoring software catching you? wrkmon disguises itself as a legitimate development process while streaming your favorite tunes in the background. The TUI looks like a process monitor, and the audio plays through mpv with no visible windows.
|
|
122
|
-
|
|
123
105
|
## Requirements
|
|
124
106
|
|
|
125
107
|
- Python 3.10+
|
|
126
|
-
- mpv
|
|
108
|
+
- mpv media player
|
|
127
109
|
|
|
128
110
|
## Development
|
|
129
111
|
|
|
130
112
|
```bash
|
|
131
|
-
# Clone the repo
|
|
132
113
|
git clone https://github.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtube.git
|
|
133
114
|
cd Wrkmon-TUI-Youtube
|
|
134
|
-
|
|
135
|
-
# Install in development mode
|
|
136
115
|
pip install -e ".[dev]"
|
|
137
|
-
|
|
138
|
-
# Run tests
|
|
139
116
|
pytest
|
|
140
117
|
```
|
|
141
118
|
|
|
@@ -143,14 +120,10 @@ pytest
|
|
|
143
120
|
|
|
144
121
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
145
122
|
|
|
146
|
-
## Contributing
|
|
147
|
-
|
|
148
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
149
|
-
|
|
150
123
|
## Author
|
|
151
124
|
|
|
152
125
|
**Umar Khan Yousafzai**
|
|
153
126
|
|
|
154
127
|
---
|
|
155
128
|
|
|
156
|
-
*
|
|
129
|
+
*Enjoy your music!*
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wrkmon"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Stealth TUI YouTube audio player - stream music while looking productive"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
authors = [
|
|
13
|
-
{ name = "Umar Khan Yousafzai", email = "
|
|
13
|
+
{ name = "Umar Khan Yousafzai", email = "umerfarooqkhan325@gmail.com" }
|
|
14
14
|
]
|
|
15
15
|
keywords = ["youtube", "audio", "player", "tui", "music", "stealth", "productivity", "terminal"]
|
|
16
16
|
classifiers = [
|
|
@@ -329,12 +329,18 @@ class WrkmonApp(App):
|
|
|
329
329
|
# ----------------------------------------
|
|
330
330
|
async def _update_playback_display(self) -> None:
|
|
331
331
|
"""Update the player bar with current playback position."""
|
|
332
|
+
player_bar = self._get_player_bar()
|
|
333
|
+
|
|
334
|
+
# Always sync repeat mode to player bar
|
|
335
|
+
try:
|
|
336
|
+
player_bar.repeat_mode = self.queue.repeat_mode
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
|
|
332
340
|
if not self._current_track:
|
|
333
341
|
return
|
|
334
342
|
|
|
335
343
|
try:
|
|
336
|
-
player_bar = self._get_player_bar()
|
|
337
|
-
|
|
338
344
|
# Get current position and duration via IPC
|
|
339
345
|
pos = await self.player.get_position()
|
|
340
346
|
dur = await self.player.get_duration()
|
|
@@ -391,6 +397,12 @@ class WrkmonApp(App):
|
|
|
391
397
|
self.query_one("#queue", QueueView).refresh_queue()
|
|
392
398
|
except Exception:
|
|
393
399
|
pass
|
|
400
|
+
# Auto-focus list when switching to search (if has results)
|
|
401
|
+
elif view_name == "search":
|
|
402
|
+
try:
|
|
403
|
+
self.query_one("#search", SearchView).focus_list()
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
394
406
|
|
|
395
407
|
async def action_toggle_pause(self) -> None:
|
|
396
408
|
"""Smart play/pause - starts playback if nothing playing."""
|
|
@@ -407,6 +419,18 @@ class WrkmonApp(App):
|
|
|
407
419
|
await self.toggle_pause()
|
|
408
420
|
return
|
|
409
421
|
|
|
422
|
+
# Nothing playing - if in search view with selected item, play it
|
|
423
|
+
if self._current_view == "search":
|
|
424
|
+
try:
|
|
425
|
+
search_view = self.query_one("#search", SearchView)
|
|
426
|
+
result = search_view._get_selected()
|
|
427
|
+
if result:
|
|
428
|
+
logger.info(f" -> Playing selected search result: {result.title}")
|
|
429
|
+
await self.play_track(result)
|
|
430
|
+
return
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
|
|
410
434
|
# Nothing playing - try to play from queue
|
|
411
435
|
current = self.queue.current
|
|
412
436
|
if current:
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Search view container for wrkmon."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.containers import Vertical
|
|
6
|
+
from textual.widgets import Static, Input, ListView, ListItem, Label
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.events import Key
|
|
9
|
+
from textual import on
|
|
10
|
+
|
|
11
|
+
from wrkmon.core.youtube import SearchResult
|
|
12
|
+
from wrkmon.ui.messages import TrackSelected, TrackQueued, StatusMessage
|
|
13
|
+
from wrkmon.ui.widgets.result_item import ResultItem
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("wrkmon.search")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoadMoreItem(ListItem):
|
|
19
|
+
"""Special list item for loading more results."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.is_load_more = True
|
|
24
|
+
|
|
25
|
+
def compose(self):
|
|
26
|
+
yield Label(" >>> Load More Results <<<", classes="load-more")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SearchView(Vertical):
|
|
30
|
+
"""Main search view - search YouTube and display results."""
|
|
31
|
+
|
|
32
|
+
BINDINGS = [
|
|
33
|
+
Binding("a", "queue_selected", "Add to Queue", show=True),
|
|
34
|
+
Binding("escape", "clear_search", "Clear", show=True),
|
|
35
|
+
Binding("/", "focus_search", "Search", show=True),
|
|
36
|
+
Binding("space", "play_selected", "Play", show=False, priority=True),
|
|
37
|
+
Binding("r", "toggle_repeat", "Repeat", show=True),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def __init__(self, **kwargs) -> None:
|
|
41
|
+
super().__init__(**kwargs)
|
|
42
|
+
self.results: list[SearchResult] = []
|
|
43
|
+
self._is_searching = False
|
|
44
|
+
self._current_query = ""
|
|
45
|
+
self._load_more_offset = 0
|
|
46
|
+
self._batch_size = 15
|
|
47
|
+
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
yield Static("SEARCH", id="view-title")
|
|
50
|
+
|
|
51
|
+
with Vertical(id="search-container"):
|
|
52
|
+
yield Input(
|
|
53
|
+
placeholder="Search processes...",
|
|
54
|
+
id="search-input",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
yield Static(
|
|
58
|
+
" # Process PID Duration Status",
|
|
59
|
+
id="list-header",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
yield ListView(id="results-list")
|
|
63
|
+
yield Static("Type to search", id="status-bar")
|
|
64
|
+
|
|
65
|
+
def on_mount(self) -> None:
|
|
66
|
+
"""Focus the search input on mount."""
|
|
67
|
+
self.query_one("#search-input", Input).focus()
|
|
68
|
+
|
|
69
|
+
@on(Input.Submitted, "#search-input")
|
|
70
|
+
async def handle_search(self, event: Input.Submitted) -> None:
|
|
71
|
+
"""Execute search when Enter is pressed."""
|
|
72
|
+
query = event.value.strip()
|
|
73
|
+
if not query or self._is_searching:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
self._is_searching = True
|
|
77
|
+
self._update_status("Searching...")
|
|
78
|
+
self._current_query = query
|
|
79
|
+
self._load_more_offset = 0
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Access the app's YouTube client
|
|
83
|
+
youtube = self.app.youtube
|
|
84
|
+
self.results = await youtube.search(query, max_results=self._batch_size)
|
|
85
|
+
self._display_results(show_load_more=True)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
self._update_status(f"Search failed: {e}")
|
|
88
|
+
self.post_message(StatusMessage(f"Search error: {e}", "error"))
|
|
89
|
+
finally:
|
|
90
|
+
self._is_searching = False
|
|
91
|
+
|
|
92
|
+
async def _load_more_results(self) -> None:
|
|
93
|
+
"""Load more search results."""
|
|
94
|
+
if not self._current_query or self._is_searching:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
self._is_searching = True
|
|
98
|
+
self._update_status("Loading more...")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
youtube = self.app.youtube
|
|
102
|
+
self._load_more_offset += self._batch_size
|
|
103
|
+
# Search with offset by fetching more and skipping existing
|
|
104
|
+
new_results = await youtube.search(
|
|
105
|
+
self._current_query,
|
|
106
|
+
max_results=self._batch_size + self._load_more_offset
|
|
107
|
+
)
|
|
108
|
+
# Get only the new results we don't have yet
|
|
109
|
+
if len(new_results) > len(self.results):
|
|
110
|
+
self.results = new_results
|
|
111
|
+
self._display_results(show_load_more=True)
|
|
112
|
+
else:
|
|
113
|
+
self._update_status(f"No more results | Found {len(self.results)} total")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self._update_status(f"Load more failed: {e}")
|
|
116
|
+
finally:
|
|
117
|
+
self._is_searching = False
|
|
118
|
+
|
|
119
|
+
def _display_results(self, show_load_more: bool = False) -> None:
|
|
120
|
+
"""Display search results in the list."""
|
|
121
|
+
list_view = self.query_one("#results-list", ListView)
|
|
122
|
+
list_view.clear()
|
|
123
|
+
|
|
124
|
+
if not self.results:
|
|
125
|
+
self._update_status("No results found")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
for i, result in enumerate(self.results, 1):
|
|
129
|
+
list_view.append(ResultItem(result, i))
|
|
130
|
+
|
|
131
|
+
# Add "Load More" option at the end
|
|
132
|
+
if show_load_more:
|
|
133
|
+
list_view.append(LoadMoreItem())
|
|
134
|
+
|
|
135
|
+
# Build status with repeat indicator
|
|
136
|
+
repeat_status = self._get_repeat_status()
|
|
137
|
+
status = f"Found {len(self.results)} | Enter/Space=Play A=Queue R=Repeat{repeat_status}"
|
|
138
|
+
self._update_status(status)
|
|
139
|
+
list_view.focus()
|
|
140
|
+
|
|
141
|
+
def _get_repeat_status(self) -> str:
|
|
142
|
+
"""Get repeat mode status string."""
|
|
143
|
+
try:
|
|
144
|
+
mode = self.app.queue.repeat_mode
|
|
145
|
+
if mode == "one":
|
|
146
|
+
return " [REPEAT ONE]"
|
|
147
|
+
elif mode == "all":
|
|
148
|
+
return " [REPEAT ALL]"
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
return ""
|
|
152
|
+
|
|
153
|
+
def _update_status(self, message: str) -> None:
|
|
154
|
+
"""Update the status bar."""
|
|
155
|
+
try:
|
|
156
|
+
self.query_one("#status-bar", Static).update(message)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
def _get_selected(self) -> SearchResult | None:
|
|
161
|
+
"""Get the currently selected result."""
|
|
162
|
+
list_view = self.query_one("#results-list", ListView)
|
|
163
|
+
if list_view.highlighted_child is None:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
item = list_view.highlighted_child
|
|
167
|
+
if isinstance(item, ResultItem):
|
|
168
|
+
return item.result
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def action_clear_search(self) -> None:
|
|
172
|
+
"""Clear the search input."""
|
|
173
|
+
search_input = self.query_one("#search-input", Input)
|
|
174
|
+
if search_input.value:
|
|
175
|
+
search_input.value = ""
|
|
176
|
+
search_input.focus()
|
|
177
|
+
|
|
178
|
+
@on(ListView.Selected, "#results-list")
|
|
179
|
+
async def handle_result_selected(self, event: ListView.Selected) -> None:
|
|
180
|
+
"""Handle Enter key on a result - play it or load more."""
|
|
181
|
+
if isinstance(event.item, LoadMoreItem):
|
|
182
|
+
await self._load_more_results()
|
|
183
|
+
elif isinstance(event.item, ResultItem):
|
|
184
|
+
result = event.item.result
|
|
185
|
+
self.post_message(TrackSelected(result))
|
|
186
|
+
repeat_status = self._get_repeat_status()
|
|
187
|
+
self._update_status(f"Playing: {result.title[:40]}...{repeat_status}")
|
|
188
|
+
|
|
189
|
+
def action_play_selected(self) -> None:
|
|
190
|
+
"""Play the selected track (Space key)."""
|
|
191
|
+
list_view = self.query_one("#results-list", ListView)
|
|
192
|
+
if not list_view.has_focus:
|
|
193
|
+
return
|
|
194
|
+
result = self._get_selected()
|
|
195
|
+
if result:
|
|
196
|
+
self.post_message(TrackSelected(result))
|
|
197
|
+
repeat_status = self._get_repeat_status()
|
|
198
|
+
self._update_status(f"Playing: {result.title[:40]}...{repeat_status}")
|
|
199
|
+
|
|
200
|
+
def action_toggle_repeat(self) -> None:
|
|
201
|
+
"""Cycle repeat mode (R key)."""
|
|
202
|
+
try:
|
|
203
|
+
mode = self.app.queue.cycle_repeat()
|
|
204
|
+
mode_names = {"none": "OFF", "one": "ONE", "all": "ALL"}
|
|
205
|
+
repeat_status = self._get_repeat_status()
|
|
206
|
+
count = len(self.results) if self.results else 0
|
|
207
|
+
self._update_status(f"Repeat: {mode_names[mode]} | Found {count}{repeat_status}")
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
def action_queue_selected(self) -> None:
|
|
212
|
+
"""Add selected track to queue."""
|
|
213
|
+
logger.info("=== 'a' PRESSED: action_queue_selected ===")
|
|
214
|
+
result = self._get_selected()
|
|
215
|
+
logger.info(f" Selected result: {result}")
|
|
216
|
+
if result:
|
|
217
|
+
logger.info(f" Posting TrackQueued for: {result.title}")
|
|
218
|
+
self.post_message(TrackQueued(result))
|
|
219
|
+
repeat_status = self._get_repeat_status()
|
|
220
|
+
self._update_status(f"Queued: {result.title[:40]}...{repeat_status}")
|
|
221
|
+
else:
|
|
222
|
+
logger.warning(" No result selected!")
|
|
223
|
+
|
|
224
|
+
def focus_input(self) -> None:
|
|
225
|
+
"""Focus the search input (called from parent)."""
|
|
226
|
+
self.query_one("#search-input", Input).focus()
|
|
227
|
+
|
|
228
|
+
def action_focus_search(self) -> None:
|
|
229
|
+
"""Focus the search input (/ key)."""
|
|
230
|
+
self.focus_input()
|
|
231
|
+
|
|
232
|
+
def on_key(self, event: Key) -> None:
|
|
233
|
+
"""Handle key events - up at top of list goes to search, down from search goes to list."""
|
|
234
|
+
if event.key == "up":
|
|
235
|
+
list_view = self.query_one("#results-list", ListView)
|
|
236
|
+
# If list is focused and at top (index 0), go to search input
|
|
237
|
+
if list_view.has_focus and list_view.index == 0:
|
|
238
|
+
self.focus_input()
|
|
239
|
+
event.prevent_default()
|
|
240
|
+
event.stop()
|
|
241
|
+
elif event.key == "down":
|
|
242
|
+
search_input = self.query_one("#search-input", Input)
|
|
243
|
+
# If search input is focused, go to list
|
|
244
|
+
if search_input.has_focus:
|
|
245
|
+
list_view = self.query_one("#results-list", ListView)
|
|
246
|
+
if len(list_view.children) > 0:
|
|
247
|
+
list_view.focus()
|
|
248
|
+
list_view.index = 0
|
|
249
|
+
event.prevent_default()
|
|
250
|
+
event.stop()
|
|
251
|
+
|
|
252
|
+
def focus_list(self) -> None:
|
|
253
|
+
"""Focus the results list (called from parent)."""
|
|
254
|
+
list_view = self.query_one("#results-list", ListView)
|
|
255
|
+
if len(list_view.children) > 0:
|
|
256
|
+
list_view.focus()
|
|
257
|
+
else:
|
|
258
|
+
self.query_one("#search-input", Input).focus()
|
|
@@ -18,6 +18,7 @@ class PlayerBar(Static):
|
|
|
18
18
|
duration = reactive(0.0)
|
|
19
19
|
volume = reactive(80)
|
|
20
20
|
status_text = reactive("") # For showing errors/buffering
|
|
21
|
+
repeat_mode = reactive("none") # none, one, all
|
|
21
22
|
|
|
22
23
|
def __init__(self, **kwargs) -> None:
|
|
23
24
|
super().__init__(**kwargs)
|
|
@@ -30,6 +31,7 @@ class PlayerBar(Static):
|
|
|
30
31
|
yield Static("NOW", id="now-label", classes="label")
|
|
31
32
|
yield Static(self._get_status_icon(), id="play-status")
|
|
32
33
|
yield Static(self.title, id="track-title")
|
|
34
|
+
yield Static("", id="repeat-indicator")
|
|
33
35
|
|
|
34
36
|
# Progress row
|
|
35
37
|
with Horizontal(id="progress-row"):
|
|
@@ -93,6 +95,18 @@ class PlayerBar(Static):
|
|
|
93
95
|
except Exception:
|
|
94
96
|
pass
|
|
95
97
|
|
|
98
|
+
def watch_repeat_mode(self, new_mode: str) -> None:
|
|
99
|
+
"""Update repeat indicator."""
|
|
100
|
+
try:
|
|
101
|
+
indicator = ""
|
|
102
|
+
if new_mode == "one":
|
|
103
|
+
indicator = "[R1]"
|
|
104
|
+
elif new_mode == "all":
|
|
105
|
+
indicator = "[RA]"
|
|
106
|
+
self.query_one("#repeat-indicator", Static).update(indicator)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
96
110
|
def update_playback(
|
|
97
111
|
self,
|
|
98
112
|
title: str | None = None,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wrkmon
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Stealth TUI YouTube audio player - stream music while looking productive
|
|
5
|
-
Author-email: Umar Khan Yousafzai <
|
|
5
|
+
Author-email: Umar Khan Yousafzai <umerfarooqkhan325@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtube
|
|
8
8
|
Project-URL: Documentation, https://github.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtube#readme
|
|
@@ -24,7 +24,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
24
24
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
|
-
License-File: LICENSE
|
|
27
|
+
License-File: LICENSE
|
|
28
28
|
Requires-Dist: textual>=0.50.0
|
|
29
29
|
Requires-Dist: typer>=0.9.0
|
|
30
30
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
@@ -35,29 +35,40 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
|
35
35
|
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
|
-
# wrkmon
|
|
38
|
+
# wrkmon
|
|
39
39
|
|
|
40
|
-
**
|
|
40
|
+
**Terminal-based YouTube Music Player** - Listen to music right from your terminal!
|
|
41
41
|
|
|
42
|
-
A
|
|
42
|
+
A beautiful TUI (Terminal User Interface) for streaming YouTube audio. No browser needed, just your terminal.
|
|
43
43
|
|
|
44
44
|

|
|
45
45
|

|
|
46
46
|

|
|
47
|
+

|
|
47
48
|
|
|
48
49
|
## Features
|
|
49
50
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
- 🖥️ **Cross-Platform** - Works on Windows, macOS, and Linux
|
|
51
|
+
- Search and stream YouTube audio
|
|
52
|
+
- Beautiful terminal interface
|
|
53
|
+
- Queue management with shuffle/repeat
|
|
54
|
+
- Play history and playlists
|
|
55
|
+
- Keyboard-driven controls
|
|
56
|
+
- Cross-platform (Windows, macOS, Linux)
|
|
57
57
|
|
|
58
58
|
## Installation
|
|
59
59
|
|
|
60
|
-
###
|
|
60
|
+
### pip (Recommended)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install wrkmon
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Note:** You also need mpv installed:
|
|
67
|
+
> - Windows: `winget install mpv`
|
|
68
|
+
> - macOS: `brew install mpv`
|
|
69
|
+
> - Linux: `sudo apt install mpv`
|
|
70
|
+
|
|
71
|
+
### Quick Install Scripts
|
|
61
72
|
|
|
62
73
|
**Windows (PowerShell):**
|
|
63
74
|
```powershell
|
|
@@ -71,49 +82,24 @@ curl -sSL https://raw.githubusercontent.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtu
|
|
|
71
82
|
|
|
72
83
|
### Package Managers
|
|
73
84
|
|
|
74
|
-
**Windows (Chocolatey):**
|
|
75
85
|
```powershell
|
|
86
|
+
# Windows (Chocolatey)
|
|
76
87
|
choco install wrkmon
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**Windows (winget):**
|
|
80
|
-
```powershell
|
|
81
|
-
winget install wrkmon
|
|
82
|
-
```
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
```bash
|
|
89
|
+
# macOS (Homebrew) - coming soon
|
|
86
90
|
brew install wrkmon
|
|
87
|
-
```
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
```bash
|
|
92
|
+
# Linux (Snap) - coming soon
|
|
91
93
|
sudo snap install wrkmon
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
**Linux (apt):**
|
|
95
|
-
```bash
|
|
96
|
-
sudo apt install wrkmon
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### pip (All Platforms)
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
pip install wrkmon
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
> **Note:** If using pip, you need to install mpv separately:
|
|
106
|
-
> - Windows: `winget install mpv` or `choco install mpv`
|
|
107
|
-
> - macOS: `brew install mpv`
|
|
108
|
-
> - Linux: `sudo apt install mpv`
|
|
109
|
-
|
|
110
96
|
## Usage
|
|
111
97
|
|
|
112
98
|
```bash
|
|
113
99
|
wrkmon # Launch the TUI
|
|
114
|
-
wrkmon search "q" # Quick search
|
|
115
|
-
wrkmon play <id> # Play a
|
|
116
|
-
wrkmon history # View
|
|
100
|
+
wrkmon search "q" # Quick search
|
|
101
|
+
wrkmon play <id> # Play a video
|
|
102
|
+
wrkmon history # View history
|
|
117
103
|
```
|
|
118
104
|
|
|
119
105
|
## Keyboard Controls
|
|
@@ -132,7 +118,7 @@ wrkmon history # View play history
|
|
|
132
118
|
| `F10` | Add to queue |
|
|
133
119
|
| `/` | Focus search |
|
|
134
120
|
| `Enter` | Play selected |
|
|
135
|
-
| `a` | Add to queue
|
|
121
|
+
| `a` | Add to queue |
|
|
136
122
|
| `Ctrl+C` | Quit |
|
|
137
123
|
|
|
138
124
|
## Screenshots
|
|
@@ -141,38 +127,29 @@ wrkmon history # View play history
|
|
|
141
127
|
┌─────────────────────────────────────────────────────────┐
|
|
142
128
|
│ wrkmon [Search] │
|
|
143
129
|
├─────────────────────────────────────────────────────────┤
|
|
144
|
-
│ Search: lofi
|
|
130
|
+
│ Search: lofi beats │
|
|
145
131
|
├─────────────────────────────────────────────────────────┤
|
|
146
|
-
│ #
|
|
147
|
-
│ 1
|
|
148
|
-
│ 2
|
|
149
|
-
│ 3
|
|
132
|
+
│ # Title Channel Duration│
|
|
133
|
+
│ 1 Lofi Hip Hop Radio ChilledCow 3:24:15│
|
|
134
|
+
│ 2 Jazz Lofi Beats Lofi Girl 2:45:00│
|
|
135
|
+
│ 3 Study Music Playlist Study 1:30:22│
|
|
150
136
|
├─────────────────────────────────────────────────────────┤
|
|
151
|
-
│ ▶ Now Playing:
|
|
137
|
+
│ ▶ Now Playing: Lofi Beats advancement █████░░░░░ 1:23:45 │
|
|
152
138
|
│ F1 Search F2 Queue F5 Play/Pause F9 Stop │
|
|
153
139
|
└─────────────────────────────────────────────────────────┘
|
|
154
140
|
```
|
|
155
141
|
|
|
156
|
-
## Why wrkmon?
|
|
157
|
-
|
|
158
|
-
Ever wanted to listen to music at work but worried about monitoring software catching you? wrkmon disguises itself as a legitimate development process while streaming your favorite tunes in the background. The TUI looks like a process monitor, and the audio plays through mpv with no visible windows.
|
|
159
|
-
|
|
160
142
|
## Requirements
|
|
161
143
|
|
|
162
144
|
- Python 3.10+
|
|
163
|
-
- mpv
|
|
145
|
+
- mpv media player
|
|
164
146
|
|
|
165
147
|
## Development
|
|
166
148
|
|
|
167
149
|
```bash
|
|
168
|
-
# Clone the repo
|
|
169
150
|
git clone https://github.com/Umar-Khan-Yousafzai/Wrkmon-TUI-Youtube.git
|
|
170
151
|
cd Wrkmon-TUI-Youtube
|
|
171
|
-
|
|
172
|
-
# Install in development mode
|
|
173
152
|
pip install -e ".[dev]"
|
|
174
|
-
|
|
175
|
-
# Run tests
|
|
176
153
|
pytest
|
|
177
154
|
```
|
|
178
155
|
|
|
@@ -180,14 +157,10 @@ pytest
|
|
|
180
157
|
|
|
181
158
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
182
159
|
|
|
183
|
-
## Contributing
|
|
184
|
-
|
|
185
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
186
|
-
|
|
187
160
|
## Author
|
|
188
161
|
|
|
189
162
|
**Umar Khan Yousafzai**
|
|
190
163
|
|
|
191
164
|
---
|
|
192
165
|
|
|
193
|
-
*
|
|
166
|
+
*Enjoy your music!*
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
"""Search view container for wrkmon."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from textual.app import ComposeResult
|
|
5
|
-
from textual.containers import Vertical
|
|
6
|
-
from textual.widgets import Static, Input, ListView
|
|
7
|
-
from textual.binding import Binding
|
|
8
|
-
from textual.events import Key
|
|
9
|
-
from textual import on
|
|
10
|
-
|
|
11
|
-
from wrkmon.core.youtube import SearchResult
|
|
12
|
-
from wrkmon.ui.messages import TrackSelected, TrackQueued, StatusMessage
|
|
13
|
-
from wrkmon.ui.widgets.result_item import ResultItem
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger("wrkmon.search")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class SearchView(Vertical):
|
|
19
|
-
"""Main search view - search YouTube and display results."""
|
|
20
|
-
|
|
21
|
-
BINDINGS = [
|
|
22
|
-
Binding("a", "queue_selected", "Add to Queue", show=True),
|
|
23
|
-
Binding("escape", "clear_search", "Clear", show=True),
|
|
24
|
-
Binding("/", "focus_search", "Search", show=True),
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
def __init__(self, **kwargs) -> None:
|
|
28
|
-
super().__init__(**kwargs)
|
|
29
|
-
self.results: list[SearchResult] = []
|
|
30
|
-
self._is_searching = False
|
|
31
|
-
|
|
32
|
-
def compose(self) -> ComposeResult:
|
|
33
|
-
yield Static("SEARCH", id="view-title")
|
|
34
|
-
|
|
35
|
-
with Vertical(id="search-container"):
|
|
36
|
-
yield Input(
|
|
37
|
-
placeholder="Search processes...",
|
|
38
|
-
id="search-input",
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
yield Static(
|
|
42
|
-
" # Process PID Duration Status",
|
|
43
|
-
id="list-header",
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
yield ListView(id="results-list")
|
|
47
|
-
yield Static("Type to search", id="status-bar")
|
|
48
|
-
|
|
49
|
-
def on_mount(self) -> None:
|
|
50
|
-
"""Focus the search input on mount."""
|
|
51
|
-
self.query_one("#search-input", Input).focus()
|
|
52
|
-
|
|
53
|
-
@on(Input.Submitted, "#search-input")
|
|
54
|
-
async def handle_search(self, event: Input.Submitted) -> None:
|
|
55
|
-
"""Execute search when Enter is pressed."""
|
|
56
|
-
query = event.value.strip()
|
|
57
|
-
if not query or self._is_searching:
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
self._is_searching = True
|
|
61
|
-
self._update_status("Searching...")
|
|
62
|
-
|
|
63
|
-
try:
|
|
64
|
-
# Access the app's YouTube client
|
|
65
|
-
youtube = self.app.youtube
|
|
66
|
-
self.results = await youtube.search(query, max_results=15)
|
|
67
|
-
self._display_results()
|
|
68
|
-
except Exception as e:
|
|
69
|
-
self._update_status(f"Search failed: {e}")
|
|
70
|
-
self.post_message(StatusMessage(f"Search error: {e}", "error"))
|
|
71
|
-
finally:
|
|
72
|
-
self._is_searching = False
|
|
73
|
-
|
|
74
|
-
def _display_results(self) -> None:
|
|
75
|
-
"""Display search results in the list."""
|
|
76
|
-
list_view = self.query_one("#results-list", ListView)
|
|
77
|
-
list_view.clear()
|
|
78
|
-
|
|
79
|
-
if not self.results:
|
|
80
|
-
self._update_status("No results found")
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
for i, result in enumerate(self.results, 1):
|
|
84
|
-
list_view.append(ResultItem(result, i))
|
|
85
|
-
|
|
86
|
-
self._update_status(f"Found {len(self.results)} | Enter=Play A=Queue /=Search ↑↓=Nav")
|
|
87
|
-
list_view.focus()
|
|
88
|
-
|
|
89
|
-
def _update_status(self, message: str) -> None:
|
|
90
|
-
"""Update the status bar."""
|
|
91
|
-
try:
|
|
92
|
-
self.query_one("#status-bar", Static).update(message)
|
|
93
|
-
except Exception:
|
|
94
|
-
pass
|
|
95
|
-
|
|
96
|
-
def _get_selected(self) -> SearchResult | None:
|
|
97
|
-
"""Get the currently selected result."""
|
|
98
|
-
list_view = self.query_one("#results-list", ListView)
|
|
99
|
-
if list_view.highlighted_child is None:
|
|
100
|
-
return None
|
|
101
|
-
|
|
102
|
-
item = list_view.highlighted_child
|
|
103
|
-
if isinstance(item, ResultItem):
|
|
104
|
-
return item.result
|
|
105
|
-
return None
|
|
106
|
-
|
|
107
|
-
def action_clear_search(self) -> None:
|
|
108
|
-
"""Clear the search input."""
|
|
109
|
-
search_input = self.query_one("#search-input", Input)
|
|
110
|
-
if search_input.value:
|
|
111
|
-
search_input.value = ""
|
|
112
|
-
search_input.focus()
|
|
113
|
-
|
|
114
|
-
@on(ListView.Selected, "#results-list")
|
|
115
|
-
def handle_result_selected(self, event: ListView.Selected) -> None:
|
|
116
|
-
"""Handle Enter key on a result - play it."""
|
|
117
|
-
if isinstance(event.item, ResultItem):
|
|
118
|
-
result = event.item.result
|
|
119
|
-
self.post_message(TrackSelected(result))
|
|
120
|
-
self._update_status(f"Playing: {result.title[:40]}...")
|
|
121
|
-
|
|
122
|
-
def action_queue_selected(self) -> None:
|
|
123
|
-
"""Add selected track to queue."""
|
|
124
|
-
logger.info("=== 'a' PRESSED: action_queue_selected ===")
|
|
125
|
-
result = self._get_selected()
|
|
126
|
-
logger.info(f" Selected result: {result}")
|
|
127
|
-
if result:
|
|
128
|
-
logger.info(f" Posting TrackQueued for: {result.title}")
|
|
129
|
-
self.post_message(TrackQueued(result))
|
|
130
|
-
self._update_status(f"Queued: {result.title[:40]}...")
|
|
131
|
-
else:
|
|
132
|
-
logger.warning(" No result selected!")
|
|
133
|
-
|
|
134
|
-
def focus_input(self) -> None:
|
|
135
|
-
"""Focus the search input (called from parent)."""
|
|
136
|
-
self.query_one("#search-input", Input).focus()
|
|
137
|
-
|
|
138
|
-
def action_focus_search(self) -> None:
|
|
139
|
-
"""Focus the search input (/ key)."""
|
|
140
|
-
self.focus_input()
|
|
141
|
-
|
|
142
|
-
def on_key(self, event: Key) -> None:
|
|
143
|
-
"""Handle key events - up at top of list goes to search."""
|
|
144
|
-
if event.key == "up":
|
|
145
|
-
list_view = self.query_one("#results-list", ListView)
|
|
146
|
-
# If list is focused and at top (index 0), go to search input
|
|
147
|
-
if list_view.has_focus and list_view.index == 0:
|
|
148
|
-
self.focus_input()
|
|
149
|
-
event.prevent_default()
|
|
150
|
-
event.stop()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|