yank-tui 0.1.0__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.
- yank_tui-0.1.0/PKG-INFO +145 -0
- yank_tui-0.1.0/README.md +117 -0
- yank_tui-0.1.0/pyproject.toml +50 -0
- yank_tui-0.1.0/setup.cfg +4 -0
- yank_tui-0.1.0/src/yank/__init__.py +3 -0
- yank_tui-0.1.0/src/yank/__main__.py +15 -0
- yank_tui-0.1.0/src/yank/app.py +301 -0
- yank_tui-0.1.0/src/yank/engine/__init__.py +26 -0
- yank_tui-0.1.0/src/yank/engine/downloader.py +550 -0
- yank_tui-0.1.0/src/yank/engine/formats.py +427 -0
- yank_tui-0.1.0/src/yank/engine/queue.py +365 -0
- yank_tui-0.1.0/src/yank/engine/updater.py +272 -0
- yank_tui-0.1.0/src/yank/models/__init__.py +12 -0
- yank_tui-0.1.0/src/yank/models/config.py +117 -0
- yank_tui-0.1.0/src/yank/models/history.py +135 -0
- yank_tui-0.1.0/src/yank/screens/__init__.py +19 -0
- yank_tui-0.1.0/src/yank/screens/download.py +231 -0
- yank_tui-0.1.0/src/yank/screens/format_select.py +205 -0
- yank_tui-0.1.0/src/yank/screens/history.py +228 -0
- yank_tui-0.1.0/src/yank/screens/options.py +410 -0
- yank_tui-0.1.0/src/yank/screens/queue.py +236 -0
- yank_tui-0.1.0/src/yank/screens/settings.py +330 -0
- yank_tui-0.1.0/src/yank/screens/url_input.py +295 -0
- yank_tui-0.1.0/src/yank/widgets/__init__.py +7 -0
- yank_tui-0.1.0/src/yank/widgets/format_table.py +141 -0
- yank_tui-0.1.0/src/yank/widgets/progress.py +173 -0
- yank_tui-0.1.0/src/yank/widgets/status_bar.py +132 -0
- yank_tui-0.1.0/src/yank_tui.egg-info/PKG-INFO +145 -0
- yank_tui-0.1.0/src/yank_tui.egg-info/SOURCES.txt +32 -0
- yank_tui-0.1.0/src/yank_tui.egg-info/dependency_links.txt +1 -0
- yank_tui-0.1.0/src/yank_tui.egg-info/entry_points.txt +2 -0
- yank_tui-0.1.0/src/yank_tui.egg-info/requires.txt +6 -0
- yank_tui-0.1.0/src/yank_tui.egg-info/top_level.txt +1 -0
- yank_tui-0.1.0/tests/test_formats.py +297 -0
yank_tui-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yank-tui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal UI wrapper for yt-dlp — paste URL, pick format, download
|
|
5
|
+
Author: yank contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wanazhar/yank
|
|
8
|
+
Project-URL: Source, https://github.com/wanazhar/yank
|
|
9
|
+
Project-URL: BugTracker, https://github.com/wanazhar/yank/issues
|
|
10
|
+
Keywords: yt-dlp,tui,download,youtube,textual
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: textual>=0.80.0
|
|
24
|
+
Requires-Dist: yt-dlp>=2024.1.0
|
|
25
|
+
Requires-Dist: pyfiglet>=0.9
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# yank ⬇️
|
|
30
|
+
|
|
31
|
+
**A beautiful TUI wrapper for yt-dlp.** Paste a URL, pick a format, download. No flags to remember.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
__
|
|
35
|
+
__ ______ _____ / /__
|
|
36
|
+
/ / / / __ `/ __ \/ //_/
|
|
37
|
+
/ /_/ / /_/ / / / / ,<
|
|
38
|
+
\__, /\__,_/_/ /_/_/|_|
|
|
39
|
+
/____/
|
|
40
|
+
|
|
41
|
+
yt-dlp TUI wrapper
|
|
42
|
+
|
|
43
|
+
Paste a video URL · or multiple URLs (one per line) for batch
|
|
44
|
+
|
|
45
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ https://… │
|
|
47
|
+
└────────────────────────────────────────────────────────────────┘
|
|
48
|
+
|
|
49
|
+
┌──────────────┐ ┌────────────────┐ ┌──────────────┐
|
|
50
|
+
│ Continue │ │ Batch mode │ │ Clear │
|
|
51
|
+
└──────────────┘ └────────────────┘ └──────────────┘
|
|
52
|
+
|
|
53
|
+
Queue [F2] History [F3] Settings [F4]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **URL input** — paste one or multiple URLs, auto-detects platform (YouTube, TikTok, Twitter/X, Instagram, Vimeo, etc.)
|
|
59
|
+
- **Format browser** — interactive table of available formats with resolution, codec, filesize
|
|
60
|
+
- **Download manager** — real-time progress bars with speed, ETA, per-download controls
|
|
61
|
+
- **Queue system** — batch multiple downloads, reorder, pause, cancel
|
|
62
|
+
- **History** — every download logged with title, URL, format, and status
|
|
63
|
+
- **Settings** — default download directory, format preferences, concurrent downloads, audio extraction
|
|
64
|
+
- **yt-dlp management** — auto-installs and updates yt-dlp, no manual setup
|
|
65
|
+
- **Beautiful dark theme** — purple/cyan accent palette with tasteful terminal styling
|
|
66
|
+
- **Keyboard + mouse** — navigate with arrow keys or click
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
### Quick install (recommended)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pipx install yank
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### From source
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/wanazhar/yank.git
|
|
80
|
+
cd yank
|
|
81
|
+
uv venv
|
|
82
|
+
uv sync
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
yank
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or from source:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
cd yank && .venv/bin/python -m yank
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Workflow
|
|
98
|
+
|
|
99
|
+
1. **Paste a URL** — the app opens to a clean input screen. Paste a video URL (YouTube, TikTok, etc.)
|
|
100
|
+
2. **Choose format** — browse available formats in a sortable table. Best quality is pre-selected
|
|
101
|
+
3. **Configure options** — set output directory, filename template, subtitles, thumbnail
|
|
102
|
+
4. **Download** — watch real-time progress with speed and ETA
|
|
103
|
+
5. **Done** — file saved, logged to history
|
|
104
|
+
|
|
105
|
+
### Keyboard shortcuts
|
|
106
|
+
|
|
107
|
+
| Key | Action |
|
|
108
|
+
|-----|--------|
|
|
109
|
+
| `Esc` | Back / Quit |
|
|
110
|
+
| `F2` | Queue view |
|
|
111
|
+
| `F3` | History |
|
|
112
|
+
| `F4` | Settings |
|
|
113
|
+
| `Ctrl+Q` | Quit |
|
|
114
|
+
| `Ctrl+P` | Palette |
|
|
115
|
+
| `↑↓` | Navigate |
|
|
116
|
+
| `Space` | Select |
|
|
117
|
+
| `Enter` | Confirm |
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
Config stored at `~/.config/yank/config.toml`:
|
|
122
|
+
|
|
123
|
+
```toml
|
|
124
|
+
download_dir = "/home/user/Downloads"
|
|
125
|
+
format_preference = "best"
|
|
126
|
+
max_concurrent = 2
|
|
127
|
+
filename_template = "%(title)s.%(ext)s"
|
|
128
|
+
subtitle_lang = "en"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## How it works
|
|
132
|
+
|
|
133
|
+
yank wraps **yt-dlp** as a Python library (not subprocess). All format extraction, downloading, and metadata handling goes through `yt_dlp.YoutubeDL` with progress hooks. The TUI is built with **Textual** — a modern Python terminal UI framework.
|
|
134
|
+
|
|
135
|
+
The app is fully local. No cloud, no accounts, no telemetry. Your downloads and history never leave your machine.
|
|
136
|
+
|
|
137
|
+
## Requirements
|
|
138
|
+
|
|
139
|
+
- Python 3.11+
|
|
140
|
+
- A terminal that supports 24-bit color (most modern terminals do)
|
|
141
|
+
- `ffmpeg` (optional — needed for some format merges and audio extraction)
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
yank_tui-0.1.0/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# yank ⬇️
|
|
2
|
+
|
|
3
|
+
**A beautiful TUI wrapper for yt-dlp.** Paste a URL, pick a format, download. No flags to remember.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
__
|
|
7
|
+
__ ______ _____ / /__
|
|
8
|
+
/ / / / __ `/ __ \/ //_/
|
|
9
|
+
/ /_/ / /_/ / / / / ,<
|
|
10
|
+
\__, /\__,_/_/ /_/_/|_|
|
|
11
|
+
/____/
|
|
12
|
+
|
|
13
|
+
yt-dlp TUI wrapper
|
|
14
|
+
|
|
15
|
+
Paste a video URL · or multiple URLs (one per line) for batch
|
|
16
|
+
|
|
17
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ https://… │
|
|
19
|
+
└────────────────────────────────────────────────────────────────┘
|
|
20
|
+
|
|
21
|
+
┌──────────────┐ ┌────────────────┐ ┌──────────────┐
|
|
22
|
+
│ Continue │ │ Batch mode │ │ Clear │
|
|
23
|
+
└──────────────┘ └────────────────┘ └──────────────┘
|
|
24
|
+
|
|
25
|
+
Queue [F2] History [F3] Settings [F4]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **URL input** — paste one or multiple URLs, auto-detects platform (YouTube, TikTok, Twitter/X, Instagram, Vimeo, etc.)
|
|
31
|
+
- **Format browser** — interactive table of available formats with resolution, codec, filesize
|
|
32
|
+
- **Download manager** — real-time progress bars with speed, ETA, per-download controls
|
|
33
|
+
- **Queue system** — batch multiple downloads, reorder, pause, cancel
|
|
34
|
+
- **History** — every download logged with title, URL, format, and status
|
|
35
|
+
- **Settings** — default download directory, format preferences, concurrent downloads, audio extraction
|
|
36
|
+
- **yt-dlp management** — auto-installs and updates yt-dlp, no manual setup
|
|
37
|
+
- **Beautiful dark theme** — purple/cyan accent palette with tasteful terminal styling
|
|
38
|
+
- **Keyboard + mouse** — navigate with arrow keys or click
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### Quick install (recommended)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pipx install yank
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### From source
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/wanazhar/yank.git
|
|
52
|
+
cd yank
|
|
53
|
+
uv venv
|
|
54
|
+
uv sync
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
yank
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or from source:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd yank && .venv/bin/python -m yank
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Workflow
|
|
70
|
+
|
|
71
|
+
1. **Paste a URL** — the app opens to a clean input screen. Paste a video URL (YouTube, TikTok, etc.)
|
|
72
|
+
2. **Choose format** — browse available formats in a sortable table. Best quality is pre-selected
|
|
73
|
+
3. **Configure options** — set output directory, filename template, subtitles, thumbnail
|
|
74
|
+
4. **Download** — watch real-time progress with speed and ETA
|
|
75
|
+
5. **Done** — file saved, logged to history
|
|
76
|
+
|
|
77
|
+
### Keyboard shortcuts
|
|
78
|
+
|
|
79
|
+
| Key | Action |
|
|
80
|
+
|-----|--------|
|
|
81
|
+
| `Esc` | Back / Quit |
|
|
82
|
+
| `F2` | Queue view |
|
|
83
|
+
| `F3` | History |
|
|
84
|
+
| `F4` | Settings |
|
|
85
|
+
| `Ctrl+Q` | Quit |
|
|
86
|
+
| `Ctrl+P` | Palette |
|
|
87
|
+
| `↑↓` | Navigate |
|
|
88
|
+
| `Space` | Select |
|
|
89
|
+
| `Enter` | Confirm |
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
Config stored at `~/.config/yank/config.toml`:
|
|
94
|
+
|
|
95
|
+
```toml
|
|
96
|
+
download_dir = "/home/user/Downloads"
|
|
97
|
+
format_preference = "best"
|
|
98
|
+
max_concurrent = 2
|
|
99
|
+
filename_template = "%(title)s.%(ext)s"
|
|
100
|
+
subtitle_lang = "en"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
yank wraps **yt-dlp** as a Python library (not subprocess). All format extraction, downloading, and metadata handling goes through `yt_dlp.YoutubeDL` with progress hooks. The TUI is built with **Textual** — a modern Python terminal UI framework.
|
|
106
|
+
|
|
107
|
+
The app is fully local. No cloud, no accounts, no telemetry. Your downloads and history never leave your machine.
|
|
108
|
+
|
|
109
|
+
## Requirements
|
|
110
|
+
|
|
111
|
+
- Python 3.11+
|
|
112
|
+
- A terminal that supports 24-bit color (most modern terminals do)
|
|
113
|
+
- `ffmpeg` (optional — needed for some format merges and audio extraction)
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yank-tui"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A terminal UI wrapper for yt-dlp — paste URL, pick format, download"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "yank contributors" }]
|
|
13
|
+
keywords = ["yt-dlp", "tui", "download", "youtube", "textual"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Multimedia :: Video",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"textual>=0.80.0",
|
|
28
|
+
"yt-dlp>=2024.1.0",
|
|
29
|
+
"pyfiglet>=0.9",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
yank = "yank.__main__:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/wanazhar/yank"
|
|
42
|
+
Source = "https://github.com/wanazhar/yank"
|
|
43
|
+
BugTracker = "https://github.com/wanazhar/yank/issues"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
pythonpath = ["src"]
|
yank_tui-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Entry point: ``python -m yank`` or ``yank`` CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
"""Launch the yank Textual application."""
|
|
8
|
+
from yank.app import YankApp
|
|
9
|
+
|
|
10
|
+
app = YankApp()
|
|
11
|
+
app.run()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main()
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Textual application class for yank."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual import work
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.widgets import Footer, Header
|
|
9
|
+
|
|
10
|
+
from yank.engine.downloader import DownloadItem, DownloadManager, DownloadStatus
|
|
11
|
+
from yank.engine.queue import QueueEngine
|
|
12
|
+
from yank.engine.updater import YtDlpManager
|
|
13
|
+
from yank.models.config import AppConfig, load_config, save_config
|
|
14
|
+
from yank.models.history import HistoryEntry, HistoryStore
|
|
15
|
+
from yank.screens.url_input import URLInputScreen
|
|
16
|
+
from yank.widgets.status_bar import StatusBar
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class YankApp(App):
|
|
20
|
+
"""
|
|
21
|
+
yank — yt-dlp TUI wrapper.
|
|
22
|
+
|
|
23
|
+
Startup shows URL input immediately. Shared services (config, history,
|
|
24
|
+
download manager, queue, yt-dlp manager) live on the app instance.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
TITLE = "yank"
|
|
28
|
+
SUB_TITLE = "yt-dlp TUI"
|
|
29
|
+
CSS = """
|
|
30
|
+
Screen {
|
|
31
|
+
background: #0a0a0f;
|
|
32
|
+
color: #e2e8f0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Header styling */
|
|
36
|
+
Header {
|
|
37
|
+
dock: top;
|
|
38
|
+
background: #0a0a0f;
|
|
39
|
+
color: #a855f7;
|
|
40
|
+
text-style: bold;
|
|
41
|
+
padding: 0 1;
|
|
42
|
+
height: 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Footer styling */
|
|
46
|
+
Footer {
|
|
47
|
+
dock: bottom;
|
|
48
|
+
background: #0a0a0f;
|
|
49
|
+
color: #64748b;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Status bar styling */
|
|
53
|
+
StatusBar {
|
|
54
|
+
dock: bottom;
|
|
55
|
+
background: #0a0a0f;
|
|
56
|
+
color: #64748b;
|
|
57
|
+
border-top: solid #a855f7;
|
|
58
|
+
padding: 0 1;
|
|
59
|
+
height: 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Button styling */
|
|
63
|
+
Button {
|
|
64
|
+
background: #a855f7;
|
|
65
|
+
color: #e2e8f0;
|
|
66
|
+
padding: 0 2;
|
|
67
|
+
border: solid #a855f7;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Button:hover {
|
|
71
|
+
background: #9333ea;
|
|
72
|
+
border: solid #9333ea;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Content width constraints */
|
|
76
|
+
VerticalScroll, HorizontalScroll {
|
|
77
|
+
max-width: 120;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Section headers */
|
|
81
|
+
.section-header {
|
|
82
|
+
text-style: bold;
|
|
83
|
+
color: #7c3aed;
|
|
84
|
+
border-bottom: solid #06b6d4;
|
|
85
|
+
padding: 1 0;
|
|
86
|
+
margin: 1 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Input fields */
|
|
90
|
+
Input {
|
|
91
|
+
border: solid #06b6d4;
|
|
92
|
+
background: #0a0a0f;
|
|
93
|
+
color: #e2e8f0;
|
|
94
|
+
padding: 0 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Input:focus {
|
|
98
|
+
border: solid #06b6d4;
|
|
99
|
+
background: #0a0a0f;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Table styling */
|
|
103
|
+
DataTable {
|
|
104
|
+
background: #0a0a0f;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
DataTable > .datatable--header {
|
|
108
|
+
background: #0a0a0f;
|
|
109
|
+
color: #06b6d4;
|
|
110
|
+
text-style: bold;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
DataTable > .datatable--cursor {
|
|
114
|
+
background: #a855f7 40%;
|
|
115
|
+
border: solid #06b6d4;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Progress bar */
|
|
119
|
+
ProgressBar {
|
|
120
|
+
background: #1a1a2e;
|
|
121
|
+
color: #a855f7;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ProgressBar.complete {
|
|
125
|
+
color: #22c55e;
|
|
126
|
+
}
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
BINDINGS = [
|
|
130
|
+
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
|
|
131
|
+
Binding("ctrl+q", "quit", "Quit", show=True),
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
def __init__(self) -> None:
|
|
135
|
+
super().__init__()
|
|
136
|
+
self.config: AppConfig = load_config()
|
|
137
|
+
self.history = HistoryStore()
|
|
138
|
+
self.history.load()
|
|
139
|
+
|
|
140
|
+
self.manager = DownloadManager(
|
|
141
|
+
self.config,
|
|
142
|
+
on_progress=self._on_download_progress,
|
|
143
|
+
on_finished=self._on_download_finished,
|
|
144
|
+
)
|
|
145
|
+
self.queue = QueueEngine(
|
|
146
|
+
self.config,
|
|
147
|
+
self.manager,
|
|
148
|
+
on_change=self._on_queue_change,
|
|
149
|
+
)
|
|
150
|
+
self.ytdlp = YtDlpManager(self.config)
|
|
151
|
+
self._update_notice: str = ""
|
|
152
|
+
|
|
153
|
+
def compose(self) -> ComposeResult:
|
|
154
|
+
# Status bar is mounted on app level so it persists across screens.
|
|
155
|
+
# Screens have their own Header/Footer; we still dock StatusBar.
|
|
156
|
+
yield StatusBar(id="app-status")
|
|
157
|
+
|
|
158
|
+
def on_mount(self) -> None:
|
|
159
|
+
# Ensure yt-dlp present (async-ish via worker)
|
|
160
|
+
self._startup_ytdlp()
|
|
161
|
+
self.queue.load()
|
|
162
|
+
self.refresh_status_bar()
|
|
163
|
+
# Immediate URL input — no splash
|
|
164
|
+
self.push_screen(URLInputScreen())
|
|
165
|
+
|
|
166
|
+
def get_status_bar(self) -> StatusBar | None:
|
|
167
|
+
try:
|
|
168
|
+
return self.query_one("#app-status", StatusBar)
|
|
169
|
+
except Exception:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def refresh_status_bar(self) -> None:
|
|
173
|
+
bar = self.get_status_bar()
|
|
174
|
+
if bar is None:
|
|
175
|
+
return
|
|
176
|
+
bar.set_status(
|
|
177
|
+
ytdlp_version=self.ytdlp.current_version(),
|
|
178
|
+
queue_size=self.queue.active_size(),
|
|
179
|
+
download_dir=self.config.download_dir,
|
|
180
|
+
notification=self._update_notice,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# ── Startup ─────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
@work(thread=True)
|
|
186
|
+
def _startup_ytdlp(self) -> None:
|
|
187
|
+
"""Install yt-dlp if missing; check for updates if due."""
|
|
188
|
+
if not self.ytdlp.is_installed():
|
|
189
|
+
self.call_from_thread(
|
|
190
|
+
self._set_notice, "Installing yt-dlp…"
|
|
191
|
+
)
|
|
192
|
+
ok = self.ytdlp.ensure_installed()
|
|
193
|
+
if ok:
|
|
194
|
+
self.call_from_thread(
|
|
195
|
+
self._set_notice, f"yt-dlp {self.ytdlp.current_version()} installed."
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
self.call_from_thread(
|
|
199
|
+
self._set_notice, "yt-dlp missing — open Settings to install."
|
|
200
|
+
)
|
|
201
|
+
self.call_from_thread(self.refresh_status_bar)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if self.ytdlp.should_check_on_startup():
|
|
205
|
+
result = self.ytdlp.check_for_updates(force=True)
|
|
206
|
+
if result.update_available:
|
|
207
|
+
notice = f"Update available: yt-dlp {result.latest_version}"
|
|
208
|
+
self.call_from_thread(self._set_notice, notice)
|
|
209
|
+
self.call_from_thread(
|
|
210
|
+
self.notify,
|
|
211
|
+
f"yt-dlp update available: {result.current_version} → {result.latest_version}",
|
|
212
|
+
severity="warning",
|
|
213
|
+
timeout=8,
|
|
214
|
+
)
|
|
215
|
+
self.call_from_thread(self.refresh_status_bar)
|
|
216
|
+
|
|
217
|
+
def _set_notice(self, text: str) -> None:
|
|
218
|
+
self._update_notice = text
|
|
219
|
+
self.refresh_status_bar()
|
|
220
|
+
# Clear notice after a while via timer on main thread
|
|
221
|
+
self.set_timer(12.0, lambda: self._clear_notice_if(text))
|
|
222
|
+
|
|
223
|
+
def _clear_notice_if(self, text: str) -> None:
|
|
224
|
+
if self._update_notice == text:
|
|
225
|
+
self._update_notice = ""
|
|
226
|
+
self.refresh_status_bar()
|
|
227
|
+
|
|
228
|
+
# ── Download callbacks (may run on worker threads) ──────────────────────
|
|
229
|
+
|
|
230
|
+
def _on_download_progress(self, item: DownloadItem) -> None:
|
|
231
|
+
self.queue.sync_from_manager(item)
|
|
232
|
+
# UI widgets poll themselves; just refresh status occasionally
|
|
233
|
+
try:
|
|
234
|
+
self.call_from_thread(self.refresh_status_bar)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
def _on_download_finished(self, item: DownloadItem) -> None:
|
|
239
|
+
self.queue.sync_from_manager(item)
|
|
240
|
+
|
|
241
|
+
status_map = {
|
|
242
|
+
DownloadStatus.COMPLETED: "success",
|
|
243
|
+
DownloadStatus.ERROR: "error",
|
|
244
|
+
DownloadStatus.CANCELLED: "cancelled",
|
|
245
|
+
}
|
|
246
|
+
hist_status = status_map.get(item.status, "error")
|
|
247
|
+
if item.status in (
|
|
248
|
+
DownloadStatus.COMPLETED,
|
|
249
|
+
DownloadStatus.ERROR,
|
|
250
|
+
DownloadStatus.CANCELLED,
|
|
251
|
+
):
|
|
252
|
+
# Calculate filesize if available
|
|
253
|
+
filesize = 0
|
|
254
|
+
if item.progress.total_bytes:
|
|
255
|
+
filesize = item.progress.total_bytes
|
|
256
|
+
elif item.progress.downloaded_bytes > 0:
|
|
257
|
+
filesize = item.progress.downloaded_bytes
|
|
258
|
+
|
|
259
|
+
entry = HistoryEntry.create(
|
|
260
|
+
title=item.title,
|
|
261
|
+
url=item.url,
|
|
262
|
+
format_id=item.format_id,
|
|
263
|
+
file_path=item.file_path,
|
|
264
|
+
status=hist_status,
|
|
265
|
+
error=item.error,
|
|
266
|
+
filesize=filesize,
|
|
267
|
+
)
|
|
268
|
+
try:
|
|
269
|
+
self.history.add(entry)
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
def _notify() -> None:
|
|
274
|
+
self.refresh_status_bar()
|
|
275
|
+
if item.status == DownloadStatus.COMPLETED:
|
|
276
|
+
self.notify(f"Done: {item.title[:50]}", severity="information")
|
|
277
|
+
elif item.status == DownloadStatus.ERROR:
|
|
278
|
+
self.notify(
|
|
279
|
+
f"Failed: {item.error or item.title[:40]}",
|
|
280
|
+
severity="error",
|
|
281
|
+
timeout=8,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
self.call_from_thread(_notify)
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
def _on_queue_change(self) -> None:
|
|
290
|
+
try:
|
|
291
|
+
self.call_from_thread(self.refresh_status_bar)
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
def action_quit(self) -> None:
|
|
296
|
+
try:
|
|
297
|
+
self.queue.save()
|
|
298
|
+
save_config(self.config)
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
self.exit()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Download engine: formats, downloader, queue, yt-dlp updater."""
|
|
2
|
+
|
|
3
|
+
from yank.engine.downloader import DownloadItem, DownloadManager, DownloadProgress
|
|
4
|
+
from yank.engine.formats import (
|
|
5
|
+
FormatInfo,
|
|
6
|
+
VideoMeta,
|
|
7
|
+
detect_platform,
|
|
8
|
+
extract_formats,
|
|
9
|
+
pick_best_format,
|
|
10
|
+
)
|
|
11
|
+
from yank.engine.queue import QueueEngine, QueueItem
|
|
12
|
+
from yank.engine.updater import YtDlpManager
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DownloadItem",
|
|
16
|
+
"DownloadManager",
|
|
17
|
+
"DownloadProgress",
|
|
18
|
+
"FormatInfo",
|
|
19
|
+
"QueueEngine",
|
|
20
|
+
"QueueItem",
|
|
21
|
+
"VideoMeta",
|
|
22
|
+
"YtDlpManager",
|
|
23
|
+
"detect_platform",
|
|
24
|
+
"extract_formats",
|
|
25
|
+
"pick_best_format",
|
|
26
|
+
]
|