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.
Files changed (34) hide show
  1. yank_tui-0.1.0/PKG-INFO +145 -0
  2. yank_tui-0.1.0/README.md +117 -0
  3. yank_tui-0.1.0/pyproject.toml +50 -0
  4. yank_tui-0.1.0/setup.cfg +4 -0
  5. yank_tui-0.1.0/src/yank/__init__.py +3 -0
  6. yank_tui-0.1.0/src/yank/__main__.py +15 -0
  7. yank_tui-0.1.0/src/yank/app.py +301 -0
  8. yank_tui-0.1.0/src/yank/engine/__init__.py +26 -0
  9. yank_tui-0.1.0/src/yank/engine/downloader.py +550 -0
  10. yank_tui-0.1.0/src/yank/engine/formats.py +427 -0
  11. yank_tui-0.1.0/src/yank/engine/queue.py +365 -0
  12. yank_tui-0.1.0/src/yank/engine/updater.py +272 -0
  13. yank_tui-0.1.0/src/yank/models/__init__.py +12 -0
  14. yank_tui-0.1.0/src/yank/models/config.py +117 -0
  15. yank_tui-0.1.0/src/yank/models/history.py +135 -0
  16. yank_tui-0.1.0/src/yank/screens/__init__.py +19 -0
  17. yank_tui-0.1.0/src/yank/screens/download.py +231 -0
  18. yank_tui-0.1.0/src/yank/screens/format_select.py +205 -0
  19. yank_tui-0.1.0/src/yank/screens/history.py +228 -0
  20. yank_tui-0.1.0/src/yank/screens/options.py +410 -0
  21. yank_tui-0.1.0/src/yank/screens/queue.py +236 -0
  22. yank_tui-0.1.0/src/yank/screens/settings.py +330 -0
  23. yank_tui-0.1.0/src/yank/screens/url_input.py +295 -0
  24. yank_tui-0.1.0/src/yank/widgets/__init__.py +7 -0
  25. yank_tui-0.1.0/src/yank/widgets/format_table.py +141 -0
  26. yank_tui-0.1.0/src/yank/widgets/progress.py +173 -0
  27. yank_tui-0.1.0/src/yank/widgets/status_bar.py +132 -0
  28. yank_tui-0.1.0/src/yank_tui.egg-info/PKG-INFO +145 -0
  29. yank_tui-0.1.0/src/yank_tui.egg-info/SOURCES.txt +32 -0
  30. yank_tui-0.1.0/src/yank_tui.egg-info/dependency_links.txt +1 -0
  31. yank_tui-0.1.0/src/yank_tui.egg-info/entry_points.txt +2 -0
  32. yank_tui-0.1.0/src/yank_tui.egg-info/requires.txt +6 -0
  33. yank_tui-0.1.0/src/yank_tui.egg-info/top_level.txt +1 -0
  34. yank_tui-0.1.0/tests/test_formats.py +297 -0
@@ -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
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """yank — a TUI wrapper for yt-dlp."""
2
+
3
+ __version__ = "0.1.0"
@@ -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
+ ]