Siphon-TUI 2.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.
- siphon_tui-2.1.0/LICENSE +21 -0
- siphon_tui-2.1.0/PKG-INFO +192 -0
- siphon_tui-2.1.0/README.md +156 -0
- siphon_tui-2.1.0/Siphon_TUI.egg-info/PKG-INFO +192 -0
- siphon_tui-2.1.0/Siphon_TUI.egg-info/SOURCES.txt +17 -0
- siphon_tui-2.1.0/Siphon_TUI.egg-info/dependency_links.txt +1 -0
- siphon_tui-2.1.0/Siphon_TUI.egg-info/entry_points.txt +2 -0
- siphon_tui-2.1.0/Siphon_TUI.egg-info/requires.txt +9 -0
- siphon_tui-2.1.0/Siphon_TUI.egg-info/top_level.txt +1 -0
- siphon_tui-2.1.0/pyproject.toml +64 -0
- siphon_tui-2.1.0/setup.cfg +4 -0
- siphon_tui-2.1.0/siphon_tui/__init__.py +5 -0
- siphon_tui-2.1.0/siphon_tui/__main__.py +4 -0
- siphon_tui-2.1.0/siphon_tui/cli.py +18 -0
- siphon_tui-2.1.0/siphon_tui/main.py +26 -0
- siphon_tui-2.1.0/siphon_tui/tui/app.py +234 -0
- siphon_tui-2.1.0/siphon_tui/utils/__init__.py +0 -0
- siphon_tui-2.1.0/siphon_tui/utils/configer.py +89 -0
- siphon_tui-2.1.0/siphon_tui/utils/download.py +133 -0
siphon_tui-2.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kernel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Siphon-TUI
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Siphon-TUI is a TUI audio/video downloader based on yt-dlp
|
|
5
|
+
Project-URL: Homepage, https://github.com/Fkernel653/Siphon-TUI
|
|
6
|
+
Project-URL: Repository, https://github.com/Fkernel653/Siphon-TUI.git
|
|
7
|
+
Project-URL: Documentation, https://github.com/Fkernel653/Siphon-TUI#readme
|
|
8
|
+
Keywords: yt-dlp,downloader,tui,terminal,textual,video,audio,youtube,cli,multimedia
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Terminals
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: yt-dlp
|
|
28
|
+
Requires-Dist: textual
|
|
29
|
+
Requires-Dist: mutagen
|
|
30
|
+
Requires-Dist: color-kiss
|
|
31
|
+
Requires-Dist: cliss
|
|
32
|
+
Requires-Dist: platformdirs
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: ruff; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Siphon-TUI — Download audio/video from YouTube, SoundCloud, and 1000+ sites via interactive terminal UI
|
|
38
|
+
|
|
39
|
+
[](https://python.org)
|
|
40
|
+
[](https://pypi.org/project/siphon-tui/)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[]()
|
|
43
|
+
[](https://github.com/Textualize/textual)
|
|
44
|
+
[](https://docs.astral.sh/ruff/)
|
|
45
|
+
|
|
46
|
+
Download and tag high-quality music and video from YouTube, YouTube Music, SoundCloud, and 1000+ sites — all from an interactive terminal UI.
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
## ✨ Features
|
|
51
|
+
|
|
52
|
+
- **Interactive TUI** — Dropdown selectors, real-time notifications, cancel support
|
|
53
|
+
- **1000+ Supported Sites** — Any site yt-dlp supports
|
|
54
|
+
- **Audio/Video Formats** — MP3, AAC, FLAC, M4A, Opus, Vorbis, WAV, MP4, MKV, WebM, and more with configurable bitrate (64–320 kbps)
|
|
55
|
+
- **Smart Codec Mapping** — Automatically pairs containers with optimal audio codecs (e.g., MP4→AAC, MKV→Opus)
|
|
56
|
+
- **Metadata Embedding** — Title, artist, album tags + cover art thumbnails
|
|
57
|
+
- **Thread-safe** — Responsive UI during downloads with background processing
|
|
58
|
+
- **Cross-platform Config** — XDG-compliant (Linux), Application Support (macOS), AppData (Windows)
|
|
59
|
+
|
|
60
|
+
## 🚀 Quick Start
|
|
61
|
+
|
|
62
|
+
### Prerequisites
|
|
63
|
+
- Python 3.10+ & FFmpeg
|
|
64
|
+
|
|
65
|
+
### Installation
|
|
66
|
+
```bash
|
|
67
|
+
pip install siphon-tui # pip
|
|
68
|
+
uv pip install siphon-tui # uv
|
|
69
|
+
pipx install siphon-tui # pipx
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Usage
|
|
73
|
+
```bash
|
|
74
|
+
siphon-tui config ~/Downloads # Set download directory (optional)
|
|
75
|
+
siphon-tui # Launch TUI (no arguments)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If you skip `config`, files will be saved to `~/Downloads` (or platform equivalent).
|
|
79
|
+
|
|
80
|
+
## ⌨️ Controls
|
|
81
|
+
|
|
82
|
+
| Key | Action |
|
|
83
|
+
|-----|--------|
|
|
84
|
+
| `Tab` | Navigate between fields |
|
|
85
|
+
| `↑`/`↓` | Navigate dropdown options |
|
|
86
|
+
| `Enter` | Confirm selection / Start download |
|
|
87
|
+
| `Esc` | Close dropdown |
|
|
88
|
+
| `Ctrl+C` | Exit application |
|
|
89
|
+
|
|
90
|
+
## 📋 Interface Elements
|
|
91
|
+
|
|
92
|
+
### Input Fields
|
|
93
|
+
| Field | Description |
|
|
94
|
+
|-------|-------------|
|
|
95
|
+
| **URL Input** | Paste video/audio URL from any supported platform |
|
|
96
|
+
| **Audio Codec** | Select audio format: MP3, AAC, FLAC, M4A, Opus, Vorbis, WAV |
|
|
97
|
+
| **Container** | Optional video container: MP4, MKV, WebM, MOV, AVI, FLV |
|
|
98
|
+
| **Bitrate** | Audio quality: 64, 128, 256, 320 kbps |
|
|
99
|
+
|
|
100
|
+
### Buttons
|
|
101
|
+
| Button | Action |
|
|
102
|
+
|--------|--------|
|
|
103
|
+
| **Download** | Start download with selected settings |
|
|
104
|
+
| **Cancel** | Cancel ongoing download |
|
|
105
|
+
|
|
106
|
+
### Smart Codec Mapping
|
|
107
|
+
When a video container is selected, the optimal audio codec is automatically set:
|
|
108
|
+
|
|
109
|
+
| Container | Auto Audio Codec |
|
|
110
|
+
|-----------|:----------------:|
|
|
111
|
+
| MP4, MOV | AAC |
|
|
112
|
+
| MKV, WebM | Opus |
|
|
113
|
+
| AVI | MP3 |
|
|
114
|
+
| FLV | AAC |
|
|
115
|
+
|
|
116
|
+
## 📖 Examples
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Audio download
|
|
120
|
+
siphon-tui
|
|
121
|
+
# → Paste URL → Select "mp3" → Select "320" kbps → Press Download
|
|
122
|
+
|
|
123
|
+
# Video download
|
|
124
|
+
siphon-tui
|
|
125
|
+
# → Paste URL → Select Container "mp4" → Bitrate auto-sets → Press Download
|
|
126
|
+
|
|
127
|
+
# Cancel download
|
|
128
|
+
# Press "Cancel" button during active download
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 📁 Project Structure
|
|
132
|
+
```
|
|
133
|
+
siphon_tui/
|
|
134
|
+
├── __init__.py
|
|
135
|
+
├── main.py # Entry point & CLI/TUI routing
|
|
136
|
+
├── cli.py # CLI interface (cliss)
|
|
137
|
+
├── tui/
|
|
138
|
+
│ ├── app.py # Textual TUI application
|
|
139
|
+
│ └── style.tcss # TUI theme & layout
|
|
140
|
+
└── utils/
|
|
141
|
+
├── configer.py # JSON config manager
|
|
142
|
+
└── download.py # Download engine (yt-dlp + mutagen)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## ⚙️ Configuration
|
|
146
|
+
|
|
147
|
+
The download path is stored in a JSON config file and can be set via CLI:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
siphon-tui config ~/Music # Set directory
|
|
151
|
+
siphon-tui config # View current path (if implemented)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Config locations (auto-managed):
|
|
155
|
+
- **Linux:** `~/.config/siphon-tui/config.json`
|
|
156
|
+
- **macOS:** `~/Library/Application Support/siphon-tui/config.json`
|
|
157
|
+
- **Windows:** `%APPDATA%\siphon-tui\config.json`
|
|
158
|
+
|
|
159
|
+
## 🔧 Requirements
|
|
160
|
+
|
|
161
|
+
| Dependency | Purpose |
|
|
162
|
+
|------------|---------|
|
|
163
|
+
| `textual` | TUI framework for interactive terminal apps |
|
|
164
|
+
| `yt-dlp` | Media extraction from 1000+ platforms |
|
|
165
|
+
| `mutagen` | Audio metadata tagging and cover art embedding |
|
|
166
|
+
| `platformdirs` | Cross-platform config paths |
|
|
167
|
+
| `color-kiss` | Terminal colors |
|
|
168
|
+
| `cliss` | CLI framework |
|
|
169
|
+
| **FFmpeg** | Audio/video conversion (system) |
|
|
170
|
+
|
|
171
|
+
## 📄 License
|
|
172
|
+
|
|
173
|
+
MIT License — see [LICENSE](LICENSE) file.
|
|
174
|
+
|
|
175
|
+
## 🙏 Acknowledgments
|
|
176
|
+
|
|
177
|
+
- [Textual](https://github.com/Textualize/textual) – Modern TUI framework
|
|
178
|
+
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) – Download engine
|
|
179
|
+
- [mutagen](https://github.com/quodlibet/mutagen) – Metadata tagging
|
|
180
|
+
- [platformdirs](https://github.com/platformdirs/platformdirs) – Config paths
|
|
181
|
+
- [color-kiss](https://github.com/Fkernel653/color-kiss) – Terminal colors
|
|
182
|
+
- [cliss](https://github.com/Fkernel653/cliss) – CLI framework
|
|
183
|
+
|
|
184
|
+
## ⚠️ Disclaimer
|
|
185
|
+
|
|
186
|
+
**For educational purposes only.** Users are responsible for complying with platform Terms of Service and applicable copyright laws. Download only content you have permission to download.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
**Author:** [Fkernel653](https://github.com/Fkernel653)
|
|
191
|
+
**Repository:** [github.com/Fkernel653/Siphon-TUI](https://github.com/Fkernel653/Siphon-TUI)
|
|
192
|
+
**PyPI:** [pypi.org/project/siphon-tui](https://pypi.org/project/siphon-tui/)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Siphon-TUI — Download audio/video from YouTube, SoundCloud, and 1000+ sites via interactive terminal UI
|
|
2
|
+
|
|
3
|
+
[](https://python.org)
|
|
4
|
+
[](https://pypi.org/project/siphon-tui/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[]()
|
|
7
|
+
[](https://github.com/Textualize/textual)
|
|
8
|
+
[](https://docs.astral.sh/ruff/)
|
|
9
|
+
|
|
10
|
+
Download and tag high-quality music and video from YouTube, YouTube Music, SoundCloud, and 1000+ sites — all from an interactive terminal UI.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- **Interactive TUI** — Dropdown selectors, real-time notifications, cancel support
|
|
17
|
+
- **1000+ Supported Sites** — Any site yt-dlp supports
|
|
18
|
+
- **Audio/Video Formats** — MP3, AAC, FLAC, M4A, Opus, Vorbis, WAV, MP4, MKV, WebM, and more with configurable bitrate (64–320 kbps)
|
|
19
|
+
- **Smart Codec Mapping** — Automatically pairs containers with optimal audio codecs (e.g., MP4→AAC, MKV→Opus)
|
|
20
|
+
- **Metadata Embedding** — Title, artist, album tags + cover art thumbnails
|
|
21
|
+
- **Thread-safe** — Responsive UI during downloads with background processing
|
|
22
|
+
- **Cross-platform Config** — XDG-compliant (Linux), Application Support (macOS), AppData (Windows)
|
|
23
|
+
|
|
24
|
+
## 🚀 Quick Start
|
|
25
|
+
|
|
26
|
+
### Prerequisites
|
|
27
|
+
- Python 3.10+ & FFmpeg
|
|
28
|
+
|
|
29
|
+
### Installation
|
|
30
|
+
```bash
|
|
31
|
+
pip install siphon-tui # pip
|
|
32
|
+
uv pip install siphon-tui # uv
|
|
33
|
+
pipx install siphon-tui # pipx
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Usage
|
|
37
|
+
```bash
|
|
38
|
+
siphon-tui config ~/Downloads # Set download directory (optional)
|
|
39
|
+
siphon-tui # Launch TUI (no arguments)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If you skip `config`, files will be saved to `~/Downloads` (or platform equivalent).
|
|
43
|
+
|
|
44
|
+
## ⌨️ Controls
|
|
45
|
+
|
|
46
|
+
| Key | Action |
|
|
47
|
+
|-----|--------|
|
|
48
|
+
| `Tab` | Navigate between fields |
|
|
49
|
+
| `↑`/`↓` | Navigate dropdown options |
|
|
50
|
+
| `Enter` | Confirm selection / Start download |
|
|
51
|
+
| `Esc` | Close dropdown |
|
|
52
|
+
| `Ctrl+C` | Exit application |
|
|
53
|
+
|
|
54
|
+
## 📋 Interface Elements
|
|
55
|
+
|
|
56
|
+
### Input Fields
|
|
57
|
+
| Field | Description |
|
|
58
|
+
|-------|-------------|
|
|
59
|
+
| **URL Input** | Paste video/audio URL from any supported platform |
|
|
60
|
+
| **Audio Codec** | Select audio format: MP3, AAC, FLAC, M4A, Opus, Vorbis, WAV |
|
|
61
|
+
| **Container** | Optional video container: MP4, MKV, WebM, MOV, AVI, FLV |
|
|
62
|
+
| **Bitrate** | Audio quality: 64, 128, 256, 320 kbps |
|
|
63
|
+
|
|
64
|
+
### Buttons
|
|
65
|
+
| Button | Action |
|
|
66
|
+
|--------|--------|
|
|
67
|
+
| **Download** | Start download with selected settings |
|
|
68
|
+
| **Cancel** | Cancel ongoing download |
|
|
69
|
+
|
|
70
|
+
### Smart Codec Mapping
|
|
71
|
+
When a video container is selected, the optimal audio codec is automatically set:
|
|
72
|
+
|
|
73
|
+
| Container | Auto Audio Codec |
|
|
74
|
+
|-----------|:----------------:|
|
|
75
|
+
| MP4, MOV | AAC |
|
|
76
|
+
| MKV, WebM | Opus |
|
|
77
|
+
| AVI | MP3 |
|
|
78
|
+
| FLV | AAC |
|
|
79
|
+
|
|
80
|
+
## 📖 Examples
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Audio download
|
|
84
|
+
siphon-tui
|
|
85
|
+
# → Paste URL → Select "mp3" → Select "320" kbps → Press Download
|
|
86
|
+
|
|
87
|
+
# Video download
|
|
88
|
+
siphon-tui
|
|
89
|
+
# → Paste URL → Select Container "mp4" → Bitrate auto-sets → Press Download
|
|
90
|
+
|
|
91
|
+
# Cancel download
|
|
92
|
+
# Press "Cancel" button during active download
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 📁 Project Structure
|
|
96
|
+
```
|
|
97
|
+
siphon_tui/
|
|
98
|
+
├── __init__.py
|
|
99
|
+
├── main.py # Entry point & CLI/TUI routing
|
|
100
|
+
├── cli.py # CLI interface (cliss)
|
|
101
|
+
├── tui/
|
|
102
|
+
│ ├── app.py # Textual TUI application
|
|
103
|
+
│ └── style.tcss # TUI theme & layout
|
|
104
|
+
└── utils/
|
|
105
|
+
├── configer.py # JSON config manager
|
|
106
|
+
└── download.py # Download engine (yt-dlp + mutagen)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## ⚙️ Configuration
|
|
110
|
+
|
|
111
|
+
The download path is stored in a JSON config file and can be set via CLI:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
siphon-tui config ~/Music # Set directory
|
|
115
|
+
siphon-tui config # View current path (if implemented)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Config locations (auto-managed):
|
|
119
|
+
- **Linux:** `~/.config/siphon-tui/config.json`
|
|
120
|
+
- **macOS:** `~/Library/Application Support/siphon-tui/config.json`
|
|
121
|
+
- **Windows:** `%APPDATA%\siphon-tui\config.json`
|
|
122
|
+
|
|
123
|
+
## 🔧 Requirements
|
|
124
|
+
|
|
125
|
+
| Dependency | Purpose |
|
|
126
|
+
|------------|---------|
|
|
127
|
+
| `textual` | TUI framework for interactive terminal apps |
|
|
128
|
+
| `yt-dlp` | Media extraction from 1000+ platforms |
|
|
129
|
+
| `mutagen` | Audio metadata tagging and cover art embedding |
|
|
130
|
+
| `platformdirs` | Cross-platform config paths |
|
|
131
|
+
| `color-kiss` | Terminal colors |
|
|
132
|
+
| `cliss` | CLI framework |
|
|
133
|
+
| **FFmpeg** | Audio/video conversion (system) |
|
|
134
|
+
|
|
135
|
+
## 📄 License
|
|
136
|
+
|
|
137
|
+
MIT License — see [LICENSE](LICENSE) file.
|
|
138
|
+
|
|
139
|
+
## 🙏 Acknowledgments
|
|
140
|
+
|
|
141
|
+
- [Textual](https://github.com/Textualize/textual) – Modern TUI framework
|
|
142
|
+
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) – Download engine
|
|
143
|
+
- [mutagen](https://github.com/quodlibet/mutagen) – Metadata tagging
|
|
144
|
+
- [platformdirs](https://github.com/platformdirs/platformdirs) – Config paths
|
|
145
|
+
- [color-kiss](https://github.com/Fkernel653/color-kiss) – Terminal colors
|
|
146
|
+
- [cliss](https://github.com/Fkernel653/cliss) – CLI framework
|
|
147
|
+
|
|
148
|
+
## ⚠️ Disclaimer
|
|
149
|
+
|
|
150
|
+
**For educational purposes only.** Users are responsible for complying with platform Terms of Service and applicable copyright laws. Download only content you have permission to download.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
**Author:** [Fkernel653](https://github.com/Fkernel653)
|
|
155
|
+
**Repository:** [github.com/Fkernel653/Siphon-TUI](https://github.com/Fkernel653/Siphon-TUI)
|
|
156
|
+
**PyPI:** [pypi.org/project/siphon-tui](https://pypi.org/project/siphon-tui/)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Siphon-TUI
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Siphon-TUI is a TUI audio/video downloader based on yt-dlp
|
|
5
|
+
Project-URL: Homepage, https://github.com/Fkernel653/Siphon-TUI
|
|
6
|
+
Project-URL: Repository, https://github.com/Fkernel653/Siphon-TUI.git
|
|
7
|
+
Project-URL: Documentation, https://github.com/Fkernel653/Siphon-TUI#readme
|
|
8
|
+
Keywords: yt-dlp,downloader,tui,terminal,textual,video,audio,youtube,cli,multimedia
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Terminals
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: yt-dlp
|
|
28
|
+
Requires-Dist: textual
|
|
29
|
+
Requires-Dist: mutagen
|
|
30
|
+
Requires-Dist: color-kiss
|
|
31
|
+
Requires-Dist: cliss
|
|
32
|
+
Requires-Dist: platformdirs
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: ruff; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Siphon-TUI — Download audio/video from YouTube, SoundCloud, and 1000+ sites via interactive terminal UI
|
|
38
|
+
|
|
39
|
+
[](https://python.org)
|
|
40
|
+
[](https://pypi.org/project/siphon-tui/)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[]()
|
|
43
|
+
[](https://github.com/Textualize/textual)
|
|
44
|
+
[](https://docs.astral.sh/ruff/)
|
|
45
|
+
|
|
46
|
+
Download and tag high-quality music and video from YouTube, YouTube Music, SoundCloud, and 1000+ sites — all from an interactive terminal UI.
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
## ✨ Features
|
|
51
|
+
|
|
52
|
+
- **Interactive TUI** — Dropdown selectors, real-time notifications, cancel support
|
|
53
|
+
- **1000+ Supported Sites** — Any site yt-dlp supports
|
|
54
|
+
- **Audio/Video Formats** — MP3, AAC, FLAC, M4A, Opus, Vorbis, WAV, MP4, MKV, WebM, and more with configurable bitrate (64–320 kbps)
|
|
55
|
+
- **Smart Codec Mapping** — Automatically pairs containers with optimal audio codecs (e.g., MP4→AAC, MKV→Opus)
|
|
56
|
+
- **Metadata Embedding** — Title, artist, album tags + cover art thumbnails
|
|
57
|
+
- **Thread-safe** — Responsive UI during downloads with background processing
|
|
58
|
+
- **Cross-platform Config** — XDG-compliant (Linux), Application Support (macOS), AppData (Windows)
|
|
59
|
+
|
|
60
|
+
## 🚀 Quick Start
|
|
61
|
+
|
|
62
|
+
### Prerequisites
|
|
63
|
+
- Python 3.10+ & FFmpeg
|
|
64
|
+
|
|
65
|
+
### Installation
|
|
66
|
+
```bash
|
|
67
|
+
pip install siphon-tui # pip
|
|
68
|
+
uv pip install siphon-tui # uv
|
|
69
|
+
pipx install siphon-tui # pipx
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Usage
|
|
73
|
+
```bash
|
|
74
|
+
siphon-tui config ~/Downloads # Set download directory (optional)
|
|
75
|
+
siphon-tui # Launch TUI (no arguments)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If you skip `config`, files will be saved to `~/Downloads` (or platform equivalent).
|
|
79
|
+
|
|
80
|
+
## ⌨️ Controls
|
|
81
|
+
|
|
82
|
+
| Key | Action |
|
|
83
|
+
|-----|--------|
|
|
84
|
+
| `Tab` | Navigate between fields |
|
|
85
|
+
| `↑`/`↓` | Navigate dropdown options |
|
|
86
|
+
| `Enter` | Confirm selection / Start download |
|
|
87
|
+
| `Esc` | Close dropdown |
|
|
88
|
+
| `Ctrl+C` | Exit application |
|
|
89
|
+
|
|
90
|
+
## 📋 Interface Elements
|
|
91
|
+
|
|
92
|
+
### Input Fields
|
|
93
|
+
| Field | Description |
|
|
94
|
+
|-------|-------------|
|
|
95
|
+
| **URL Input** | Paste video/audio URL from any supported platform |
|
|
96
|
+
| **Audio Codec** | Select audio format: MP3, AAC, FLAC, M4A, Opus, Vorbis, WAV |
|
|
97
|
+
| **Container** | Optional video container: MP4, MKV, WebM, MOV, AVI, FLV |
|
|
98
|
+
| **Bitrate** | Audio quality: 64, 128, 256, 320 kbps |
|
|
99
|
+
|
|
100
|
+
### Buttons
|
|
101
|
+
| Button | Action |
|
|
102
|
+
|--------|--------|
|
|
103
|
+
| **Download** | Start download with selected settings |
|
|
104
|
+
| **Cancel** | Cancel ongoing download |
|
|
105
|
+
|
|
106
|
+
### Smart Codec Mapping
|
|
107
|
+
When a video container is selected, the optimal audio codec is automatically set:
|
|
108
|
+
|
|
109
|
+
| Container | Auto Audio Codec |
|
|
110
|
+
|-----------|:----------------:|
|
|
111
|
+
| MP4, MOV | AAC |
|
|
112
|
+
| MKV, WebM | Opus |
|
|
113
|
+
| AVI | MP3 |
|
|
114
|
+
| FLV | AAC |
|
|
115
|
+
|
|
116
|
+
## 📖 Examples
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Audio download
|
|
120
|
+
siphon-tui
|
|
121
|
+
# → Paste URL → Select "mp3" → Select "320" kbps → Press Download
|
|
122
|
+
|
|
123
|
+
# Video download
|
|
124
|
+
siphon-tui
|
|
125
|
+
# → Paste URL → Select Container "mp4" → Bitrate auto-sets → Press Download
|
|
126
|
+
|
|
127
|
+
# Cancel download
|
|
128
|
+
# Press "Cancel" button during active download
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 📁 Project Structure
|
|
132
|
+
```
|
|
133
|
+
siphon_tui/
|
|
134
|
+
├── __init__.py
|
|
135
|
+
├── main.py # Entry point & CLI/TUI routing
|
|
136
|
+
├── cli.py # CLI interface (cliss)
|
|
137
|
+
├── tui/
|
|
138
|
+
│ ├── app.py # Textual TUI application
|
|
139
|
+
│ └── style.tcss # TUI theme & layout
|
|
140
|
+
└── utils/
|
|
141
|
+
├── configer.py # JSON config manager
|
|
142
|
+
└── download.py # Download engine (yt-dlp + mutagen)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## ⚙️ Configuration
|
|
146
|
+
|
|
147
|
+
The download path is stored in a JSON config file and can be set via CLI:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
siphon-tui config ~/Music # Set directory
|
|
151
|
+
siphon-tui config # View current path (if implemented)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Config locations (auto-managed):
|
|
155
|
+
- **Linux:** `~/.config/siphon-tui/config.json`
|
|
156
|
+
- **macOS:** `~/Library/Application Support/siphon-tui/config.json`
|
|
157
|
+
- **Windows:** `%APPDATA%\siphon-tui\config.json`
|
|
158
|
+
|
|
159
|
+
## 🔧 Requirements
|
|
160
|
+
|
|
161
|
+
| Dependency | Purpose |
|
|
162
|
+
|------------|---------|
|
|
163
|
+
| `textual` | TUI framework for interactive terminal apps |
|
|
164
|
+
| `yt-dlp` | Media extraction from 1000+ platforms |
|
|
165
|
+
| `mutagen` | Audio metadata tagging and cover art embedding |
|
|
166
|
+
| `platformdirs` | Cross-platform config paths |
|
|
167
|
+
| `color-kiss` | Terminal colors |
|
|
168
|
+
| `cliss` | CLI framework |
|
|
169
|
+
| **FFmpeg** | Audio/video conversion (system) |
|
|
170
|
+
|
|
171
|
+
## 📄 License
|
|
172
|
+
|
|
173
|
+
MIT License — see [LICENSE](LICENSE) file.
|
|
174
|
+
|
|
175
|
+
## 🙏 Acknowledgments
|
|
176
|
+
|
|
177
|
+
- [Textual](https://github.com/Textualize/textual) – Modern TUI framework
|
|
178
|
+
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) – Download engine
|
|
179
|
+
- [mutagen](https://github.com/quodlibet/mutagen) – Metadata tagging
|
|
180
|
+
- [platformdirs](https://github.com/platformdirs/platformdirs) – Config paths
|
|
181
|
+
- [color-kiss](https://github.com/Fkernel653/color-kiss) – Terminal colors
|
|
182
|
+
- [cliss](https://github.com/Fkernel653/cliss) – CLI framework
|
|
183
|
+
|
|
184
|
+
## ⚠️ Disclaimer
|
|
185
|
+
|
|
186
|
+
**For educational purposes only.** Users are responsible for complying with platform Terms of Service and applicable copyright laws. Download only content you have permission to download.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
**Author:** [Fkernel653](https://github.com/Fkernel653)
|
|
191
|
+
**Repository:** [github.com/Fkernel653/Siphon-TUI](https://github.com/Fkernel653/Siphon-TUI)
|
|
192
|
+
**PyPI:** [pypi.org/project/siphon-tui](https://pypi.org/project/siphon-tui/)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
Siphon_TUI.egg-info/PKG-INFO
|
|
5
|
+
Siphon_TUI.egg-info/SOURCES.txt
|
|
6
|
+
Siphon_TUI.egg-info/dependency_links.txt
|
|
7
|
+
Siphon_TUI.egg-info/entry_points.txt
|
|
8
|
+
Siphon_TUI.egg-info/requires.txt
|
|
9
|
+
Siphon_TUI.egg-info/top_level.txt
|
|
10
|
+
siphon_tui/__init__.py
|
|
11
|
+
siphon_tui/__main__.py
|
|
12
|
+
siphon_tui/cli.py
|
|
13
|
+
siphon_tui/main.py
|
|
14
|
+
siphon_tui/tui/app.py
|
|
15
|
+
siphon_tui/utils/__init__.py
|
|
16
|
+
siphon_tui/utils/configer.py
|
|
17
|
+
siphon_tui/utils/download.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
siphon_tui
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "Siphon-TUI"
|
|
7
|
+
version = "2.1.0"
|
|
8
|
+
description = "Siphon-TUI is a TUI audio/video downloader based on yt-dlp"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
keywords = [
|
|
12
|
+
"yt-dlp",
|
|
13
|
+
"downloader",
|
|
14
|
+
"tui",
|
|
15
|
+
"terminal",
|
|
16
|
+
"textual",
|
|
17
|
+
"video",
|
|
18
|
+
"audio",
|
|
19
|
+
"youtube",
|
|
20
|
+
"cli",
|
|
21
|
+
"multimedia",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Environment :: Console",
|
|
25
|
+
"Intended Audience :: End Users/Desktop",
|
|
26
|
+
"Natural Language :: English",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
35
|
+
"Topic :: Multimedia :: Video",
|
|
36
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
37
|
+
"Topic :: Terminals",
|
|
38
|
+
"Topic :: Utilities",
|
|
39
|
+
]
|
|
40
|
+
dependencies = [
|
|
41
|
+
"yt-dlp",
|
|
42
|
+
"textual",
|
|
43
|
+
"mutagen",
|
|
44
|
+
"color-kiss",
|
|
45
|
+
"cliss",
|
|
46
|
+
"platformdirs",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
dev = ["ruff"]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/Fkernel653/Siphon-TUI"
|
|
54
|
+
Repository = "https://github.com/Fkernel653/Siphon-TUI.git"
|
|
55
|
+
Documentation = "https://github.com/Fkernel653/Siphon-TUI#readme"
|
|
56
|
+
|
|
57
|
+
[project.scripts]
|
|
58
|
+
siphon-tui = "siphon_tui.main:main"
|
|
59
|
+
|
|
60
|
+
[tool.setuptools.packages.find]
|
|
61
|
+
include = ["siphon_tui*"]
|
|
62
|
+
|
|
63
|
+
[tool.setuptools.package-data]
|
|
64
|
+
siphon_tui = ["*.tcss", "*.json"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
def run_cli():
|
|
2
|
+
from cliss import CLI
|
|
3
|
+
|
|
4
|
+
from .tui.app import get_version
|
|
5
|
+
|
|
6
|
+
app = CLI(
|
|
7
|
+
name="Siphon-TUI",
|
|
8
|
+
description="Siphon-TUI is a TUI audio/video downloader based on yt-dlp",
|
|
9
|
+
version=get_version(),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def config(path: str):
|
|
14
|
+
from .utils.configer import set_path
|
|
15
|
+
|
|
16
|
+
print(set_path(path))
|
|
17
|
+
|
|
18
|
+
app.run()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
def main():
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from color_kiss import GREEN, RESET
|
|
5
|
+
from color_kiss.utils import error
|
|
6
|
+
|
|
7
|
+
if len(sys.argv) <= 1:
|
|
8
|
+
from .tui.app import run_tui
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
run_tui()
|
|
12
|
+
except KeyboardInterrupt:
|
|
13
|
+
print(f"{GREEN}Goodbye!{RESET}")
|
|
14
|
+
sys.exit(0)
|
|
15
|
+
except Exception as e:
|
|
16
|
+
sys.exit(error(str(e)))
|
|
17
|
+
else:
|
|
18
|
+
from .cli import run_cli
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
run_cli()
|
|
22
|
+
except KeyboardInterrupt:
|
|
23
|
+
print(f"{GREEN}Goodbye!{RESET}")
|
|
24
|
+
sys.exit(0)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
sys.exit(error(str(e)))
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from threading import Thread
|
|
2
|
+
|
|
3
|
+
from textual import on
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
6
|
+
from textual.widgets import Button, Footer, Header, Input, Select
|
|
7
|
+
|
|
8
|
+
from siphon_tui.utils.configer import get_path
|
|
9
|
+
|
|
10
|
+
AUDIO_CODECS = ["M4A", "MP3", "FLAC", "Opus", "Vorbis", "WAV"]
|
|
11
|
+
VIDEO_CONTAINERS = ["MP4", "MKV", "WebM", "MOV", "AVI", "FLV"]
|
|
12
|
+
LINES_KBPS = ["320", "256", "128", "64"]
|
|
13
|
+
|
|
14
|
+
VIDEO_CONTAINER_AUDIO_MAP = {
|
|
15
|
+
"mp4": "m4a",
|
|
16
|
+
"mov": "m4a",
|
|
17
|
+
"mkv": "opus",
|
|
18
|
+
"webm": "opus",
|
|
19
|
+
"avi": "mp3",
|
|
20
|
+
"flv": "aac",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
SELECT_IDS = {
|
|
24
|
+
"codec": "audio_codec_select",
|
|
25
|
+
"container": "video_container_select",
|
|
26
|
+
"kbps": "kbps_select",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_version() -> str:
|
|
31
|
+
"""Get version from installed package metadata."""
|
|
32
|
+
try:
|
|
33
|
+
from importlib.metadata import version
|
|
34
|
+
|
|
35
|
+
return version("Siphon-TUI")
|
|
36
|
+
except Exception:
|
|
37
|
+
return "unknown"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SiphonTUI(App):
|
|
41
|
+
CSS_PATH = "style.tcss"
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.theme = "rose-pine"
|
|
46
|
+
self.codec = None
|
|
47
|
+
self.container = None
|
|
48
|
+
self.kbps = 256
|
|
49
|
+
|
|
50
|
+
path, error_msg = get_path()
|
|
51
|
+
self.download_path = path
|
|
52
|
+
|
|
53
|
+
self.downloading = False
|
|
54
|
+
self.cancelled = False
|
|
55
|
+
self._path_error = error_msg
|
|
56
|
+
|
|
57
|
+
def compose(self) -> ComposeResult:
|
|
58
|
+
yield Header()
|
|
59
|
+
with Container(id="main_container"):
|
|
60
|
+
yield Input(id="url_input", placeholder="Enter your URL", type="text")
|
|
61
|
+
with Vertical(id="select_section"):
|
|
62
|
+
with Horizontal(id="codecs_row"):
|
|
63
|
+
yield Select(
|
|
64
|
+
((c, c.lower()) for c in AUDIO_CODECS),
|
|
65
|
+
id=SELECT_IDS["codec"],
|
|
66
|
+
prompt="Audio codec",
|
|
67
|
+
)
|
|
68
|
+
yield Select(
|
|
69
|
+
((c, c.lower()) for c in VIDEO_CONTAINERS),
|
|
70
|
+
id=SELECT_IDS["container"],
|
|
71
|
+
prompt="Container (optional)",
|
|
72
|
+
)
|
|
73
|
+
yield Select(
|
|
74
|
+
((k, k) for k in LINES_KBPS),
|
|
75
|
+
id=SELECT_IDS["kbps"],
|
|
76
|
+
prompt="Bitrate (kbps)",
|
|
77
|
+
)
|
|
78
|
+
with Horizontal(id="button_row"):
|
|
79
|
+
yield Button("Download", variant="success", id="accept_button")
|
|
80
|
+
yield Button(
|
|
81
|
+
"Cancel", variant="error", id="cancel_button", disabled=True
|
|
82
|
+
)
|
|
83
|
+
yield Footer()
|
|
84
|
+
|
|
85
|
+
def on_mount(self) -> None:
|
|
86
|
+
self.title = "Siphon-TUI"
|
|
87
|
+
self.sub_title = f"v{get_version()}"
|
|
88
|
+
|
|
89
|
+
if self._path_error:
|
|
90
|
+
self.notify(
|
|
91
|
+
f"⚠️ {self._path_error}\nUsing: {self.download_path}",
|
|
92
|
+
severity="warning",
|
|
93
|
+
timeout=10,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@on(Select.Changed)
|
|
97
|
+
def select_changed(self, event: Select.Changed) -> None:
|
|
98
|
+
if event.value in (Select.BLANK, Select.NULL):
|
|
99
|
+
setattr(
|
|
100
|
+
self,
|
|
101
|
+
{
|
|
102
|
+
"audio_codec_select": "codec",
|
|
103
|
+
"video_container_select": "container",
|
|
104
|
+
"kbps_select": "kbps",
|
|
105
|
+
}[event.select.id],
|
|
106
|
+
None,
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if event.select.id == SELECT_IDS["codec"]:
|
|
111
|
+
self.codec = str(event.value).lower()
|
|
112
|
+
elif event.select.id == SELECT_IDS["container"]:
|
|
113
|
+
self.container = str(event.value).lower()
|
|
114
|
+
if self.container:
|
|
115
|
+
audio_codec = VIDEO_CONTAINER_AUDIO_MAP.get(self.container)
|
|
116
|
+
if audio_codec:
|
|
117
|
+
self.codec = audio_codec
|
|
118
|
+
codec_select = self.query_one(f"#{SELECT_IDS['codec']}", Select)
|
|
119
|
+
for option in codec_select._options:
|
|
120
|
+
if (
|
|
121
|
+
option[1] not in (Select.BLANK, Select.NULL)
|
|
122
|
+
and str(option[1]).lower() == audio_codec
|
|
123
|
+
):
|
|
124
|
+
codec_select.value = option[1]
|
|
125
|
+
break
|
|
126
|
+
elif event.select.id == SELECT_IDS["kbps"]:
|
|
127
|
+
self.kbps = int(event.value)
|
|
128
|
+
|
|
129
|
+
@on(Button.Pressed, "#accept_button")
|
|
130
|
+
def action_download(self) -> None:
|
|
131
|
+
url = self.query_one("#url_input", Input).value.strip()
|
|
132
|
+
if not url:
|
|
133
|
+
return self.notify("❌ Please enter a URL", severity="warning")
|
|
134
|
+
if not url.startswith(("http://", "https://")):
|
|
135
|
+
return self.notify("❌ Invalid URL", severity="error")
|
|
136
|
+
if not self._validate_settings():
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self.cancelled = False
|
|
140
|
+
self.downloading = True
|
|
141
|
+
self.query_one("#accept_button", Button).disabled = True
|
|
142
|
+
self.query_one("#cancel_button", Button).disabled = False
|
|
143
|
+
self.query_one("#url_input", Input).disabled = True
|
|
144
|
+
|
|
145
|
+
msg = (
|
|
146
|
+
f"⬇️ Downloading {self.codec.upper()} -> {self.container.upper()} @ {self.kbps}kbps..."
|
|
147
|
+
if self.container
|
|
148
|
+
else f"⬇️ Downloading {self.codec.upper()} @ {self.kbps}kbps (audio only)..."
|
|
149
|
+
)
|
|
150
|
+
self.notify(msg)
|
|
151
|
+
|
|
152
|
+
self.download_thread = Thread(
|
|
153
|
+
target=self._start_download, args=(url,), daemon=True
|
|
154
|
+
)
|
|
155
|
+
self.download_thread.start()
|
|
156
|
+
|
|
157
|
+
@on(Button.Pressed, "#cancel_button")
|
|
158
|
+
def action_cancel(self) -> None:
|
|
159
|
+
if self.downloading:
|
|
160
|
+
self.cancelled = True
|
|
161
|
+
self.notify("Cancelling...", severity="warning")
|
|
162
|
+
else:
|
|
163
|
+
self.notify("Nothing to cancel", severity="warning")
|
|
164
|
+
self._reset_ui()
|
|
165
|
+
|
|
166
|
+
def _start_download(self, url: str) -> None:
|
|
167
|
+
from siphon_tui.utils.download import (
|
|
168
|
+
Download,
|
|
169
|
+
DownloadCancelledError,
|
|
170
|
+
DownloadError,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
downloader = Download(
|
|
175
|
+
url=url,
|
|
176
|
+
codec=self.container or self.codec,
|
|
177
|
+
kbps=self.kbps,
|
|
178
|
+
download_path=self.download_path,
|
|
179
|
+
)
|
|
180
|
+
downloader.set_cancel_check(lambda: self.cancelled)
|
|
181
|
+
downloader.download()
|
|
182
|
+
self.call_from_thread(
|
|
183
|
+
self._download_complete,
|
|
184
|
+
True,
|
|
185
|
+
f"Download completed on {self.download_path}",
|
|
186
|
+
)
|
|
187
|
+
except DownloadCancelledError:
|
|
188
|
+
self.call_from_thread(self._download_complete, False, "Download cancelled")
|
|
189
|
+
except DownloadError as e:
|
|
190
|
+
self.call_from_thread(self._download_complete, False, str(e))
|
|
191
|
+
except Exception as e:
|
|
192
|
+
self.call_from_thread(self._download_complete, False, f"Error: {e}")
|
|
193
|
+
|
|
194
|
+
def _download_complete(self, success: bool, message: str) -> None:
|
|
195
|
+
self.downloading = False
|
|
196
|
+
self.cancelled = False
|
|
197
|
+
self._reset_ui()
|
|
198
|
+
self.notify(
|
|
199
|
+
f"{'✅' if success else '❌'} {message}",
|
|
200
|
+
severity="information" if success else "error",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def _reset_ui(self) -> None:
|
|
204
|
+
self.query_one("#accept_button", Button).disabled = False
|
|
205
|
+
self.query_one("#cancel_button", Button).disabled = True
|
|
206
|
+
url_input = self.query_one("#url_input", Input)
|
|
207
|
+
url_input.disabled = False
|
|
208
|
+
url_input.value = ""
|
|
209
|
+
url_input.focus()
|
|
210
|
+
|
|
211
|
+
def _validate_settings(self) -> bool:
|
|
212
|
+
if self.container:
|
|
213
|
+
if not self.codec:
|
|
214
|
+
self.notify(
|
|
215
|
+
"❌ Failed to set audio codec for container", severity="error"
|
|
216
|
+
)
|
|
217
|
+
return False
|
|
218
|
+
elif not self.codec:
|
|
219
|
+
codec_select = self.query_one(f"#{SELECT_IDS['codec']}", Select)
|
|
220
|
+
if codec_select.value not in (Select.BLANK, Select.NULL):
|
|
221
|
+
self.codec = str(codec_select.value).lower()
|
|
222
|
+
else:
|
|
223
|
+
self.notify(
|
|
224
|
+
"❌ Please select audio codec or video container",
|
|
225
|
+
severity="warning",
|
|
226
|
+
)
|
|
227
|
+
return False
|
|
228
|
+
self.kbps = self.kbps or 256
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_tui():
|
|
233
|
+
app = SiphonTUI()
|
|
234
|
+
app.run()
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from color_kiss.utils import error, success
|
|
6
|
+
from platformdirs import user_config_dir
|
|
7
|
+
|
|
8
|
+
# Cross-platform paths:
|
|
9
|
+
# Windows: %APPDATA%/Siphon/
|
|
10
|
+
# macOS: ~/Library/Application Support/Siphon/
|
|
11
|
+
# Linux: ~/.config/Siphon/
|
|
12
|
+
CONFIG_DIR = Path(user_config_dir("Siphon"))
|
|
13
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
14
|
+
HOME_PATH = str(Path.home())
|
|
15
|
+
|
|
16
|
+
KEY_NAME = "path"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ensure_config_dir() -> None:
|
|
20
|
+
"""Create config directory if it doesn't exist."""
|
|
21
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_config() -> dict:
|
|
25
|
+
"""Load config file or return empty dict if not exists."""
|
|
26
|
+
if not CONFIG_FILE.exists():
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
31
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
32
|
+
error("Config file is corrupted. Creating new one...")
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _save_config(data: dict) -> None:
|
|
37
|
+
"""Save config data to file."""
|
|
38
|
+
_ensure_config_dir()
|
|
39
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
40
|
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def set_path(path: str) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Manage download directory storage.
|
|
46
|
+
- With path: saves to config.json
|
|
47
|
+
- Without path: displays current config
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
input_path = Path(path).expanduser().resolve()
|
|
51
|
+
if not input_path.is_dir():
|
|
52
|
+
sys.exit(error("Please enter the correct path!"))
|
|
53
|
+
|
|
54
|
+
path_str = str(input_path)
|
|
55
|
+
|
|
56
|
+
_ensure_config_dir()
|
|
57
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
58
|
+
json.dump({KEY_NAME: path_str}, f, ensure_ascii=False, indent=4)
|
|
59
|
+
|
|
60
|
+
return success(f"\nPath: {path_str}\nConfig file: {CONFIG_FILE}")
|
|
61
|
+
except PermissionError:
|
|
62
|
+
return error(f"\nPermission denied! Cannot write to {CONFIG_FILE}")
|
|
63
|
+
except OSError as e:
|
|
64
|
+
return error(f"\nError saving configuration: {e}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_path() -> tuple[str, str | None]:
|
|
68
|
+
"""
|
|
69
|
+
Read download path from config.json.
|
|
70
|
+
Returns (path, error_message).
|
|
71
|
+
If error_message is None, path is valid.
|
|
72
|
+
"""
|
|
73
|
+
if not CONFIG_FILE.exists():
|
|
74
|
+
return HOME_PATH, None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
78
|
+
config = json.load(f)
|
|
79
|
+
|
|
80
|
+
if not config or KEY_NAME not in config:
|
|
81
|
+
return HOME_PATH, "Download path not set in config"
|
|
82
|
+
|
|
83
|
+
path = config[KEY_NAME]
|
|
84
|
+
if not Path(path).is_dir():
|
|
85
|
+
return HOME_PATH, f"Download path does not exist: {path}"
|
|
86
|
+
|
|
87
|
+
return path, None
|
|
88
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
89
|
+
return HOME_PATH, "Config file is corrupted! Run: siphon-tui set /path"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync YouTube downloader with cancellation support for Rhythmer TUI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
AUDIO_CODECS = frozenset({"mp3", "aac", "flac", "m4a", "opus", "vorbis", "wav"})
|
|
11
|
+
|
|
12
|
+
VIDEO_CONTAINER_AUDIO_MAP = {
|
|
13
|
+
"mp4": "m4a",
|
|
14
|
+
"mov": "m4a",
|
|
15
|
+
"mkv": "opus",
|
|
16
|
+
"webm": "opus",
|
|
17
|
+
"avi": "mp3",
|
|
18
|
+
"flv": "aac",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DownloadError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DownloadCancelledError(DownloadError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Download:
|
|
32
|
+
"""Synchronous audio/video downloader using yt-dlp with cancellation support."""
|
|
33
|
+
|
|
34
|
+
url: str
|
|
35
|
+
codec: str
|
|
36
|
+
kbps: int
|
|
37
|
+
download_path: str
|
|
38
|
+
max_concurrent: int = 3
|
|
39
|
+
|
|
40
|
+
_cancel_callback: Optional[Callable[[], bool]] = field(default=None, repr=False)
|
|
41
|
+
_cancelled: bool = field(default=False, init=False, repr=False)
|
|
42
|
+
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
"""Validate inputs on instantiation."""
|
|
45
|
+
self.codec = self.codec.lower()
|
|
46
|
+
self.is_audio = self.codec in AUDIO_CODECS
|
|
47
|
+
|
|
48
|
+
if shutil.which("ffmpeg") is None:
|
|
49
|
+
raise DownloadError(
|
|
50
|
+
"FFmpeg not found in PATH! Please install FFmpeg first."
|
|
51
|
+
)
|
|
52
|
+
if not Path(self.download_path).exists():
|
|
53
|
+
raise DownloadError(f"Download path does not exist: {self.download_path}")
|
|
54
|
+
|
|
55
|
+
def set_cancel_check(self, callback: Callable[[], bool]) -> None:
|
|
56
|
+
"""Set a callback that returns True if download should be cancelled."""
|
|
57
|
+
self._cancel_callback = callback
|
|
58
|
+
|
|
59
|
+
def cancel(self) -> None:
|
|
60
|
+
"""Mark the download as cancelled."""
|
|
61
|
+
self._cancelled = True
|
|
62
|
+
|
|
63
|
+
def _check_cancelled(self) -> bool:
|
|
64
|
+
"""Check if download has been cancelled via callback or flag."""
|
|
65
|
+
if self._cancelled:
|
|
66
|
+
return True
|
|
67
|
+
if self._cancel_callback and self._cancel_callback():
|
|
68
|
+
self._cancelled = True
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def _get_opts(self) -> dict:
|
|
73
|
+
"""Build yt-dlp options dict based on codec type (audio or video)."""
|
|
74
|
+
base_opts = {
|
|
75
|
+
"quiet": True,
|
|
76
|
+
"no_warnings": True,
|
|
77
|
+
"nooverwrites": True,
|
|
78
|
+
"outtmpl": str(Path(self.download_path) / "%(title)s.%(ext)s"),
|
|
79
|
+
"concurrent_fragment_downloads": self.max_concurrent,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if self.is_audio:
|
|
83
|
+
base_opts["format"] = "bestaudio/best"
|
|
84
|
+
base_opts["postprocessors"] = [
|
|
85
|
+
{
|
|
86
|
+
"key": "FFmpegExtractAudio",
|
|
87
|
+
"preferredcodec": self.codec,
|
|
88
|
+
"preferredquality": str(self.kbps),
|
|
89
|
+
},
|
|
90
|
+
{"key": "FFmpegMetadata"},
|
|
91
|
+
{"key": "EmbedThumbnail"},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
if self.codec == "wav":
|
|
95
|
+
base_opts["postprocessors"] = [
|
|
96
|
+
p
|
|
97
|
+
for p in base_opts["postprocessors"]
|
|
98
|
+
if p["key"] not in ["FFmpegMetadata", "EmbedThumbnail"]
|
|
99
|
+
]
|
|
100
|
+
elif self.codec in {"m4a", "aac"}:
|
|
101
|
+
base_opts["format"] = (
|
|
102
|
+
"bestaudio[ext=m4a]/bestaudio[ext=aac]/bestaudio/best"
|
|
103
|
+
)
|
|
104
|
+
base_opts["postprocessors"] = [
|
|
105
|
+
p
|
|
106
|
+
for p in base_opts["postprocessors"]
|
|
107
|
+
if p["key"] != "FFmpegExtractAudio"
|
|
108
|
+
]
|
|
109
|
+
else:
|
|
110
|
+
audio_ext = VIDEO_CONTAINER_AUDIO_MAP.get(self.codec, "m4a")
|
|
111
|
+
format_str = (
|
|
112
|
+
f"bestvideo[ext=mp4]+bestaudio[ext={audio_ext}]/bestvideo+bestaudio/best"
|
|
113
|
+
if self.codec == "mp4"
|
|
114
|
+
else f"bestvideo+bestaudio[ext={audio_ext}]/bestvideo+bestaudio/best"
|
|
115
|
+
)
|
|
116
|
+
base_opts.update(format=format_str, merge_output_format=self.codec)
|
|
117
|
+
|
|
118
|
+
return base_opts
|
|
119
|
+
|
|
120
|
+
def download(self) -> None:
|
|
121
|
+
"""Download a single URL synchronously. Raises DownloadError or DownloadCancelledError."""
|
|
122
|
+
if self._check_cancelled():
|
|
123
|
+
raise DownloadCancelledError("Download was cancelled before starting")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
from yt_dlp import YoutubeDL
|
|
127
|
+
|
|
128
|
+
with YoutubeDL(self._get_opts()) as ydl:
|
|
129
|
+
ydl.download([self.url])
|
|
130
|
+
except Exception as e:
|
|
131
|
+
if self._cancelled:
|
|
132
|
+
raise DownloadCancelledError("Download was cancelled") from e
|
|
133
|
+
raise DownloadError(f"Download failed: {e}") from e
|