├── static
├── favicon.png
├── css
│ ├── vaporwave.css
│ ├── matrix.css
│ ├── carbonation.css
│ ├── kelethin.css
│ ├── music.css
│ ├── fun.css
│ └── starry-night.css
└── js
│ └── app.js
├── assets
├── themeshots
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ └── 7.png
└── carbonatedWaterOrg-logo-1.jpeg
├── requirements.txt
├── .dockerignore
├── docker-compose.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
└── app
├── main.py
└── options.py
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/static/favicon.png
--------------------------------------------------------------------------------
/assets/themeshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/themeshots/1.png
--------------------------------------------------------------------------------
/assets/themeshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/themeshots/2.png
--------------------------------------------------------------------------------
/assets/themeshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/themeshots/3.png
--------------------------------------------------------------------------------
/assets/themeshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/themeshots/4.png
--------------------------------------------------------------------------------
/assets/themeshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/themeshots/5.png
--------------------------------------------------------------------------------
/assets/themeshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/themeshots/6.png
--------------------------------------------------------------------------------
/assets/themeshots/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/themeshots/7.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi==0.116.1
2 | uvicorn==0.35.0
3 | jinja2==3.1.6
4 | yt-dlp
5 | websockets>=12.0
6 | python-multipart
--------------------------------------------------------------------------------
/assets/carbonatedWaterOrg-logo-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carbonatedWaterOrg/yt-dlp-co2/HEAD/assets/carbonatedWaterOrg-logo-1.jpeg
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Documentation and assets
2 | docs/
3 | assets/
4 |
5 | # Git files
6 | .git/
7 | .gitignore
8 |
9 | # IDE files
10 | .vscode/
11 | .idea/
12 | *.swp
13 | *.swo
14 |
15 | # OS files
16 | .DS_Store
17 | Thumbs.db
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | yt-dlp-co2:
3 | build: .
4 | ports: # Comment out this and the line below if using
5 | - "8000:8000" # a reverse proxy on the same Docker host
6 | volumes:
7 | - ./downloads:/app/downloads
8 | environment:
9 | - PYTHONUNBUFFERED=1
10 | restart: unless-stopped
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | env/
8 | venv/
9 | ENV/
10 | env.bak/
11 | venv.bak/
12 |
13 | # IDE
14 | .vscode/
15 | .idea/
16 | *.swp
17 | *.swo
18 | *~
19 |
20 | # OS
21 | .DS_Store
22 | .DS_Store?
23 | ._*
24 | .Spotlight-V100
25 | .Trashes
26 | ehthumbs.db
27 | Thumbs.db
28 |
29 | # Project specific
30 | downloads/*
31 | *.log
32 | .env
33 | .env.local
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.13.7-alpine
2 |
3 | WORKDIR /app
4 |
5 | # Install system dependencies for yt-dlp
6 | RUN apk add --no-cache \
7 | ffmpeg \
8 | ca-certificates
9 |
10 | # Copy requirements first for better caching
11 | COPY requirements.txt .
12 |
13 | # Install Python dependencies
14 | RUN pip install --no-cache-dir -r requirements.txt
15 |
16 | # Copy application code
17 | COPY app/ ./app/
18 | COPY index.html ./
19 | COPY static/ ./static/
20 | COPY README.md ./README.md
21 |
22 | # Create directories
23 | RUN mkdir -p /app/downloads /app/config
24 |
25 | # Non-root user for security
26 | RUN addgroup -g 1000 appuser && \
27 | adduser -u 1000 -G appuser -D appuser && \
28 | chown -R appuser:appuser /app
29 |
30 | USER appuser
31 |
32 | EXPOSE 8000
33 |
34 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 CarbonatedWater.org
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # yt-dlp-co₂ from carbonatedWater.org
7 |
8 | It's a modern web interface for yt-dlp with real-time progress tracking and multiple pretty cool themes, from the DEFINITIVE, OFFICIAL, WORLDWIDE provider of Carbonated Water rankings, carbonatedWater.org.
9 |
10 |
11 | ## Quick Start
12 | ```bash
13 | # Navigate to the directory that you want the downloads folder placed at.
14 | docker run -d -p 8000:8000 -v ./downloads:/app/downloads carbonatedwaterorg/yt-dlp-co2
15 | sudo chown -R $USER:$USER ./downloads
16 | # Open http://localhost:8000 and begin downloading
17 | ```
18 |
19 | ## Features
20 |
21 | - **It uses yt-dlp** which is really great
22 | - **It has all the options** that yt-dlp has pretty much
23 | - **Real-time download progress** via WebSocket
24 | - **7 stunning visual themes** (Carbonation, Vaporwave, Matrix, Music, Fun, Starry Night, Kelethin)
25 | - **Format selection** with quality preview
26 | - **150+ comprehensive options** - Every yt-dlp CLI option accessible through intuitive web interface
27 | - **Advanced post-processing** - Audio extraction, subtitle embedding, thumbnail handling
28 | - **Keeping it Python** pretty much for all of it
29 |
30 | ## Themes
31 |
32 | It has a few good ones.
33 |
34 |
35 |
36 | | 🫧 **Default** | 🌴 **Vaporwave** | 🕶️ **Matrix** | 🎵 **Music** |
37 | |:---:|:---:|:---:|:---:|
38 | |
|
|
|
|
39 | | *It's carbonated water* | *This word was written on something* | *Not like the movie* | *It feels like music* |
40 |
41 | | 🎉 **Fun** | 🌌 **Starry Night** | 🧝 **Kelethin** |
42 | |:---:|:---:|:---:|
43 | |
|
|
|
44 | | *How it feels* | *How it sounds* | *WTB Fungi and FBSS* |
45 |
46 |
47 |
48 | ## Building and Using
49 |
50 | ```bash
51 | # Clone the repository to your current directory
52 | git clone https://github.com/carbonatedWaterOrg/yt-dlp-co2.git .
53 |
54 | # Docker Compose (recommended)
55 | docker-compose up -d
56 | sudo chown -R $USER:$USER ./downloads
57 |
58 | # Docker
59 | docker build -t yt-dlp-co2 .
60 | docker run -d -p 8000:8000 -v ./downloads:/app/downloads yt-dlp-co2
61 | sudo chown -R $USER:$USER ./downloads
62 |
63 | # Local development
64 | pip install -r requirements.txt
65 | uvicorn app.main:app --reload --port 8000
66 | ```
67 |
68 | Access at http://localhost:8000
69 |
70 | ## Architecture
71 |
72 | - **Backend**: FastAPI + yt-dlp library integration
73 | - **Frontend**: HTMX + Alpine.js + TailwindCSS
74 | - **Container**: Python 3.13 Alpine
75 | - **Downloads**: Mounted to `./downloads/`
76 |
77 | ## Usage
78 |
79 | 1. Paste video URL
80 | 2. Click "Formats" to see available quality options
81 | 3. Click "Options" to access 150+ advanced yt-dlp settings
82 | 4. Configure downloads with playlist controls, subtitle options, audio extraction, and more
83 | 5. Select format or click "Download" for best quality
84 | 6. Monitor progress in real-time
85 | 7. Switch themes using header buttons
86 |
87 | **Recommendations**: We put this behind a reverse proxy on the same Docker host. We like Caddy.
88 |
89 | ### Advanced Options
90 |
91 | Access every yt-dlp feature through organized categories:
92 | - **Video Selection**: Playlist items, date filters, view count limits
93 | - **Post-Processing**: Audio extraction, format conversion, subtitle embedding
94 | - **Download Control**: Retry settings, rate limiting, concurrent fragments
95 | - **Authentication**: Login credentials for private content
96 | - **SponsorBlock**: Skip sponsor segments automatically
97 | - **And much more**: Network settings, geo-bypass, filesystem options
98 |
99 | **Use responsibly**: Only download content you have permission to access offline, such as your own uploads, Creative Commons content, or from platforms that explicitly allow downloads.
100 |
101 | [GitHub Repo](https://github.com/carbonatedWaterOrg/yt-dlp-co2)
102 |
103 | ## Built With
104 |
105 | [Python 3.13](https://www.python.org/) • [FastAPI](https://fastapi.tiangolo.com/) • [Uvicorn](https://www.uvicorn.org/) • [yt-dlp](https://github.com/yt-dlp/yt-dlp) • [HTMX](https://htmx.org/) • [Alpine.js](https://alpinejs.dev/) • [Tailwind CSS](https://tailwindcss.com/) • [Docker](https://www.docker.com/)
--------------------------------------------------------------------------------
/static/css/vaporwave.css:
--------------------------------------------------------------------------------
1 | /* Vaporwave Theme for yt-dlp-co2 */
2 | @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
3 |
4 | :root {
5 | --neon-pink: #ff00ff;
6 | --neon-cyan: #00ffff;
7 | --neon-purple: #8a2be2;
8 | --dark-bg: #1a0033;
9 | --card-bg: rgba(30, 0, 60, 0.8);
10 | --grid-color: rgba(0, 255, 255, 0.3);
11 | }
12 |
13 | body {
14 | background: linear-gradient(135deg, #1a0033 0%, #330066 50%, #0f001f 100%);
15 | color: var(--neon-cyan);
16 | font-family: 'Orbitron', monospace;
17 | min-height: 100vh;
18 | position: relative;
19 | overflow-x: hidden;
20 | }
21 |
22 | /* Animated grid background */
23 | body::before {
24 | content: '';
25 | position: fixed;
26 | top: 0;
27 | left: 0;
28 | width: 100%;
29 | height: 100%;
30 | background-image:
31 | linear-gradient(rgba(0, 255, 255, 0.1) 1px, transparent 1px),
32 | linear-gradient(90deg, rgba(0, 255, 255, 0.1) 1px, transparent 1px);
33 | background-size: 50px 50px;
34 | animation: grid-move 20s linear infinite;
35 | z-index: -1;
36 | }
37 |
38 | @keyframes grid-move {
39 | 0% { transform: translate(0, 0); }
40 | 100% { transform: translate(50px, 50px); }
41 | }
42 |
43 | /* Neon text effects */
44 | h1 {
45 | color: var(--neon-pink);
46 | text-shadow:
47 | 0 0 5px var(--neon-pink),
48 | 0 0 10px var(--neon-pink),
49 | 0 0 15px var(--neon-pink),
50 | 0 0 20px var(--neon-pink);
51 | font-weight: 900;
52 | text-transform: uppercase;
53 | letter-spacing: 3px;
54 | }
55 |
56 | .tagline {
57 | color: var(--neon-cyan);
58 | text-shadow:
59 | 0 0 5px var(--neon-cyan),
60 | 0 0 10px var(--neon-cyan);
61 | font-style: italic;
62 | }
63 |
64 | /* Card styling */
65 | .card {
66 | background: var(--card-bg);
67 | border: 2px solid var(--neon-cyan);
68 | border-radius: 15px;
69 | box-shadow:
70 | 0 0 20px rgba(0, 255, 255, 0.5),
71 | inset 0 0 20px rgba(0, 255, 255, 0.1);
72 | backdrop-filter: blur(10px);
73 | }
74 |
75 | /* Button styling */
76 | .btn-neon {
77 | background: linear-gradient(45deg, var(--neon-pink), var(--neon-purple));
78 | border: 2px solid var(--neon-pink);
79 | color: white;
80 | padding: 12px 24px;
81 | border-radius: 25px;
82 | font-weight: bold;
83 | text-transform: uppercase;
84 | letter-spacing: 1px;
85 | cursor: pointer;
86 | transition: all 0.3s ease;
87 | box-shadow:
88 | 0 0 20px rgba(255, 0, 255, 0.5),
89 | inset 0 0 20px rgba(255, 0, 255, 0.1);
90 | }
91 |
92 | .btn-neon:hover:not(:disabled) {
93 | transform: translateY(-2px);
94 | box-shadow:
95 | 0 5px 30px rgba(255, 0, 255, 0.7),
96 | inset 0 0 30px rgba(255, 0, 255, 0.2);
97 | text-shadow: 0 0 10px white;
98 | }
99 |
100 | .btn-neon:disabled {
101 | opacity: 0.4;
102 | cursor: not-allowed;
103 | transform: none;
104 | filter: grayscale(50%);
105 | }
106 |
107 | /* Input styling */
108 | .input-neon {
109 | background: rgba(0, 0, 0, 0.5);
110 | border: 2px solid var(--neon-cyan);
111 | border-radius: 10px;
112 | color: var(--neon-cyan);
113 | padding: 12px 16px;
114 | font-family: 'Orbitron', monospace;
115 | box-shadow:
116 | 0 0 15px rgba(0, 255, 255, 0.3),
117 | inset 0 0 15px rgba(0, 255, 255, 0.1);
118 | }
119 |
120 | .input-neon:focus {
121 | outline: none;
122 | border-color: var(--neon-pink);
123 | box-shadow:
124 | 0 0 25px rgba(255, 0, 255, 0.5),
125 | inset 0 0 25px rgba(255, 0, 255, 0.1);
126 | }
127 |
128 | .input-neon::placeholder {
129 | color: rgba(0, 255, 255, 0.6);
130 | }
131 |
132 | /* Progress bar */
133 | .progress-neon {
134 | background: rgba(0, 0, 0, 0.5);
135 | border: 1px solid var(--neon-cyan);
136 | border-radius: 10px;
137 | height: 20px;
138 | overflow: hidden;
139 | position: relative;
140 | }
141 |
142 | .progress-neon::before {
143 | content: '';
144 | position: absolute;
145 | top: 0;
146 | left: 0;
147 | height: 100%;
148 | background: linear-gradient(90deg, var(--neon-cyan), var(--neon-pink));
149 | width: var(--progress, 0%);
150 | transition: width 0.3s ease;
151 | box-shadow: 0 0 10px rgba(0, 255, 255, 0.8);
152 | }
153 |
154 | /* Success/Error states */
155 | .success-neon {
156 | border-color: #00ff00;
157 | background: rgba(0, 255, 0, 0.1);
158 | box-shadow: 0 0 15px rgba(0, 255, 0, 0.5);
159 | }
160 |
161 | .error-neon {
162 | border-color: #ff0000;
163 | background: rgba(255, 0, 0, 0.1);
164 | box-shadow: 0 0 15px rgba(255, 0, 0, 0.5);
165 | }
166 |
167 | /* Format selection */
168 | .format-option {
169 | background: rgba(0, 0, 0, 0.3);
170 | border: 1px solid var(--neon-cyan);
171 | border-radius: 8px;
172 | padding: 8px 12px;
173 | margin: 4px;
174 | cursor: pointer;
175 | transition: all 0.3s ease;
176 | }
177 |
178 | .format-option:hover {
179 | transform: translateY(-1px);
180 | }
181 |
182 | .format-option:hover,
183 | .format-option.selected {
184 | border-color: var(--neon-pink);
185 | background: rgba(255, 0, 255, 0.2);
186 | box-shadow: 0 0 10px rgba(255, 0, 255, 0.5);
187 | }
188 |
189 | /* Scrollbar styling */
190 | ::-webkit-scrollbar {
191 | width: 12px;
192 | }
193 |
194 | ::-webkit-scrollbar-track {
195 | background: var(--dark-bg);
196 | }
197 |
198 | ::-webkit-scrollbar-thumb {
199 | background: linear-gradient(45deg, var(--neon-pink), var(--neon-cyan));
200 | border-radius: 6px;
201 | box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
202 | }
203 |
204 | /* Responsive Button Layouts */
205 | .theme-buttons-container {
206 | position: relative;
207 | z-index: 1000;
208 | }
209 |
210 | .action-buttons-container {
211 | width: 100%;
212 | }
213 |
214 | .action-buttons-container .btn-neon {
215 | flex: 1;
216 | min-width: 0;
217 | text-align: center;
218 | }
219 |
220 | .download-primary {
221 | min-width: 100px;
222 | flex-shrink: 0;
223 | }
224 |
225 | /* Header spacing adjustments for mobile */
226 | .header-title-section {
227 | margin-top: 1rem;
228 | }
229 |
230 | /* Responsive adjustments */
231 | @media (max-width: 768px) {
232 | body::before {
233 | background-size: 30px 30px;
234 | }
235 |
236 | h1 {
237 | font-size: 2rem;
238 | letter-spacing: 2px;
239 | }
240 |
241 | .btn-neon {
242 | padding: 8px 16px;
243 | font-size: 0.85rem;
244 | letter-spacing: 0.5px;
245 | }
246 |
247 | .theme-option {
248 | min-width: 60px;
249 | padding: 8px 12px;
250 | font-size: 0.7rem;
251 | }
252 |
253 | .theme-buttons-container {
254 | margin-bottom: 1rem;
255 | }
256 |
257 | .header-title-section {
258 | margin-top: 0;
259 | }
260 |
261 | .action-buttons-container {
262 | grid-template-columns: 1fr 1fr;
263 | gap: 0.5rem;
264 | }
265 | }
266 |
267 | @media (max-width: 480px) {
268 | .theme-option {
269 | min-width: 50px;
270 | padding: 6px 8px;
271 | font-size: 0.65rem;
272 | }
273 |
274 | .btn-neon {
275 | padding: 6px 12px;
276 | font-size: 0.8rem;
277 | letter-spacing: 0px;
278 | }
279 |
280 | .download-primary {
281 | min-width: 80px;
282 | }
283 | }
284 |
285 | /* Animation for loading states */
286 | @keyframes pulse-neon {
287 | 0%, 100% { opacity: 1; }
288 | 50% { opacity: 0.5; }
289 | }
290 |
291 | .loading {
292 | animation: pulse-neon 1.5s ease-in-out infinite;
293 | }
294 |
295 | /* Close button styling */
296 | .card:hover .opacity-0 {
297 | opacity: 1;
298 | }
299 |
300 | .close-btn {
301 | background: rgba(0, 0, 0, 0.5);
302 | border-radius: 50%;
303 | padding: 4px;
304 | transition: all 0.3s ease;
305 | }
306 |
307 | .close-btn:hover {
308 | background: rgba(255, 0, 255, 0.3);
309 | box-shadow: 0 0 10px rgba(255, 0, 255, 0.5);
310 | }
311 |
312 | /* Theme option buttons */
313 | .theme-option {
314 | background: rgba(0, 0, 0, 0.7);
315 | border: 2px solid var(--neon-cyan);
316 | border-radius: 8px;
317 | color: var(--neon-cyan);
318 | transition: all 0.3s ease;
319 | cursor: pointer;
320 | min-width: 70px;
321 | text-align: center;
322 | position: relative;
323 | z-index: 1000;
324 | }
325 |
326 | .theme-option:hover {
327 | background: rgba(255, 0, 255, 0.2);
328 | box-shadow: 0 0 15px rgba(0, 255, 255, 0.5);
329 | text-shadow: 0 0 5px var(--neon-cyan);
330 | transform: translateY(-1px);
331 | }
332 |
333 | .theme-option.active {
334 | background: rgba(255, 0, 255, 0.3);
335 | box-shadow: 0 0 20px rgba(255, 0, 255, 0.7);
336 | text-shadow: 0 0 8px var(--neon-pink);
337 | border-color: var(--neon-pink);
338 | }
339 |
340 | /* Footer styling */
341 | footer {
342 | border-top: 2px solid var(--neon-cyan);
343 | background: rgba(0, 0, 0, 0.3);
344 | box-shadow: 0 -5px 15px rgba(0, 255, 255, 0.3);
345 | }
346 |
347 | footer p {
348 | color: var(--neon-cyan);
349 | text-shadow: 0 0 5px var(--neon-cyan);
350 | }
--------------------------------------------------------------------------------
/static/css/matrix.css:
--------------------------------------------------------------------------------
1 | /* Matrix Theme for yt-dlp-co2 */
2 | @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono:wght@400&display=swap');
3 |
4 | :root {
5 | --matrix-green: #00ff41;
6 | --matrix-dark-green: #008f11;
7 | --matrix-bg: #0d1117;
8 | --matrix-card-bg: rgba(13, 17, 23, 0.9);
9 | --matrix-border: #00ff41;
10 | --matrix-text: #00ff41;
11 | --matrix-accent: #39ff14;
12 | }
13 |
14 | body {
15 | background: #000000;
16 | color: var(--matrix-text);
17 | font-family: 'Share Tech Mono', monospace;
18 | min-height: 100vh;
19 | position: relative;
20 | overflow-x: hidden;
21 | }
22 |
23 | /* Matrix digital rain background */
24 | body::before {
25 | content: '';
26 | position: fixed;
27 | top: 0;
28 | left: 0;
29 | width: 100%;
30 | height: 100%;
31 | background:
32 | radial-gradient(ellipse at center, transparent 0%, rgba(0, 0, 0, 0.8) 70%),
33 | repeating-linear-gradient(
34 | 0deg,
35 | transparent 0px,
36 | rgba(0, 255, 65, 0.03) 1px,
37 | transparent 2px,
38 | transparent 40px
39 | ),
40 | repeating-linear-gradient(
41 | 90deg,
42 | transparent 0px,
43 | rgba(0, 255, 65, 0.03) 1px,
44 | transparent 2px,
45 | transparent 40px
46 | );
47 | animation: matrix-rain 15s linear infinite;
48 | z-index: -2;
49 | }
50 |
51 | /* Matrix rain container */
52 | #matrix-rain {
53 | position: fixed;
54 | top: 0;
55 | left: 0;
56 | width: 100%;
57 | height: 100%;
58 | z-index: -1;
59 | pointer-events: none;
60 | overflow: hidden;
61 | }
62 |
63 | .matrix-column {
64 | position: absolute;
65 | top: -100px;
66 | font-family: 'Share Tech Mono', monospace;
67 | font-size: 14px;
68 | line-height: 20px;
69 | color: rgba(0, 255, 65, 0.15);
70 | text-shadow: 0 0 2px rgba(0, 255, 65, 0.1);
71 | animation: matrix-fall linear infinite;
72 | }
73 |
74 | .matrix-column .char {
75 | display: block;
76 | opacity: 0.8;
77 | transition: opacity 0.1s;
78 | }
79 |
80 | .matrix-column .char.fade {
81 | opacity: 0.3;
82 | }
83 |
84 | @keyframes matrix-rain {
85 | 0% { transform: translateY(-50px); }
86 | 100% { transform: translateY(50px); }
87 | }
88 |
89 | @keyframes matrix-fall {
90 | 0% { transform: translateY(-100px); }
91 | 100% { transform: translateY(calc(100vh + 100px)); }
92 | }
93 |
94 | /* Matrix text effects */
95 | h1 {
96 | color: var(--matrix-green);
97 | text-shadow:
98 | 0 0 5px var(--matrix-green),
99 | 0 0 10px var(--matrix-green),
100 | 0 0 15px var(--matrix-green);
101 | font-weight: 400;
102 | text-transform: uppercase;
103 | letter-spacing: 4px;
104 | font-family: 'Share Tech Mono', monospace;
105 | }
106 |
107 | .tagline {
108 | color: var(--matrix-accent);
109 | text-shadow:
110 | 0 0 3px var(--matrix-accent),
111 | 0 0 6px var(--matrix-accent);
112 | font-style: normal;
113 | }
114 |
115 | /* Card styling */
116 | .card {
117 | background: var(--matrix-card-bg);
118 | border: 1px solid var(--matrix-border);
119 | border-radius: 4px;
120 | box-shadow:
121 | 0 0 10px rgba(0, 255, 65, 0.3),
122 | inset 0 0 10px rgba(0, 255, 65, 0.05);
123 | backdrop-filter: blur(5px);
124 | }
125 |
126 | /* Button styling */
127 | .btn-neon {
128 | background: linear-gradient(45deg, #000000, #001100);
129 | border: 1px solid var(--matrix-green);
130 | color: var(--matrix-green);
131 | padding: 12px 24px;
132 | border-radius: 4px;
133 | font-weight: 400;
134 | text-transform: uppercase;
135 | letter-spacing: 2px;
136 | cursor: pointer;
137 | transition: all 0.3s ease;
138 | box-shadow:
139 | 0 0 10px rgba(0, 255, 65, 0.3),
140 | inset 0 0 10px rgba(0, 255, 65, 0.05);
141 | font-family: 'Share Tech Mono', monospace;
142 | }
143 |
144 | .btn-neon:hover:not(:disabled) {
145 | background: linear-gradient(45deg, #001100, #002200);
146 | box-shadow:
147 | 0 0 20px rgba(0, 255, 65, 0.6),
148 | inset 0 0 20px rgba(0, 255, 65, 0.1);
149 | text-shadow: 0 0 5px var(--matrix-green);
150 | transform: translateY(-1px);
151 | }
152 |
153 | .btn-neon:disabled {
154 | opacity: 0.3;
155 | cursor: not-allowed;
156 | transform: none;
157 | filter: brightness(0.5);
158 | }
159 |
160 | /* Input styling */
161 | .input-neon {
162 | background: rgba(0, 0, 0, 0.8);
163 | border: 1px solid var(--matrix-green);
164 | border-radius: 4px;
165 | color: var(--matrix-green);
166 | padding: 12px 16px;
167 | font-family: 'Share Tech Mono', monospace;
168 | box-shadow:
169 | 0 0 8px rgba(0, 255, 65, 0.2),
170 | inset 0 0 8px rgba(0, 255, 65, 0.05);
171 | }
172 |
173 | .input-neon:focus {
174 | outline: none;
175 | border-color: var(--matrix-accent);
176 | box-shadow:
177 | 0 0 15px rgba(57, 255, 20, 0.4),
178 | inset 0 0 15px rgba(57, 255, 20, 0.1);
179 | background: rgba(0, 17, 0, 0.8);
180 | }
181 |
182 | .input-neon::placeholder {
183 | color: rgba(0, 255, 65, 0.5);
184 | }
185 |
186 | /* Progress bar */
187 | .progress-neon {
188 | background: rgba(0, 0, 0, 0.8);
189 | border: 1px solid var(--matrix-green);
190 | border-radius: 4px;
191 | height: 20px;
192 | overflow: hidden;
193 | position: relative;
194 | }
195 |
196 | .progress-neon::before {
197 | content: '';
198 | position: absolute;
199 | top: 0;
200 | left: 0;
201 | height: 100%;
202 | background: linear-gradient(90deg, var(--matrix-dark-green), var(--matrix-green));
203 | width: var(--progress, 0%);
204 | transition: width 0.3s ease;
205 | box-shadow: 0 0 10px rgba(0, 255, 65, 0.6);
206 | }
207 |
208 | /* Success/Error states */
209 | .success-neon {
210 | border-color: var(--matrix-accent);
211 | background: rgba(57, 255, 20, 0.1);
212 | box-shadow: 0 0 15px rgba(57, 255, 20, 0.3);
213 | }
214 |
215 | .error-neon {
216 | border-color: #ff0040;
217 | background: rgba(255, 0, 64, 0.1);
218 | box-shadow: 0 0 15px rgba(255, 0, 64, 0.3);
219 | }
220 |
221 | /* Format selection */
222 | .format-option {
223 | background: rgba(0, 0, 0, 0.6);
224 | border: 1px solid var(--matrix-dark-green);
225 | border-radius: 4px;
226 | padding: 8px 12px;
227 | margin: 4px;
228 | cursor: pointer;
229 | transition: all 0.3s ease;
230 | color: var(--matrix-text);
231 | }
232 |
233 | .format-option:hover {
234 | transform: translateY(-1px);
235 | border-color: var(--matrix-green);
236 | background: rgba(0, 17, 0, 0.8);
237 | }
238 |
239 | .format-option:hover,
240 | .format-option.selected {
241 | border-color: var(--matrix-accent);
242 | background: rgba(57, 255, 20, 0.1);
243 | box-shadow: 0 0 8px rgba(57, 255, 20, 0.3);
244 | }
245 |
246 | /* Theme option buttons */
247 | .theme-option {
248 | background: rgba(0, 0, 0, 0.8);
249 | border: 1px solid var(--matrix-green);
250 | border-radius: 4px;
251 | color: var(--matrix-green);
252 | transition: all 0.3s ease;
253 | cursor: pointer;
254 | min-width: 70px;
255 | text-align: center;
256 | font-family: 'Share Tech Mono', monospace;
257 | font-weight: 600;
258 | position: relative;
259 | z-index: 1000;
260 | }
261 |
262 | .theme-option:hover {
263 | background: rgba(0, 17, 0, 0.9);
264 | box-shadow: 0 0 10px rgba(0, 255, 65, 0.4);
265 | text-shadow: 0 0 5px var(--matrix-green);
266 | }
267 |
268 | .theme-option.active {
269 | background: rgba(0, 17, 0, 0.9);
270 | box-shadow: 0 0 15px rgba(57, 255, 20, 0.6);
271 | text-shadow: 0 0 8px var(--matrix-accent);
272 | border-color: var(--matrix-accent);
273 | }
274 |
275 | /* Scrollbar styling */
276 | ::-webkit-scrollbar {
277 | width: 12px;
278 | }
279 |
280 | ::-webkit-scrollbar-track {
281 | background: #000000;
282 | }
283 |
284 | ::-webkit-scrollbar-thumb {
285 | background: linear-gradient(45deg, var(--matrix-dark-green), var(--matrix-green));
286 | border-radius: 2px;
287 | box-shadow: 0 0 5px rgba(0, 255, 65, 0.3);
288 | }
289 |
290 | /* Responsive Button Layouts */
291 | .theme-buttons-container {
292 | position: relative;
293 | z-index: 1000;
294 | }
295 |
296 | .action-buttons-container {
297 | width: 100%;
298 | }
299 |
300 | .action-buttons-container .btn-neon {
301 | flex: 1;
302 | min-width: 0;
303 | text-align: center;
304 | }
305 |
306 | .download-primary {
307 | min-width: 100px;
308 | flex-shrink: 0;
309 | }
310 |
311 | /* Header spacing adjustments for mobile */
312 | .header-title-section {
313 | margin-top: 1rem;
314 | }
315 |
316 | /* Responsive adjustments */
317 | @media (max-width: 768px) {
318 | body::before {
319 | background-size: 20px 20px;
320 | }
321 |
322 | h1 {
323 | font-size: 2rem;
324 | letter-spacing: 2px;
325 | }
326 |
327 | .btn-neon {
328 | padding: 8px 16px;
329 | font-size: 0.85rem;
330 | letter-spacing: 1px;
331 | }
332 |
333 | .theme-option {
334 | min-width: 60px;
335 | padding: 8px 12px;
336 | font-size: 0.7rem;
337 | letter-spacing: 1px;
338 | }
339 |
340 | .theme-buttons-container {
341 | margin-bottom: 1rem;
342 | }
343 |
344 | .header-title-section {
345 | margin-top: 0;
346 | }
347 |
348 | .action-buttons-container {
349 | grid-template-columns: 1fr 1fr;
350 | gap: 0.5rem;
351 | }
352 | }
353 |
354 | @media (max-width: 480px) {
355 | .theme-option {
356 | min-width: 50px;
357 | padding: 6px 8px;
358 | font-size: 0.65rem;
359 | letter-spacing: 0.5px;
360 | }
361 |
362 | .btn-neon {
363 | padding: 6px 12px;
364 | font-size: 0.8rem;
365 | letter-spacing: 0.5px;
366 | }
367 |
368 | .download-primary {
369 | min-width: 80px;
370 | }
371 | }
372 |
373 | /* Animation for loading states */
374 | @keyframes pulse-matrix {
375 | 0%, 100% { opacity: 1; }
376 | 50% { opacity: 0.6; }
377 | }
378 |
379 | .loading {
380 | animation: pulse-matrix 1.5s ease-in-out infinite;
381 | }
382 |
383 | /* Close button styling */
384 | .card:hover .opacity-0 {
385 | opacity: 1;
386 | }
387 |
388 | .close-btn {
389 | background: rgba(0, 0, 0, 0.8);
390 | border-radius: 4px;
391 | padding: 4px;
392 | transition: all 0.3s ease;
393 | border: 1px solid var(--matrix-dark-green);
394 | }
395 |
396 | .close-btn:hover {
397 | background: rgba(0, 17, 0, 0.9);
398 | box-shadow: 0 0 8px rgba(0, 255, 65, 0.3);
399 | border-color: var(--matrix-green);
400 | }
401 |
402 | /* Footer styling */
403 | footer {
404 | border-top: 1px solid var(--matrix-green);
405 | background: rgba(0, 0, 0, 0.8);
406 | box-shadow: 0 -5px 15px rgba(0, 255, 65, 0.2);
407 | }
408 |
409 | footer p {
410 | color: var(--matrix-text);
411 | text-shadow: 0 0 3px var(--matrix-green);
412 | }
413 |
414 | /* Matrix-specific glitch effect for errors */
415 | .error-neon {
416 | animation: matrix-glitch 0.3s ease-in-out;
417 | }
418 |
419 | @keyframes matrix-glitch {
420 | 0%, 100% { transform: translateX(0); }
421 | 20% { transform: translateX(-2px); }
422 | 40% { transform: translateX(2px); }
423 | 60% { transform: translateX(-1px); }
424 | 80% { transform: translateX(1px); }
425 | }
--------------------------------------------------------------------------------
/static/css/carbonation.css:
--------------------------------------------------------------------------------
1 | /* CARBONATION Theme for yt-dlp-co2 */
2 | /* The ultimate fizzy, bubbly, refreshing default theme! 🥤✨ */
3 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800;900&display=swap');
4 |
5 | :root {
6 | --modern-white: #ffffff;
7 | --water-bottle-blue: #e3f2fd;
8 | --pale-blue: #ecf4ff;
9 | --soft-cyan: #f3f9ff;
10 | --light-border: #d1e7dd;
11 | --accent-blue: #87ceeb;
12 | --text-primary: #1e293b;
13 | --text-secondary: #64748b;
14 | --bubble-blue: #b3d9ff;
15 | --carbonation-bg: #f5faff;
16 | --carbonation-card: rgba(255, 255, 255, 0.95);
17 | --bubble-glow: #d1e7dd;
18 | --success-green: #10b981;
19 | --error-red: #ef4444;
20 | }
21 |
22 | body {
23 | background: linear-gradient(135deg, var(--water-bottle-blue) 0%, var(--pale-blue) 50%, var(--soft-cyan) 100%);
24 | color: var(--text-primary);
25 | font-family: 'Inter', sans-serif;
26 | min-height: 100vh;
27 | position: relative;
28 | overflow-x: hidden;
29 | }
30 |
31 | /* Subtle static bubble background */
32 | body::before {
33 | content: '';
34 | position: fixed;
35 | top: 0;
36 | left: 0;
37 | width: 100%;
38 | height: 100%;
39 | background-image:
40 | /* Faint static bubbles - beautiful and simple */
41 | radial-gradient(circle 4px at 15% 25%, rgba(14, 165, 233, 0.08) 40%, transparent 41%),
42 | radial-gradient(circle 3px at 35% 40%, rgba(6, 182, 212, 0.06) 40%, transparent 41%),
43 | radial-gradient(circle 5px at 65% 30%, rgba(59, 130, 246, 0.07) 40%, transparent 41%),
44 | radial-gradient(circle 2px at 25% 60%, rgba(14, 165, 233, 0.08) 40%, transparent 41%),
45 | radial-gradient(circle 4px at 75% 55%, rgba(6, 182, 212, 0.06) 40%, transparent 41%),
46 | radial-gradient(circle 3px at 45% 70%, rgba(59, 130, 246, 0.07) 40%, transparent 41%),
47 | radial-gradient(circle 2px at 85% 20%, rgba(14, 165, 233, 0.08) 40%, transparent 41%),
48 | radial-gradient(circle 3px at 55% 15%, rgba(6, 182, 212, 0.06) 40%, transparent 41%),
49 |
50 | /* Subtle micro sparkles */
51 | radial-gradient(circle 1px at 20% 35%, rgba(255, 255, 255, 0.4) 50%, transparent 51%),
52 | radial-gradient(circle 1px at 60% 45%, rgba(255, 255, 255, 0.3) 50%, transparent 51%),
53 | radial-gradient(circle 1px at 40% 80%, rgba(255, 255, 255, 0.4) 50%, transparent 51%),
54 | radial-gradient(circle 1px at 80% 65%, rgba(255, 255, 255, 0.3) 50%, transparent 51%),
55 | radial-gradient(circle 1px at 30% 10%, rgba(255, 255, 255, 0.4) 50%, transparent 51%),
56 | radial-gradient(circle 1px at 70% 85%, rgba(255, 255, 255, 0.3) 50%, transparent 51%);
57 | z-index: -2;
58 | }
59 |
60 | /* Simple container for future use if needed */
61 | #carbonation-bubbles {
62 | position: fixed;
63 | top: 0;
64 | left: 0;
65 | width: 100%;
66 | height: 100%;
67 | z-index: -1;
68 | pointer-events: none;
69 | overflow: hidden;
70 | }
71 |
72 | /* Modern clean text effects */
73 | h1 {
74 | color: var(--text-primary);
75 | text-shadow: none;
76 | font-weight: 700;
77 | text-transform: none;
78 | letter-spacing: -0.025em;
79 | font-family: 'Inter', sans-serif;
80 | cursor: pointer;
81 | transition: all 0.2s ease;
82 | }
83 |
84 | h1:hover {
85 | color: var(--accent-blue);
86 | transform: scale(1.01);
87 | }
88 |
89 | .tagline {
90 | color: var(--text-secondary);
91 | text-shadow: none;
92 | font-style: italic;
93 | font-weight: 400;
94 | cursor: pointer;
95 | transition: all 0.2s ease;
96 | }
97 |
98 | .tagline:hover {
99 | color: var(--accent-blue);
100 | transform: scale(1.02);
101 | }
102 |
103 | /* Modern clean card styling */
104 | .card {
105 | background: var(--carbonation-card);
106 | border: 1px solid var(--light-border);
107 | border-radius: 12px;
108 | box-shadow:
109 | 0 1px 3px rgba(0, 0, 0, 0.1),
110 | 0 1px 2px rgba(0, 0, 0, 0.06);
111 | backdrop-filter: blur(8px);
112 | position: relative;
113 | transition: all 0.2s ease;
114 | }
115 |
116 | .card:hover {
117 | border-color: var(--modern-blue);
118 | box-shadow:
119 | 0 4px 6px rgba(0, 0, 0, 0.07),
120 | 0 1px 3px rgba(0, 0, 0, 0.1);
121 | }
122 |
123 | .card::before {
124 | content: none;
125 | }
126 |
127 |
128 | /* Modern clean button styling */
129 | .btn-neon {
130 | background: var(--accent-blue);
131 | border: none;
132 | color: white;
133 | padding: 12px 24px;
134 | border-radius: 8px;
135 | font-weight: 600;
136 | text-transform: none;
137 | letter-spacing: 0;
138 | cursor: pointer;
139 | transition: all 0.2s ease;
140 | box-shadow:
141 | 0 1px 3px rgba(0, 0, 0, 0.12),
142 | 0 1px 2px rgba(0, 0, 0, 0.24);
143 | font-family: 'Inter', sans-serif;
144 | position: relative;
145 | }
146 |
147 | .btn-neon:hover:not(:disabled) {
148 | background: var(--bubble-blue);
149 | transform: translateY(-1px);
150 | box-shadow:
151 | 0 2px 8px rgba(0, 0, 0, 0.15),
152 | 0 1px 4px rgba(0, 0, 0, 0.3);
153 | }
154 |
155 | .btn-neon:disabled {
156 | opacity: 0.5;
157 | cursor: not-allowed;
158 | transform: none;
159 | background: var(--text-secondary);
160 | }
161 |
162 | /* Modern clean input styling */
163 | .input-neon {
164 | background: var(--modern-white);
165 | border: 1px solid var(--light-border);
166 | border-radius: 8px;
167 | color: var(--text-primary);
168 | padding: 12px 16px;
169 | font-family: 'Inter', sans-serif;
170 | box-shadow:
171 | 0 1px 3px rgba(0, 0, 0, 0.1),
172 | 0 1px 2px rgba(0, 0, 0, 0.06);
173 | transition: all 0.2s ease;
174 | }
175 |
176 | .input-neon:focus {
177 | outline: none;
178 | border-color: var(--modern-blue);
179 | box-shadow:
180 | 0 0 0 3px rgba(59, 130, 246, 0.1),
181 | 0 1px 3px rgba(0, 0, 0, 0.1),
182 | 0 1px 2px rgba(0, 0, 0, 0.06);
183 | }
184 |
185 | .input-neon::placeholder {
186 | color: var(--text-secondary);
187 | font-style: normal;
188 | }
189 |
190 | /* Modern progress bar */
191 | .progress-neon {
192 | background: var(--clean-gray);
193 | border: 1px solid var(--light-border);
194 | border-radius: 8px;
195 | height: 8px;
196 | overflow: hidden;
197 | position: relative;
198 | }
199 |
200 | .progress-neon::before {
201 | content: '';
202 | position: absolute;
203 | top: 0;
204 | left: 0;
205 | height: 100%;
206 | background: linear-gradient(90deg, var(--modern-blue), var(--bright-accent));
207 | width: var(--progress, 0%);
208 | transition: width 0.3s ease;
209 | border-radius: 8px;
210 | }
211 |
212 | /* Success/Error states */
213 | .success-neon {
214 | border-color: var(--success-green);
215 | background: rgba(16, 185, 129, 0.05);
216 | box-shadow:
217 | 0 1px 3px rgba(0, 0, 0, 0.1),
218 | 0 1px 2px rgba(0, 0, 0, 0.06);
219 | }
220 |
221 | .error-neon {
222 | border-color: var(--error-red);
223 | background: rgba(239, 68, 68, 0.05);
224 | box-shadow:
225 | 0 1px 3px rgba(0, 0, 0, 0.1),
226 | 0 1px 2px rgba(0, 0, 0, 0.06);
227 | }
228 |
229 | /* Modern format selection */
230 | .format-option {
231 | background: var(--modern-white);
232 | border: 1px solid var(--light-border);
233 | border-radius: 8px;
234 | padding: 12px 16px;
235 | margin: 4px;
236 | cursor: pointer;
237 | transition: all 0.2s ease;
238 | color: var(--text-primary);
239 | }
240 |
241 | .format-option:hover {
242 | border-color: var(--modern-blue);
243 | box-shadow:
244 | 0 1px 3px rgba(0, 0, 0, 0.1),
245 | 0 1px 2px rgba(0, 0, 0, 0.06);
246 | }
247 |
248 | .format-option.selected {
249 | border-color: var(--modern-blue);
250 | background: rgba(59, 130, 246, 0.05);
251 | box-shadow:
252 | 0 0 0 3px rgba(59, 130, 246, 0.1),
253 | 0 1px 3px rgba(0, 0, 0, 0.1),
254 | 0 1px 2px rgba(0, 0, 0, 0.06);
255 | }
256 |
257 | /* Modern theme option buttons */
258 | .theme-option {
259 | background: var(--modern-white);
260 | border: 1px solid var(--light-border);
261 | border-radius: 8px;
262 | color: var(--text-primary);
263 | transition: all 0.2s ease;
264 | cursor: pointer;
265 | min-width: 70px;
266 | text-align: center;
267 | font-family: 'Inter', sans-serif;
268 | font-weight: 600;
269 | position: relative;
270 | z-index: 1000;
271 | box-shadow:
272 | 0 1px 3px rgba(0, 0, 0, 0.1),
273 | 0 1px 2px rgba(0, 0, 0, 0.06);
274 | }
275 |
276 | .theme-option:hover {
277 | border-color: var(--modern-blue);
278 | transform: translateY(-1px);
279 | box-shadow:
280 | 0 2px 4px rgba(0, 0, 0, 0.1),
281 | 0 1px 2px rgba(0, 0, 0, 0.06);
282 | }
283 |
284 | .theme-option.active {
285 | background: var(--modern-blue);
286 | color: white;
287 | border-color: var(--modern-blue);
288 | box-shadow:
289 | 0 2px 4px rgba(0, 0, 0, 0.1),
290 | 0 1px 2px rgba(0, 0, 0, 0.06);
291 | }
292 |
293 | /* Modern scrollbar styling */
294 | ::-webkit-scrollbar {
295 | width: 12px;
296 | }
297 |
298 | ::-webkit-scrollbar-track {
299 | background: var(--clean-gray);
300 | border-radius: 6px;
301 | }
302 |
303 | ::-webkit-scrollbar-thumb {
304 | background: var(--light-border);
305 | border-radius: 6px;
306 | transition: background 0.2s ease;
307 | }
308 |
309 | ::-webkit-scrollbar-thumb:hover {
310 | background: var(--text-secondary);
311 | }
312 |
313 | /* Responsive Button Layouts */
314 | .theme-buttons-container {
315 | position: relative;
316 | z-index: 1000;
317 | }
318 |
319 | .action-buttons-container {
320 | width: 100%;
321 | }
322 |
323 | .action-buttons-container .btn-neon {
324 | flex: 1;
325 | min-width: 0;
326 | text-align: center;
327 | }
328 |
329 | .download-primary {
330 | min-width: 100px;
331 | flex-shrink: 0;
332 | }
333 |
334 | /* Header spacing adjustments for mobile */
335 | .header-title-section {
336 | margin-top: 1rem;
337 | }
338 |
339 | /* Responsive adjustments */
340 | @media (max-width: 768px) {
341 | h1 {
342 | font-size: 2rem;
343 | letter-spacing: 2px;
344 | }
345 |
346 | .btn-neon {
347 | padding: 10px 16px;
348 | font-size: 0.85rem;
349 | }
350 |
351 | .theme-option {
352 | min-width: 60px;
353 | padding: 8px 12px;
354 | font-size: 0.7rem;
355 | }
356 |
357 | .theme-buttons-container {
358 | margin-bottom: 1rem;
359 | }
360 |
361 | .header-title-section {
362 | margin-top: 0;
363 | }
364 |
365 | .action-buttons-container {
366 | grid-template-columns: 1fr 1fr;
367 | gap: 0.5rem;
368 | }
369 | }
370 |
371 | @media (max-width: 480px) {
372 | .theme-option {
373 | min-width: 50px;
374 | padding: 6px 8px;
375 | font-size: 0.65rem;
376 | }
377 |
378 | .btn-neon {
379 | padding: 8px 12px;
380 | font-size: 0.8rem;
381 | }
382 |
383 | .download-primary {
384 | min-width: 80px;
385 | }
386 | }
387 |
388 | /* Simple loading state */
389 | .loading {
390 | opacity: 0.7;
391 | }
392 |
393 | /* Close button styling */
394 | .card:hover .opacity-0 {
395 | opacity: 1;
396 | }
397 |
398 | .close-btn {
399 | background: var(--modern-white);
400 | border-radius: 8px;
401 | padding: 6px;
402 | transition: all 0.2s ease;
403 | border: 1px solid var(--light-border);
404 | }
405 |
406 | .close-btn:hover {
407 | background: var(--clean-gray);
408 | border-color: var(--modern-blue);
409 | }
--------------------------------------------------------------------------------
/static/css/kelethin.css:
--------------------------------------------------------------------------------
1 | /* Kelethin Treetop City Theme for yt-dlp-co2 */
2 | /* Inspired by the majestic elven city in the boughs of Greater Faydark */
3 | @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&display=swap');
4 |
5 | :root {
6 | --kelethin-bark: #8b4513;
7 | --kelethin-leaf: #9acd32;
8 | --kelethin-gold: #ffd700;
9 | --kelethin-emerald: #50c878;
10 | --kelethin-forest: #228b22;
11 | --kelethin-branch: #654321;
12 | --kelethin-bg: #0f1f0f;
13 | --kelethin-card-bg: rgba(20, 35, 20, 0.9);
14 | --kelethin-accent: #adff2f;
15 | --kelethin-glow: #32cd32;
16 | }
17 |
18 | body {
19 | background: linear-gradient(135deg, var(--kelethin-bg) 0%, #1a2f1a 30%, #0d220d 70%, var(--kelethin-bg) 100%);
20 | color: var(--kelethin-emerald);
21 | font-family: 'Cinzel', serif;
22 | min-height: 100vh;
23 | position: relative;
24 | overflow-x: hidden;
25 | }
26 |
27 | /* Treetop canopy background with subtle branch patterns */
28 | body::before {
29 | content: '';
30 | position: fixed;
31 | top: 0;
32 | left: 0;
33 | width: 100%;
34 | height: 100%;
35 | background-image:
36 | /* Tree branch silhouettes */
37 | radial-gradient(ellipse 200px 50px at 30% 20%, rgba(139, 69, 19, 0.15) 0%, transparent 70%),
38 | radial-gradient(ellipse 150px 40px at 70% 15%, rgba(139, 69, 19, 0.12) 0%, transparent 70%),
39 | radial-gradient(ellipse 180px 45px at 50% 25%, rgba(139, 69, 19, 0.1) 0%, transparent 70%),
40 | /* Leaf clusters */
41 | radial-gradient(circle at 25% 30%, rgba(154, 205, 50, 0.08) 0%, transparent 40%),
42 | radial-gradient(circle at 75% 35%, rgba(173, 255, 47, 0.06) 0%, transparent 45%),
43 | radial-gradient(circle at 45% 28%, rgba(154, 205, 50, 0.05) 0%, transparent 35%),
44 | /* Dappled sunlight */
45 | radial-gradient(circle at 60% 60%, rgba(255, 215, 0, 0.04) 0%, transparent 30%),
46 | radial-gradient(circle at 20% 70%, rgba(255, 215, 0, 0.03) 0%, transparent 25%);
47 | animation: canopy-sway 12s ease-in-out infinite alternate;
48 | z-index: -2;
49 | }
50 |
51 |
52 | @keyframes canopy-sway {
53 | 0% { transform: scale(1) translateX(0px) rotate(0deg); opacity: 0.7; }
54 | 100% { transform: scale(1.02) translateX(2px) rotate(0.5deg); opacity: 0.9; }
55 | }
56 |
57 |
58 | /* Kelethin wood-elf text effects */
59 | h1 {
60 | color: var(--kelethin-gold);
61 | text-shadow:
62 | 0 0 10px var(--kelethin-gold),
63 | 0 0 20px var(--kelethin-gold),
64 | 0 0 30px rgba(255, 215, 0, 0.6),
65 | /* Subtle bark texture shadow */
66 | 2px 2px 0px var(--kelethin-branch);
67 | font-weight: 700;
68 | text-transform: uppercase;
69 | letter-spacing: 4px;
70 | font-family: 'Cinzel', serif;
71 | }
72 |
73 | .tagline {
74 | color: var(--kelethin-accent);
75 | text-shadow:
76 | 0 0 5px var(--kelethin-leaf),
77 | 0 0 10px var(--kelethin-accent);
78 | font-style: italic;
79 | font-weight: 400;
80 | }
81 |
82 | /* Wooden platform card styling - like Kelethin's tree platforms */
83 | .card {
84 | background: var(--kelethin-card-bg);
85 | border: 2px solid var(--kelethin-branch);
86 | border-radius: 12px;
87 | box-shadow:
88 | 0 0 20px rgba(139, 69, 19, 0.4),
89 | inset 0 0 20px rgba(154, 205, 50, 0.05),
90 | /* Wood grain shadow */
91 | 0 4px 8px rgba(101, 67, 33, 0.3);
92 | backdrop-filter: blur(8px);
93 | position: relative;
94 | }
95 |
96 | .card::before {
97 | content: '';
98 | position: absolute;
99 | top: -2px;
100 | left: -2px;
101 | right: -2px;
102 | bottom: -2px;
103 | background: linear-gradient(45deg, var(--kelethin-bark), var(--kelethin-leaf), var(--kelethin-gold));
104 | border-radius: 12px;
105 | z-index: -1;
106 | opacity: 0.2;
107 | }
108 |
109 | /* Wood grain texture overlay */
110 | .card::after {
111 | content: '';
112 | position: absolute;
113 | top: 0;
114 | left: 0;
115 | right: 0;
116 | bottom: 0;
117 | background-image:
118 | repeating-linear-gradient(
119 | 90deg,
120 | transparent 0px,
121 | rgba(139, 69, 19, 0.03) 1px,
122 | transparent 2px,
123 | transparent 8px
124 | );
125 | border-radius: 12px;
126 | pointer-events: none;
127 | }
128 |
129 | /* Elven-crafted button styling */
130 | .btn-neon {
131 | background: linear-gradient(45deg, var(--kelethin-forest), var(--kelethin-emerald));
132 | border: 2px solid var(--kelethin-bark);
133 | color: var(--kelethin-gold);
134 | padding: 12px 24px;
135 | border-radius: 20px;
136 | font-weight: 600;
137 | text-transform: uppercase;
138 | letter-spacing: 1px;
139 | cursor: pointer;
140 | transition: all 0.3s ease;
141 | box-shadow:
142 | 0 0 15px rgba(139, 69, 19, 0.4),
143 | inset 0 0 15px rgba(154, 205, 50, 0.1);
144 | font-family: 'Cinzel', serif;
145 | position: relative;
146 | }
147 |
148 | .btn-neon:hover:not(:disabled) {
149 | transform: translateY(-2px);
150 | box-shadow:
151 | 0 5px 25px rgba(255, 215, 0, 0.6),
152 | inset 0 0 25px rgba(154, 205, 50, 0.2);
153 | text-shadow: 0 0 10px var(--kelethin-gold);
154 | background: linear-gradient(45deg, var(--kelethin-emerald), var(--kelethin-glow));
155 | }
156 |
157 | .btn-neon:disabled {
158 | opacity: 0.4;
159 | cursor: not-allowed;
160 | transform: none;
161 | filter: grayscale(30%);
162 | }
163 |
164 | /* Input styling */
165 | .input-neon {
166 | background: rgba(10, 26, 10, 0.8);
167 | border: 2px solid var(--kelethin-emerald);
168 | border-radius: 10px;
169 | color: var(--kelethin-accent);
170 | padding: 12px 16px;
171 | font-family: 'Cinzel', serif;
172 | box-shadow:
173 | 0 0 15px rgba(80, 200, 120, 0.3),
174 | inset 0 0 15px rgba(80, 200, 120, 0.05);
175 | }
176 |
177 | .input-neon:focus {
178 | outline: none;
179 | border-color: var(--kelethin-gold);
180 | box-shadow:
181 | 0 0 25px rgba(255, 215, 0, 0.5),
182 | inset 0 0 25px rgba(255, 215, 0, 0.1);
183 | background: rgba(16, 32, 16, 0.9);
184 | }
185 |
186 | .input-neon::placeholder {
187 | color: rgba(152, 251, 152, 0.6);
188 | font-style: italic;
189 | }
190 |
191 | /* Progress bar */
192 | .progress-neon {
193 | background: rgba(10, 26, 10, 0.8);
194 | border: 2px solid var(--kelethin-emerald);
195 | border-radius: 10px;
196 | height: 20px;
197 | overflow: hidden;
198 | position: relative;
199 | }
200 |
201 | .progress-neon::before {
202 | content: '';
203 | position: absolute;
204 | top: 0;
205 | left: 0;
206 | height: 100%;
207 | background: linear-gradient(90deg, var(--kelethin-forest), var(--kelethin-gold));
208 | width: var(--progress, 0%);
209 | transition: width 0.3s ease;
210 | box-shadow: 0 0 15px rgba(255, 215, 0, 0.6);
211 | }
212 |
213 | /* Success/Error states */
214 | .success-neon {
215 | border-color: var(--kelethin-glow);
216 | background: rgba(50, 205, 50, 0.1);
217 | box-shadow: 0 0 20px rgba(50, 205, 50, 0.4);
218 | }
219 |
220 | .error-neon {
221 | border-color: #cd5c5c;
222 | background: rgba(205, 92, 92, 0.1);
223 | box-shadow: 0 0 20px rgba(205, 92, 92, 0.4);
224 | }
225 |
226 | /* Format selection */
227 | .format-option {
228 | background: rgba(10, 26, 10, 0.6);
229 | border: 1px solid var(--kelethin-forest);
230 | border-radius: 8px;
231 | padding: 8px 12px;
232 | margin: 4px;
233 | cursor: pointer;
234 | transition: all 0.3s ease;
235 | color: var(--kelethin-accent);
236 | }
237 |
238 | .format-option:hover {
239 | transform: translateY(-1px);
240 | border-color: var(--kelethin-emerald);
241 | background: rgba(16, 32, 16, 0.8);
242 | }
243 |
244 | .format-option:hover,
245 | .format-option.selected {
246 | border-color: var(--kelethin-gold);
247 | background: rgba(255, 215, 0, 0.1);
248 | box-shadow: 0 0 12px rgba(255, 215, 0, 0.3);
249 | }
250 |
251 | /* Theme option buttons */
252 | .theme-option {
253 | background: rgba(10, 26, 10, 0.8);
254 | border: 2px solid var(--kelethin-emerald);
255 | border-radius: 10px;
256 | color: var(--kelethin-gold);
257 | transition: all 0.3s ease;
258 | cursor: pointer;
259 | min-width: 70px;
260 | text-align: center;
261 | font-family: 'Cinzel', serif;
262 | font-weight: 600;
263 | position: relative;
264 | z-index: 1000;
265 | }
266 |
267 | .theme-option:hover {
268 | background: rgba(16, 32, 16, 0.9);
269 | box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
270 | text-shadow: 0 0 8px var(--kelethin-gold);
271 | transform: translateY(-1px);
272 | }
273 |
274 | .theme-option.active {
275 | background: rgba(16, 32, 16, 0.9);
276 | box-shadow: 0 0 20px rgba(255, 215, 0, 0.6);
277 | text-shadow: 0 0 10px var(--kelethin-gold);
278 | border-color: var(--kelethin-gold);
279 | }
280 |
281 | /* Scrollbar styling */
282 | ::-webkit-scrollbar {
283 | width: 12px;
284 | }
285 |
286 | ::-webkit-scrollbar-track {
287 | background: var(--kelethin-bg);
288 | }
289 |
290 | ::-webkit-scrollbar-thumb {
291 | background: linear-gradient(45deg, var(--kelethin-forest), var(--kelethin-emerald));
292 | border-radius: 6px;
293 | box-shadow: 0 0 8px rgba(80, 200, 120, 0.3);
294 | }
295 |
296 | /* Responsive Button Layouts */
297 | .theme-buttons-container {
298 | position: relative;
299 | z-index: 1000;
300 | }
301 |
302 | .action-buttons-container {
303 | width: 100%;
304 | }
305 |
306 | .action-buttons-container .btn-neon {
307 | flex: 1;
308 | min-width: 0;
309 | text-align: center;
310 | }
311 |
312 | .download-primary {
313 | min-width: 100px;
314 | flex-shrink: 0;
315 | }
316 |
317 | /* Header spacing adjustments for mobile */
318 | .header-title-section {
319 | margin-top: 1rem;
320 | }
321 |
322 | /* Responsive adjustments */
323 | @media (max-width: 768px) {
324 | h1 {
325 | font-size: 2rem;
326 | letter-spacing: 2px;
327 | }
328 |
329 | .btn-neon {
330 | padding: 8px 16px;
331 | font-size: 0.85rem;
332 | letter-spacing: 0.5px;
333 | }
334 |
335 | .theme-option {
336 | min-width: 60px;
337 | padding: 8px 12px;
338 | font-size: 0.7rem;
339 | }
340 |
341 | .theme-buttons-container {
342 | margin-bottom: 1rem;
343 | }
344 |
345 | .header-title-section {
346 | margin-top: 0;
347 | }
348 |
349 | .action-buttons-container {
350 | grid-template-columns: 1fr 1fr;
351 | gap: 0.5rem;
352 | }
353 | }
354 |
355 | @media (max-width: 480px) {
356 | .theme-option {
357 | min-width: 50px;
358 | padding: 6px 8px;
359 | font-size: 0.65rem;
360 | }
361 |
362 | .btn-neon {
363 | padding: 6px 12px;
364 | font-size: 0.8rem;
365 | letter-spacing: 0px;
366 | }
367 |
368 | .download-primary {
369 | min-width: 80px;
370 | }
371 | }
372 |
373 | /* Animation for loading states */
374 | @keyframes pulse-elf {
375 | 0%, 100% { opacity: 1; }
376 | 50% { opacity: 0.6; }
377 | }
378 |
379 | .loading {
380 | animation: pulse-elf 1.5s ease-in-out infinite;
381 | }
382 |
383 | /* Close button styling */
384 | .card:hover .opacity-0 {
385 | opacity: 1;
386 | }
387 |
388 | .close-btn {
389 | background: rgba(10, 26, 10, 0.8);
390 | border-radius: 6px;
391 | padding: 4px;
392 | transition: all 0.3s ease;
393 | border: 1px solid var(--kelethin-forest);
394 | }
395 |
396 | .close-btn:hover {
397 | background: rgba(16, 32, 16, 0.9);
398 | box-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
399 | border-color: var(--kelethin-emerald);
400 | }
401 |
402 | /* Footer styling */
403 | footer {
404 | border-top: 2px solid var(--kelethin-emerald);
405 | background: rgba(10, 26, 10, 0.8);
406 | box-shadow: 0 -5px 15px rgba(80, 200, 120, 0.2);
407 | }
408 |
409 | footer p {
410 | color: var(--kelethin-accent);
411 | text-shadow: 0 0 5px var(--kelethin-emerald);
412 | }
413 |
414 | /* Magical glow animation */
415 | @keyframes magical-glow {
416 | 0%, 100% { box-shadow: 0 0 5px var(--kelethin-gold); }
417 | 50% { box-shadow: 0 0 20px var(--kelethin-gold), 0 0 30px var(--kelethin-emerald); }
418 | }
419 |
420 | .card:hover {
421 | animation: magical-glow 2s ease-in-out infinite;
422 | }
--------------------------------------------------------------------------------
/static/css/music.css:
--------------------------------------------------------------------------------
1 | /* Music Theme for yt-dlp-co2 */
2 | /* Inspired by audio visualizers, sound waves, and music production */
3 | @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600;700&display=swap');
4 |
5 | :root {
6 | --music-bass: #ff6b35;
7 | --music-mid: #f7931e;
8 | --music-treble: #ffd23f;
9 | --music-vocal: #ee5a24;
10 | --music-bg: #2c2c54;
11 | --music-dark: #1a1a2e;
12 | --music-card-bg: rgba(44, 44, 84, 0.9);
13 | --music-accent: #ff3838;
14 | --music-glow: #ff6348;
15 | --music-wave: #ff9ff3;
16 | }
17 |
18 | body {
19 | background: linear-gradient(135deg, var(--music-dark) 0%, #16213e 30%, var(--music-bg) 70%, var(--music-dark) 100%);
20 | color: var(--music-treble);
21 | font-family: 'JetBrains Mono', monospace;
22 | min-height: 100vh;
23 | position: relative;
24 | overflow-x: hidden;
25 | }
26 |
27 | /* Audio equalizer background */
28 | body::before {
29 | content: '';
30 | position: fixed;
31 | top: 0;
32 | left: 0;
33 | width: 100%;
34 | height: 100%;
35 | background-image:
36 | /* Equalizer bars */
37 | repeating-linear-gradient(
38 | 90deg,
39 | transparent 0px,
40 | rgba(255, 107, 53, 0.1) 5px,
41 | transparent 10px,
42 | transparent 20px
43 | ),
44 | repeating-linear-gradient(
45 | 0deg,
46 | transparent 0px,
47 | rgba(247, 147, 30, 0.05) 2px,
48 | transparent 4px,
49 | transparent 40px
50 | ),
51 | /* Sound wave patterns */
52 | radial-gradient(ellipse at 20% 50%, rgba(255, 210, 63, 0.08) 0%, transparent 50%),
53 | radial-gradient(ellipse at 80% 30%, rgba(255, 56, 56, 0.06) 0%, transparent 40%),
54 | radial-gradient(ellipse at 50% 70%, rgba(255, 159, 243, 0.04) 0%, transparent 30%);
55 | animation: equalizer-pulse 4s ease-in-out infinite alternate;
56 | z-index: -2;
57 | }
58 |
59 | /* Floating audio bars */
60 | #audio-visualizer {
61 | position: fixed;
62 | bottom: 0;
63 | left: 0;
64 | width: 100%;
65 | height: 100px;
66 | z-index: -10;
67 | pointer-events: none;
68 | overflow: hidden;
69 | }
70 |
71 | .audio-bar {
72 | position: absolute;
73 | bottom: 0;
74 | width: 4px;
75 | background: linear-gradient(to top, var(--music-bass), var(--music-treble));
76 | border-radius: 2px 2px 0 0;
77 | animation: audio-bounce linear infinite;
78 | box-shadow: 0 0 8px var(--music-glow);
79 | }
80 |
81 | @keyframes equalizer-pulse {
82 | 0% { transform: scale(1) skewX(0deg); opacity: 0.6; }
83 | 100% { transform: scale(1.02) skewX(0.5deg); opacity: 0.8; }
84 | }
85 |
86 | @keyframes audio-bounce {
87 | 0%, 100% {
88 | height: 10px;
89 | background: linear-gradient(to top, var(--music-bass), var(--music-mid));
90 | }
91 | 25% {
92 | height: 60px;
93 | background: linear-gradient(to top, var(--music-bass), var(--music-treble));
94 | }
95 | 50% {
96 | height: 25px;
97 | background: linear-gradient(to top, var(--music-mid), var(--music-wave));
98 | }
99 | 75% {
100 | height: 45px;
101 | background: linear-gradient(to top, var(--music-vocal), var(--music-accent));
102 | }
103 | }
104 |
105 | /* Music-themed text effects */
106 | h1 {
107 | color: var(--music-accent);
108 | text-shadow:
109 | 0 0 10px var(--music-accent),
110 | 0 0 20px var(--music-glow),
111 | 0 0 30px rgba(255, 56, 56, 0.5),
112 | /* Beat drop shadow */
113 | 3px 3px 0px var(--music-bass);
114 | font-weight: 700;
115 | text-transform: uppercase;
116 | letter-spacing: 3px;
117 | font-family: 'JetBrains Mono', monospace;
118 | animation: beat-pulse 2s ease-in-out infinite;
119 | }
120 |
121 | @keyframes beat-pulse {
122 | 0%, 100% { transform: scale(1); }
123 | 50% { transform: scale(1.02); }
124 | }
125 |
126 | .tagline {
127 | color: var(--music-wave);
128 | text-shadow:
129 | 0 0 5px var(--music-wave),
130 | 0 0 10px var(--music-vocal);
131 | font-style: italic;
132 | font-weight: 300;
133 | }
134 |
135 | /* Studio equipment card styling */
136 | .card {
137 | background: var(--music-card-bg);
138 | border: 2px solid var(--music-bass);
139 | border-radius: 8px;
140 | box-shadow:
141 | 0 0 20px rgba(255, 107, 53, 0.3),
142 | inset 0 0 20px rgba(255, 210, 63, 0.05),
143 | /* Studio lighting effect */
144 | 0 8px 16px rgba(26, 26, 46, 0.4);
145 | backdrop-filter: blur(8px);
146 | position: relative;
147 | }
148 |
149 | .card::before {
150 | content: '';
151 | position: absolute;
152 | top: -2px;
153 | left: -2px;
154 | right: -2px;
155 | bottom: -2px;
156 | background: linear-gradient(45deg, var(--music-bass), var(--music-treble), var(--music-wave));
157 | border-radius: 8px;
158 | z-index: -1;
159 | opacity: 0.3;
160 | }
161 |
162 | /* Console-style buttons */
163 | .btn-neon {
164 | background: linear-gradient(45deg, var(--music-bg), var(--music-dark));
165 | border: 2px solid var(--music-accent);
166 | color: var(--music-treble);
167 | padding: 12px 24px;
168 | border-radius: 6px;
169 | font-weight: 600;
170 | text-transform: uppercase;
171 | letter-spacing: 1px;
172 | cursor: pointer;
173 | transition: all 0.3s ease;
174 | box-shadow:
175 | 0 0 15px rgba(255, 56, 56, 0.4),
176 | inset 0 0 15px rgba(255, 210, 63, 0.1);
177 | font-family: 'JetBrains Mono', monospace;
178 | position: relative;
179 | }
180 |
181 | .btn-neon:hover:not(:disabled) {
182 | transform: translateY(-2px);
183 | box-shadow:
184 | 0 5px 25px rgba(255, 56, 56, 0.6),
185 | inset 0 0 25px rgba(255, 210, 63, 0.2);
186 | text-shadow: 0 0 10px var(--music-treble);
187 | background: linear-gradient(45deg, var(--music-accent), var(--music-glow));
188 | animation: button-beat 0.3s ease-out;
189 | }
190 |
191 | @keyframes button-beat {
192 | 0% { transform: translateY(-2px) scale(1); }
193 | 50% { transform: translateY(-2px) scale(1.05); }
194 | 100% { transform: translateY(-2px) scale(1); }
195 | }
196 |
197 | .btn-neon:disabled {
198 | opacity: 0.4;
199 | cursor: not-allowed;
200 | transform: none;
201 | filter: grayscale(40%);
202 | }
203 |
204 | /* Mixer-style inputs */
205 | .input-neon {
206 | background: rgba(26, 26, 46, 0.8);
207 | border: 2px solid var(--music-bass);
208 | border-radius: 6px;
209 | color: var(--music-treble);
210 | padding: 12px 16px;
211 | font-family: 'JetBrains Mono', monospace;
212 | box-shadow:
213 | 0 0 15px rgba(255, 107, 53, 0.3),
214 | inset 0 0 15px rgba(255, 107, 53, 0.05);
215 | }
216 |
217 | .input-neon:focus {
218 | outline: none;
219 | border-color: var(--music-accent);
220 | box-shadow:
221 | 0 0 25px rgba(255, 56, 56, 0.5),
222 | inset 0 0 25px rgba(255, 56, 56, 0.1);
223 | background: rgba(44, 44, 84, 0.9);
224 | }
225 |
226 | .input-neon::placeholder {
227 | color: rgba(255, 210, 63, 0.6);
228 | font-style: italic;
229 | }
230 |
231 | /* Audio level meter */
232 | .progress-neon {
233 | background: rgba(26, 26, 46, 0.8);
234 | border: 2px solid var(--music-bass);
235 | border-radius: 6px;
236 | height: 20px;
237 | overflow: hidden;
238 | position: relative;
239 | }
240 |
241 | .progress-neon::before {
242 | content: '';
243 | position: absolute;
244 | top: 0;
245 | left: 0;
246 | height: 100%;
247 | background: linear-gradient(90deg, var(--music-bass), var(--music-accent));
248 | width: var(--progress, 0%);
249 | transition: width 0.3s ease;
250 | box-shadow: 0 0 15px rgba(255, 56, 56, 0.6);
251 | }
252 |
253 | /* Success/Error states */
254 | .success-neon {
255 | border-color: var(--music-treble);
256 | background: rgba(255, 210, 63, 0.1);
257 | box-shadow: 0 0 20px rgba(255, 210, 63, 0.4);
258 | }
259 |
260 | .error-neon {
261 | border-color: #e74c3c;
262 | background: rgba(231, 76, 60, 0.1);
263 | box-shadow: 0 0 20px rgba(231, 76, 60, 0.4);
264 | }
265 |
266 | /* Track selection */
267 | .format-option {
268 | background: rgba(26, 26, 46, 0.6);
269 | border: 1px solid var(--music-bg);
270 | border-radius: 6px;
271 | padding: 8px 12px;
272 | margin: 4px;
273 | cursor: pointer;
274 | transition: all 0.3s ease;
275 | color: var(--music-treble);
276 | }
277 |
278 | .format-option:hover {
279 | transform: translateY(-1px);
280 | border-color: var(--music-bass);
281 | background: rgba(44, 44, 84, 0.8);
282 | }
283 |
284 | .format-option:hover,
285 | .format-option.selected {
286 | border-color: var(--music-accent);
287 | background: rgba(255, 56, 56, 0.1);
288 | box-shadow: 0 0 12px rgba(255, 56, 56, 0.3);
289 | }
290 |
291 | /* Theme option buttons */
292 | .theme-option {
293 | background: rgba(26, 26, 46, 0.8);
294 | border: 2px solid var(--music-bass);
295 | border-radius: 6px;
296 | color: var(--music-treble);
297 | transition: all 0.3s ease;
298 | cursor: pointer;
299 | min-width: 70px;
300 | text-align: center;
301 | font-family: 'JetBrains Mono', monospace;
302 | font-weight: 600;
303 | position: relative;
304 | z-index: 1000;
305 | }
306 |
307 | .theme-option:hover {
308 | background: rgba(44, 44, 84, 0.9);
309 | box-shadow: 0 0 15px rgba(255, 107, 53, 0.4);
310 | text-shadow: 0 0 8px var(--music-treble);
311 | transform: translateY(-1px);
312 | }
313 |
314 | .theme-option.active {
315 | background: rgba(44, 44, 84, 0.9);
316 | box-shadow: 0 0 20px rgba(255, 56, 56, 0.6);
317 | text-shadow: 0 0 10px var(--music-accent);
318 | border-color: var(--music-accent);
319 | }
320 |
321 | /* Scrollbar styling */
322 | ::-webkit-scrollbar {
323 | width: 12px;
324 | }
325 |
326 | ::-webkit-scrollbar-track {
327 | background: var(--music-dark);
328 | }
329 |
330 | ::-webkit-scrollbar-thumb {
331 | background: linear-gradient(45deg, var(--music-bass), var(--music-accent));
332 | border-radius: 6px;
333 | box-shadow: 0 0 8px rgba(255, 56, 56, 0.3);
334 | }
335 |
336 | /* Responsive Button Layouts */
337 | .theme-buttons-container {
338 | position: relative;
339 | z-index: 1000;
340 | }
341 |
342 | .action-buttons-container {
343 | width: 100%;
344 | }
345 |
346 | .action-buttons-container .btn-neon {
347 | flex: 1;
348 | min-width: 0;
349 | text-align: center;
350 | }
351 |
352 | .download-primary {
353 | min-width: 100px;
354 | flex-shrink: 0;
355 | }
356 |
357 | /* Header spacing adjustments for mobile */
358 | .header-title-section {
359 | margin-top: 1rem;
360 | }
361 |
362 | /* Responsive adjustments */
363 | @media (max-width: 768px) {
364 | h1 {
365 | font-size: 2rem;
366 | letter-spacing: 2px;
367 | }
368 |
369 | .btn-neon {
370 | padding: 8px 16px;
371 | font-size: 0.85rem;
372 | }
373 |
374 | .theme-option {
375 | min-width: 60px;
376 | padding: 8px 12px;
377 | font-size: 0.7rem;
378 | }
379 |
380 | .theme-buttons-container {
381 | margin-bottom: 1rem;
382 | }
383 |
384 | .header-title-section {
385 | margin-top: 0;
386 | }
387 |
388 | .action-buttons-container {
389 | grid-template-columns: 1fr 1fr;
390 | gap: 0.5rem;
391 | }
392 | }
393 |
394 | @media (max-width: 480px) {
395 | .theme-option {
396 | min-width: 50px;
397 | padding: 6px 8px;
398 | font-size: 0.65rem;
399 | }
400 |
401 | .btn-neon {
402 | padding: 6px 12px;
403 | font-size: 0.8rem;
404 | }
405 |
406 | .download-primary {
407 | min-width: 80px;
408 | }
409 | }
410 |
411 | /* Animation for loading states */
412 | @keyframes pulse-music {
413 | 0%, 100% { opacity: 1; }
414 | 50% { opacity: 0.7; }
415 | }
416 |
417 | .loading {
418 | animation: pulse-music 1.5s ease-in-out infinite;
419 | }
420 |
421 | /* Close button styling */
422 | .card:hover .opacity-0 {
423 | opacity: 1;
424 | }
425 |
426 | .close-btn {
427 | background: rgba(26, 26, 46, 0.8);
428 | border-radius: 4px;
429 | padding: 4px;
430 | transition: all 0.3s ease;
431 | border: 1px solid var(--music-bg);
432 | }
433 |
434 | .close-btn:hover {
435 | background: rgba(44, 44, 84, 0.9);
436 | box-shadow: 0 0 10px rgba(255, 107, 53, 0.3);
437 | border-color: var(--music-bass);
438 | }
439 |
440 | /* Footer styling */
441 | footer {
442 | border-top: 2px solid var(--music-bass);
443 | background: rgba(26, 26, 46, 0.8);
444 | box-shadow: 0 -5px 15px rgba(255, 107, 53, 0.2);
445 | }
446 |
447 | footer p {
448 | color: var(--music-treble);
449 | text-shadow: 0 0 5px var(--music-bass);
450 | }
451 |
452 | /* Beat-responsive glow animation */
453 | @keyframes studio-glow {
454 | 0%, 100% { box-shadow: 0 0 5px var(--music-bass); }
455 | 33% { box-shadow: 0 0 20px var(--music-accent), 0 0 30px var(--music-glow); }
456 | 66% { box-shadow: 0 0 15px var(--music-treble), 0 0 25px var(--music-wave); }
457 | }
458 |
459 | .card:hover {
460 | animation: studio-glow 3s ease-in-out infinite;
461 | }
--------------------------------------------------------------------------------
/static/css/fun.css:
--------------------------------------------------------------------------------
1 | /* Fun Theme for yt-dlp-co2 */
2 | /* Playful and colorful for a fun experience! 🎉 */
3 | @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300;400;600;700&display=swap');
4 |
5 | :root {
6 | --cat-orange: #ff8c42;
7 | --cat-cream: #fdeaa7;
8 | --cat-pink: #ff6b9d;
9 | --cat-purple: #c44569;
10 | --cat-brown: #8d4925;
11 | --cat-gray: #95a5a6;
12 | --cat-bg: #2c2c54;
13 | --cat-dark: #1a1a2e;
14 | --cat-card-bg: rgba(253, 234, 167, 0.95);
15 | --cat-accent: #ff6b9d;
16 | --cat-paw: #ff8c42;
17 | --cat-whisker: #34495e;
18 | }
19 |
20 | body {
21 | background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 30%, #fd79a8 70%, #e84393 100%);
22 | color: var(--cat-brown);
23 | font-family: 'Comfortaa', cursive;
24 | min-height: 100vh;
25 | position: relative;
26 | overflow-x: hidden;
27 | }
28 |
29 | /* Floating paw prints background */
30 | body::before {
31 | content: '';
32 | position: fixed;
33 | top: 0;
34 | left: 0;
35 | width: 100%;
36 | height: 100%;
37 | background-image:
38 | /* Paw print patterns */
39 | radial-gradient(circle 8px at 20% 30%, rgba(255, 140, 66, 0.3) 40%, transparent 41%),
40 | radial-gradient(circle 6px at 22% 25%, rgba(255, 140, 66, 0.3) 40%, transparent 41%),
41 | radial-gradient(circle 6px at 18% 25%, rgba(255, 140, 66, 0.3) 40%, transparent 41%),
42 | radial-gradient(circle 4px at 20% 22%, rgba(255, 140, 66, 0.3) 40%, transparent 41%),
43 |
44 | radial-gradient(circle 8px at 70% 60%, rgba(255, 107, 157, 0.25) 40%, transparent 41%),
45 | radial-gradient(circle 6px at 72% 55%, rgba(255, 107, 157, 0.25) 40%, transparent 41%),
46 | radial-gradient(circle 6px at 68% 55%, rgba(255, 107, 157, 0.25) 40%, transparent 41%),
47 | radial-gradient(circle 4px at 70% 52%, rgba(255, 107, 157, 0.25) 40%, transparent 41%),
48 |
49 | radial-gradient(circle 8px at 85% 20%, rgba(196, 69, 105, 0.2) 40%, transparent 41%),
50 | radial-gradient(circle 6px at 87% 15%, rgba(196, 69, 105, 0.2) 40%, transparent 41%),
51 | radial-gradient(circle 6px at 83% 15%, rgba(196, 69, 105, 0.2) 40%, transparent 41%),
52 | radial-gradient(circle 4px at 85% 12%, rgba(196, 69, 105, 0.2) 40%, transparent 41%),
53 |
54 | /* Soft yarn ball textures */
55 | radial-gradient(ellipse at 40% 80%, rgba(255, 140, 66, 0.1) 0%, transparent 50%),
56 | radial-gradient(ellipse at 60% 40%, rgba(255, 107, 157, 0.08) 0%, transparent 40%);
57 | animation: paw-drift 20s ease-in-out infinite;
58 | z-index: -2;
59 | }
60 |
61 | /* Floating cat toys */
62 | #cat-toys {
63 | position: fixed;
64 | top: 0;
65 | left: 0;
66 | width: 100%;
67 | height: 100%;
68 | z-index: -10;
69 | pointer-events: none;
70 | overflow: hidden;
71 | }
72 |
73 | .cat-toy {
74 | position: absolute;
75 | font-size: 20px;
76 | animation: cat-float linear infinite;
77 | opacity: 0.7;
78 | }
79 |
80 | @keyframes paw-drift {
81 | 0% { transform: translateY(0px) translateX(0px); }
82 | 50% { transform: translateY(-10px) translateX(5px); }
83 | 100% { transform: translateY(0px) translateX(0px); }
84 | }
85 |
86 | @keyframes cat-float {
87 | 0% {
88 | transform: translateY(100vh) translateX(0px) rotate(0deg);
89 | opacity: 0;
90 | }
91 | 10% {
92 | opacity: 0.7;
93 | }
94 | 90% {
95 | opacity: 0.7;
96 | }
97 | 100% {
98 | transform: translateY(-100px) translateX(50px) rotate(360deg);
99 | opacity: 0;
100 | }
101 | }
102 |
103 | /* Purr-fect text effects */
104 | h1 {
105 | color: var(--cat-purple);
106 | text-shadow:
107 | 0 0 10px var(--cat-pink),
108 | 0 0 20px var(--cat-purple),
109 | 0 0 30px rgba(196, 69, 105, 0.5),
110 | /* Fluffy shadow */
111 | 3px 3px 0px var(--cat-orange);
112 | font-weight: 700;
113 | text-transform: uppercase;
114 | letter-spacing: 3px;
115 | font-family: 'Comfortaa', cursive;
116 | animation: cat-purr 3s ease-in-out infinite;
117 | }
118 |
119 | @keyframes cat-purr {
120 | 0%, 100% { transform: scale(1); }
121 | 50% { transform: scale(1.01); }
122 | }
123 |
124 | .tagline {
125 | color: var(--cat-brown);
126 | text-shadow:
127 | 0 0 5px var(--cat-cream),
128 | 0 0 10px var(--cat-orange);
129 | font-style: italic;
130 | font-weight: 400;
131 | }
132 |
133 | /* Cozy cat bed card styling */
134 | .card {
135 | background: var(--cat-card-bg);
136 | border: 3px solid var(--cat-orange);
137 | border-radius: 20px;
138 | box-shadow:
139 | 0 0 25px rgba(255, 140, 66, 0.4),
140 | inset 0 0 25px rgba(255, 107, 157, 0.1),
141 | /* Soft cushion effect */
142 | 0 10px 20px rgba(141, 73, 37, 0.2);
143 | backdrop-filter: blur(5px);
144 | position: relative;
145 | }
146 |
147 | .card::before {
148 | content: '';
149 | position: absolute;
150 | top: -3px;
151 | left: -3px;
152 | right: -3px;
153 | bottom: -3px;
154 | background: linear-gradient(45deg, var(--cat-orange), var(--cat-pink), var(--cat-cream));
155 | border-radius: 20px;
156 | z-index: -1;
157 | opacity: 0.4;
158 | }
159 |
160 | /* Fluffy texture overlay */
161 | .card::after {
162 | content: '';
163 | position: absolute;
164 | top: 0;
165 | left: 0;
166 | right: 0;
167 | bottom: 0;
168 | background-image:
169 | radial-gradient(circle 2px at 25% 25%, rgba(255, 140, 66, 0.1) 50%, transparent 51%),
170 | radial-gradient(circle 1px at 75% 75%, rgba(255, 107, 157, 0.1) 50%, transparent 51%),
171 | radial-gradient(circle 1px at 50% 50%, rgba(196, 69, 105, 0.08) 50%, transparent 51%);
172 | border-radius: 20px;
173 | pointer-events: none;
174 | }
175 |
176 | /* Cat toy buttons */
177 | .btn-neon {
178 | background: linear-gradient(45deg, var(--cat-orange), var(--cat-pink));
179 | border: 3px solid var(--cat-purple);
180 | color: white;
181 | padding: 14px 28px;
182 | border-radius: 25px;
183 | font-weight: 600;
184 | text-transform: uppercase;
185 | letter-spacing: 1px;
186 | cursor: pointer;
187 | transition: all 0.3s ease;
188 | box-shadow:
189 | 0 0 20px rgba(255, 107, 157, 0.5),
190 | inset 0 0 20px rgba(253, 234, 167, 0.2);
191 | font-family: 'Comfortaa', cursive;
192 | position: relative;
193 | }
194 |
195 | .btn-neon:hover:not(:disabled) {
196 | transform: translateY(-3px) scale(1.02);
197 | box-shadow:
198 | 0 8px 30px rgba(255, 107, 157, 0.7),
199 | inset 0 0 30px rgba(253, 234, 167, 0.3);
200 | text-shadow: 0 0 10px white;
201 | background: linear-gradient(45deg, var(--cat-pink), var(--cat-purple));
202 | animation: cat-wiggle 0.5s ease-out;
203 | }
204 |
205 | @keyframes cat-wiggle {
206 | 0% { transform: translateY(-3px) rotate(0deg); }
207 | 25% { transform: translateY(-3px) rotate(1deg); }
208 | 75% { transform: translateY(-3px) rotate(-1deg); }
209 | 100% { transform: translateY(-3px) rotate(0deg); }
210 | }
211 |
212 | .btn-neon:disabled {
213 | opacity: 0.5;
214 | cursor: not-allowed;
215 | transform: none;
216 | filter: grayscale(30%);
217 | }
218 |
219 | /* Cat food bowl inputs */
220 | .input-neon {
221 | background: rgba(253, 234, 167, 0.9);
222 | border: 3px solid var(--cat-orange);
223 | border-radius: 15px;
224 | color: var(--cat-brown);
225 | padding: 14px 18px;
226 | font-family: 'Comfortaa', cursive;
227 | box-shadow:
228 | 0 0 20px rgba(255, 140, 66, 0.4),
229 | inset 0 0 20px rgba(255, 107, 157, 0.1);
230 | }
231 |
232 | .input-neon:focus {
233 | outline: none;
234 | border-color: var(--cat-pink);
235 | box-shadow:
236 | 0 0 30px rgba(255, 107, 157, 0.6),
237 | inset 0 0 30px rgba(255, 107, 157, 0.2);
238 | background: rgba(253, 234, 167, 1);
239 | }
240 |
241 | .input-neon::placeholder {
242 | color: rgba(141, 73, 37, 0.6);
243 | font-style: italic;
244 | }
245 |
246 | /* Cat treat progress bar */
247 | .progress-neon {
248 | background: rgba(253, 234, 167, 0.9);
249 | border: 3px solid var(--cat-orange);
250 | border-radius: 15px;
251 | height: 24px;
252 | overflow: hidden;
253 | position: relative;
254 | }
255 |
256 | .progress-neon::before {
257 | content: '';
258 | position: absolute;
259 | top: 0;
260 | left: 0;
261 | height: 100%;
262 | background: linear-gradient(90deg, var(--cat-orange), var(--cat-pink));
263 | width: var(--progress, 0%);
264 | transition: width 0.3s ease;
265 | box-shadow: 0 0 15px rgba(255, 107, 157, 0.8);
266 | }
267 |
268 | /* Success/Error states */
269 | .success-neon {
270 | border-color: #2ecc71;
271 | background: rgba(46, 204, 113, 0.1);
272 | box-shadow: 0 0 25px rgba(46, 204, 113, 0.5);
273 | }
274 |
275 | .error-neon {
276 | border-color: #e74c3c;
277 | background: rgba(231, 76, 60, 0.1);
278 | box-shadow: 0 0 25px rgba(231, 76, 60, 0.5);
279 | }
280 |
281 | /* Cat video selection */
282 | .format-option {
283 | background: rgba(253, 234, 167, 0.8);
284 | border: 2px solid var(--cat-cream);
285 | border-radius: 12px;
286 | padding: 10px 14px;
287 | margin: 4px;
288 | cursor: pointer;
289 | transition: all 0.3s ease;
290 | color: var(--cat-brown);
291 | }
292 |
293 | .format-option:hover {
294 | transform: translateY(-2px);
295 | border-color: var(--cat-orange);
296 | background: rgba(253, 234, 167, 1);
297 | }
298 |
299 | .format-option:hover,
300 | .format-option.selected {
301 | border-color: var(--cat-pink);
302 | background: rgba(255, 107, 157, 0.2);
303 | box-shadow: 0 0 15px rgba(255, 107, 157, 0.4);
304 | }
305 |
306 | /* Theme option buttons */
307 | .theme-option {
308 | background: rgba(253, 234, 167, 0.9);
309 | border: 3px solid var(--cat-orange);
310 | border-radius: 15px;
311 | color: var(--cat-brown);
312 | transition: all 0.3s ease;
313 | cursor: pointer;
314 | min-width: 80px;
315 | text-align: center;
316 | font-family: 'Comfortaa', cursive;
317 | font-weight: 600;
318 | position: relative;
319 | z-index: 1000;
320 | }
321 |
322 | .theme-option:hover {
323 | background: rgba(253, 234, 167, 1);
324 | box-shadow: 0 0 20px rgba(255, 140, 66, 0.5);
325 | text-shadow: 0 0 8px var(--cat-orange);
326 | transform: translateY(-2px) scale(1.02);
327 | }
328 |
329 | .theme-option.active {
330 | background: rgba(255, 107, 157, 0.3);
331 | box-shadow: 0 0 25px rgba(255, 107, 157, 0.7);
332 | text-shadow: 0 0 10px var(--cat-pink);
333 | border-color: var(--cat-pink);
334 | }
335 |
336 | /* Scrollbar styling */
337 | ::-webkit-scrollbar {
338 | width: 14px;
339 | }
340 |
341 | ::-webkit-scrollbar-track {
342 | background: var(--cat-cream);
343 | border-radius: 7px;
344 | }
345 |
346 | ::-webkit-scrollbar-thumb {
347 | background: linear-gradient(45deg, var(--cat-orange), var(--cat-pink));
348 | border-radius: 7px;
349 | box-shadow: 0 0 8px rgba(255, 107, 157, 0.4);
350 | }
351 |
352 | /* Responsive Button Layouts */
353 | .theme-buttons-container {
354 | position: relative;
355 | z-index: 1000;
356 | }
357 |
358 | .action-buttons-container {
359 | width: 100%;
360 | }
361 |
362 | .action-buttons-container .btn-neon {
363 | flex: 1;
364 | min-width: 0;
365 | text-align: center;
366 | }
367 |
368 | .download-primary {
369 | min-width: 100px;
370 | flex-shrink: 0;
371 | }
372 |
373 | /* Header spacing adjustments for mobile */
374 | .header-title-section {
375 | margin-top: 1rem;
376 | }
377 |
378 | /* Responsive adjustments */
379 | @media (max-width: 768px) {
380 | h1 {
381 | font-size: 2rem;
382 | letter-spacing: 2px;
383 | }
384 |
385 | .btn-neon {
386 | padding: 8px 16px;
387 | font-size: 0.85rem;
388 | }
389 |
390 | .theme-option {
391 | min-width: 60px;
392 | padding: 8px 12px;
393 | font-size: 0.7rem;
394 | }
395 |
396 | .theme-buttons-container {
397 | margin-bottom: 1rem;
398 | }
399 |
400 | .header-title-section {
401 | margin-top: 0;
402 | }
403 |
404 | .action-buttons-container {
405 | grid-template-columns: 1fr 1fr;
406 | gap: 0.5rem;
407 | }
408 | }
409 |
410 | @media (max-width: 480px) {
411 | .theme-option {
412 | min-width: 50px;
413 | padding: 6px 8px;
414 | font-size: 0.65rem;
415 | }
416 |
417 | .btn-neon {
418 | padding: 6px 12px;
419 | font-size: 0.8rem;
420 | }
421 |
422 | .download-primary {
423 | min-width: 80px;
424 | }
425 | }
426 |
427 | /* Animation for loading states */
428 | @keyframes purr-pulse {
429 | 0%, 100% { opacity: 1; }
430 | 50% { opacity: 0.8; }
431 | }
432 |
433 | .loading {
434 | animation: purr-pulse 1.5s ease-in-out infinite;
435 | }
436 |
437 | /* Close button styling */
438 | .card:hover .opacity-0 {
439 | opacity: 1;
440 | }
441 |
442 | .close-btn {
443 | background: rgba(253, 234, 167, 0.9);
444 | border-radius: 8px;
445 | padding: 6px;
446 | transition: all 0.3s ease;
447 | border: 2px solid var(--cat-orange);
448 | }
449 |
450 | .close-btn:hover {
451 | background: rgba(255, 107, 157, 0.3);
452 | box-shadow: 0 0 12px rgba(255, 107, 157, 0.4);
453 | border-color: var(--cat-pink);
454 | }
455 |
456 | /* Footer styling */
457 | footer {
458 | border-top: 3px solid var(--cat-orange);
459 | background: rgba(253, 234, 167, 0.9);
460 | box-shadow: 0 -8px 20px rgba(255, 140, 66, 0.3);
461 | }
462 |
463 | footer p {
464 | color: var(--cat-brown);
465 | text-shadow: 0 0 5px var(--cat-cream);
466 | }
467 |
468 | /* Cat stretch animation */
469 | @keyframes cat-stretch {
470 | 0%, 100% { transform: scaleX(1); }
471 | 50% { transform: scaleX(1.02); }
472 | }
473 |
474 | .card:hover {
475 | animation: cat-stretch 2s ease-in-out infinite;
476 | }
--------------------------------------------------------------------------------
/static/css/starry-night.css:
--------------------------------------------------------------------------------
1 | /* Starry Night Theme for yt-dlp-co2 */
2 | /* Inspired by Van Gogh's masterpiece "The Starry Night" */
3 | @import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&display=swap');
4 |
5 | :root {
6 | --starry-deep-blue: #1e3a5f;
7 | --starry-night-blue: #2c5f7e;
8 | --starry-cosmic-blue: #4a90a4;
9 | --starry-gold: #ffd700;
10 | --starry-moon: #fff8dc;
11 | --starry-star: #ffffe0;
12 | --starry-cypress: #2f4f2f;
13 | --starry-bg: #0f1419;
14 | --starry-card-bg: rgba(30, 58, 95, 0.85);
15 | --starry-swirl: #87ceeb;
16 | --starry-accent: #daa520;
17 | --starry-glow: #add8e6;
18 | }
19 |
20 | body {
21 | background: radial-gradient(ellipse at center top, var(--starry-night-blue) 0%, var(--starry-deep-blue) 40%, var(--starry-bg) 100%);
22 | color: var(--starry-moon);
23 | font-family: 'Merriweather', serif;
24 | min-height: 100vh;
25 | position: relative;
26 | overflow-x: hidden;
27 | }
28 |
29 | /* Van Gogh swirling sky background with stars */
30 | body::before {
31 | content: '';
32 | position: fixed;
33 | top: 0;
34 | left: 0;
35 | width: 100%;
36 | height: 100%;
37 | background-image:
38 | /* Large swirling patterns */
39 | radial-gradient(ellipse 300px 150px at 70% 20%, rgba(135, 206, 235, 0.15) 0%, transparent 50%),
40 | radial-gradient(ellipse 200px 100px at 30% 30%, rgba(135, 206, 235, 0.12) 0%, transparent 60%),
41 | radial-gradient(ellipse 250px 120px at 85% 40%, rgba(135, 206, 235, 0.1) 0%, transparent 55%),
42 |
43 | /* Cypress tree silhouettes */
44 | radial-gradient(ellipse 40px 200px at 5% 100%, rgba(47, 79, 47, 0.8) 0%, transparent 70%),
45 | radial-gradient(ellipse 30px 180px at 3% 100%, rgba(47, 79, 47, 0.6) 0%, transparent 70%),
46 | radial-gradient(ellipse 25px 160px at 95% 100%, rgba(47, 79, 47, 0.7) 0%, transparent 70%),
47 |
48 | /* Bright stars scattered across the sky */
49 | radial-gradient(circle 3px at 20% 25%, var(--starry-star) 40%, transparent 41%),
50 | radial-gradient(circle 2px at 35% 15%, var(--starry-gold) 50%, transparent 51%),
51 | radial-gradient(circle 4px at 60% 30%, var(--starry-moon) 40%, transparent 41%),
52 | radial-gradient(circle 2px at 75% 18%, var(--starry-star) 50%, transparent 51%),
53 | radial-gradient(circle 3px at 85% 35%, var(--starry-gold) 45%, transparent 46%),
54 | radial-gradient(circle 2px at 15% 45%, var(--starry-star) 50%, transparent 51%),
55 | radial-gradient(circle 3px at 40% 50%, var(--starry-moon) 40%, transparent 41%),
56 | radial-gradient(circle 2px at 65% 55%, var(--starry-gold) 50%, transparent 51%),
57 | radial-gradient(circle 4px at 90% 60%, var(--starry-star) 35%, transparent 36%),
58 | radial-gradient(circle 2px at 25% 70%, var(--starry-star) 50%, transparent 51%),
59 |
60 | /* The prominent moon */
61 | radial-gradient(circle 25px at 80% 15%, var(--starry-moon) 0%, rgba(255, 248, 220, 0.8) 30%, transparent 70%);
62 | animation: cosmic-swirl 25s ease-in-out infinite alternate;
63 | z-index: -2;
64 | }
65 |
66 | /* Floating stardust particles */
67 | #starry-particles {
68 | position: fixed;
69 | top: 0;
70 | left: 0;
71 | width: 100%;
72 | height: 100%;
73 | z-index: -1;
74 | pointer-events: none;
75 | overflow: hidden;
76 | }
77 |
78 | .star-particle {
79 | position: absolute;
80 | width: 2px;
81 | height: 2px;
82 | background: var(--starry-star);
83 | border-radius: 50%;
84 | animation: starry-twinkle linear infinite;
85 | box-shadow: 0 0 6px var(--starry-gold);
86 | }
87 |
88 | @keyframes cosmic-swirl {
89 | 0% {
90 | transform: rotate(0deg) scale(1);
91 | opacity: 0.8;
92 | }
93 | 50% {
94 | transform: rotate(0.5deg) scale(1.01);
95 | opacity: 0.9;
96 | }
97 | 100% {
98 | transform: rotate(1deg) scale(1.02);
99 | opacity: 0.85;
100 | }
101 | }
102 |
103 | @keyframes starry-twinkle {
104 | 0% {
105 | opacity: 0.3;
106 | transform: scale(0.8);
107 | }
108 | 50% {
109 | opacity: 1;
110 | transform: scale(1.2);
111 | box-shadow: 0 0 12px var(--starry-gold);
112 | }
113 | 100% {
114 | opacity: 0.3;
115 | transform: scale(0.8);
116 | }
117 | }
118 |
119 | /* Van Gogh inspired text effects */
120 | h1 {
121 | color: var(--starry-gold);
122 | text-shadow:
123 | 0 0 15px var(--starry-gold),
124 | 0 0 25px var(--starry-moon),
125 | 0 0 35px rgba(255, 215, 0, 0.6),
126 | /* Brushstroke shadow effect */
127 | 3px 3px 0px var(--starry-accent),
128 | -1px -1px 0px var(--starry-cosmic-blue);
129 | font-weight: 900;
130 | text-transform: uppercase;
131 | letter-spacing: 4px;
132 | font-family: 'Merriweather', serif;
133 | animation: celestial-glow 4s ease-in-out infinite;
134 | }
135 |
136 | @keyframes celestial-glow {
137 | 0%, 100% {
138 | text-shadow:
139 | 0 0 15px var(--starry-gold),
140 | 0 0 25px var(--starry-moon),
141 | 3px 3px 0px var(--starry-accent);
142 | }
143 | 50% {
144 | text-shadow:
145 | 0 0 25px var(--starry-gold),
146 | 0 0 35px var(--starry-moon),
147 | 0 0 45px rgba(255, 215, 0, 0.8),
148 | 3px 3px 0px var(--starry-accent);
149 | }
150 | }
151 |
152 | .tagline {
153 | color: var(--starry-glow);
154 | text-shadow:
155 | 0 0 8px var(--starry-swirl),
156 | 0 0 15px var(--starry-cosmic-blue);
157 | font-style: italic;
158 | font-weight: 300;
159 | }
160 |
161 | /* Impressionistic canvas card styling */
162 | .card {
163 | background: var(--starry-card-bg);
164 | border: 3px solid var(--starry-cosmic-blue);
165 | border-radius: 15px;
166 | box-shadow:
167 | 0 0 25px rgba(135, 206, 235, 0.4),
168 | inset 0 0 25px rgba(255, 215, 0, 0.08),
169 | /* Artistic depth */
170 | 0 8px 20px rgba(15, 20, 25, 0.6);
171 | backdrop-filter: blur(8px);
172 | position: relative;
173 | }
174 |
175 | .card::before {
176 | content: '';
177 | position: absolute;
178 | top: -3px;
179 | left: -3px;
180 | right: -3px;
181 | bottom: -3px;
182 | background: linear-gradient(45deg, var(--starry-deep-blue), var(--starry-cosmic-blue), var(--starry-gold));
183 | border-radius: 15px;
184 | z-index: -1;
185 | opacity: 0.3;
186 | }
187 |
188 | /* Van Gogh brushstroke texture overlay */
189 | .card::after {
190 | content: '';
191 | position: absolute;
192 | top: 0;
193 | left: 0;
194 | right: 0;
195 | bottom: 0;
196 | background-image:
197 | /* Horizontal brushstrokes */
198 | repeating-linear-gradient(
199 | 90deg,
200 | transparent 0px,
201 | rgba(135, 206, 235, 0.04) 1px,
202 | transparent 2px,
203 | transparent 12px
204 | ),
205 | /* Diagonal brushstrokes */
206 | repeating-linear-gradient(
207 | 45deg,
208 | transparent 0px,
209 | rgba(255, 215, 0, 0.02) 1px,
210 | transparent 2px,
211 | transparent 16px
212 | );
213 | border-radius: 15px;
214 | pointer-events: none;
215 | }
216 |
217 | /* Celestial button styling */
218 | .btn-neon {
219 | background: linear-gradient(45deg, var(--starry-deep-blue), var(--starry-night-blue));
220 | border: 2px solid var(--starry-gold);
221 | color: var(--starry-moon);
222 | padding: 14px 28px;
223 | border-radius: 20px;
224 | font-weight: 700;
225 | text-transform: uppercase;
226 | letter-spacing: 2px;
227 | cursor: pointer;
228 | transition: all 0.4s ease;
229 | box-shadow:
230 | 0 0 20px rgba(255, 215, 0, 0.4),
231 | inset 0 0 20px rgba(135, 206, 235, 0.1);
232 | font-family: 'Merriweather', serif;
233 | position: relative;
234 | }
235 |
236 | .btn-neon:hover:not(:disabled) {
237 | transform: translateY(-3px) scale(1.02);
238 | box-shadow:
239 | 0 8px 35px rgba(255, 215, 0, 0.7),
240 | inset 0 0 35px rgba(135, 206, 235, 0.2);
241 | text-shadow: 0 0 15px var(--starry-moon);
242 | background: linear-gradient(45deg, var(--starry-cosmic-blue), var(--starry-swirl));
243 | animation: stellar-pulse 0.6s ease-out;
244 | }
245 |
246 | @keyframes stellar-pulse {
247 | 0% { transform: translateY(-3px) scale(1.02); }
248 | 50% {
249 | transform: translateY(-3px) scale(1.05);
250 | box-shadow:
251 | 0 12px 40px rgba(255, 215, 0, 0.9),
252 | inset 0 0 40px rgba(135, 206, 235, 0.3);
253 | }
254 | 100% { transform: translateY(-3px) scale(1.02); }
255 | }
256 |
257 | .btn-neon:disabled {
258 | opacity: 0.4;
259 | cursor: not-allowed;
260 | transform: none;
261 | filter: grayscale(40%);
262 | }
263 |
264 | /* Night sky input styling */
265 | .input-neon {
266 | background: rgba(15, 20, 25, 0.9);
267 | border: 2px solid var(--starry-cosmic-blue);
268 | border-radius: 12px;
269 | color: var(--starry-moon);
270 | padding: 14px 18px;
271 | font-family: 'Merriweather', serif;
272 | box-shadow:
273 | 0 0 20px rgba(135, 206, 235, 0.3),
274 | inset 0 0 20px rgba(135, 206, 235, 0.05);
275 | }
276 |
277 | .input-neon:focus {
278 | outline: none;
279 | border-color: var(--starry-gold);
280 | box-shadow:
281 | 0 0 30px rgba(255, 215, 0, 0.5),
282 | inset 0 0 30px rgba(255, 215, 0, 0.1);
283 | background: rgba(30, 58, 95, 0.9);
284 | }
285 |
286 | .input-neon::placeholder {
287 | color: rgba(173, 216, 230, 0.6);
288 | font-style: italic;
289 | }
290 |
291 | /* Cosmic progress bar */
292 | .progress-neon {
293 | background: rgba(15, 20, 25, 0.9);
294 | border: 2px solid var(--starry-cosmic-blue);
295 | border-radius: 12px;
296 | height: 22px;
297 | overflow: hidden;
298 | position: relative;
299 | }
300 |
301 | .progress-neon::before {
302 | content: '';
303 | position: absolute;
304 | top: 0;
305 | left: 0;
306 | height: 100%;
307 | background: linear-gradient(90deg, var(--starry-cosmic-blue), var(--starry-gold));
308 | width: var(--progress, 0%);
309 | transition: width 0.3s ease;
310 | box-shadow: 0 0 20px rgba(255, 215, 0, 0.8);
311 | }
312 |
313 | /* Success/Error states */
314 | .success-neon {
315 | border-color: var(--starry-glow);
316 | background: rgba(173, 216, 230, 0.1);
317 | box-shadow: 0 0 25px rgba(173, 216, 230, 0.5);
318 | }
319 |
320 | .error-neon {
321 | border-color: #cd5c5c;
322 | background: rgba(205, 92, 92, 0.1);
323 | box-shadow: 0 0 25px rgba(205, 92, 92, 0.5);
324 | }
325 |
326 | /* Video format constellation */
327 | .format-option {
328 | background: rgba(15, 20, 25, 0.7);
329 | border: 2px solid var(--starry-deep-blue);
330 | border-radius: 10px;
331 | padding: 10px 14px;
332 | margin: 4px;
333 | cursor: pointer;
334 | transition: all 0.3s ease;
335 | color: var(--starry-glow);
336 | }
337 |
338 | .format-option:hover {
339 | transform: translateY(-2px);
340 | border-color: var(--starry-cosmic-blue);
341 | background: rgba(30, 58, 95, 0.8);
342 | }
343 |
344 | .format-option:hover,
345 | .format-option.selected {
346 | border-color: var(--starry-gold);
347 | background: rgba(255, 215, 0, 0.15);
348 | box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
349 | }
350 |
351 | /* Theme option buttons */
352 | .theme-option {
353 | background: rgba(15, 20, 25, 0.9);
354 | border: 2px solid var(--starry-cosmic-blue);
355 | border-radius: 12px;
356 | color: var(--starry-gold);
357 | transition: all 0.3s ease;
358 | cursor: pointer;
359 | min-width: 70px;
360 | text-align: center;
361 | font-family: 'Merriweather', serif;
362 | font-weight: 700;
363 | position: relative;
364 | z-index: 1000;
365 | }
366 |
367 | .theme-option:hover {
368 | background: rgba(30, 58, 95, 0.9);
369 | box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
370 | text-shadow: 0 0 10px var(--starry-gold);
371 | transform: translateY(-2px) scale(1.02);
372 | }
373 |
374 | .theme-option.active {
375 | background: rgba(30, 58, 95, 0.9);
376 | box-shadow: 0 0 25px rgba(255, 215, 0, 0.7);
377 | text-shadow: 0 0 12px var(--starry-gold);
378 | border-color: var(--starry-gold);
379 | }
380 |
381 | /* Scrollbar styling */
382 | ::-webkit-scrollbar {
383 | width: 14px;
384 | }
385 |
386 | ::-webkit-scrollbar-track {
387 | background: var(--starry-bg);
388 | border-radius: 7px;
389 | }
390 |
391 | ::-webkit-scrollbar-thumb {
392 | background: linear-gradient(45deg, var(--starry-deep-blue), var(--starry-cosmic-blue));
393 | border-radius: 7px;
394 | box-shadow: 0 0 10px rgba(135, 206, 235, 0.4);
395 | }
396 |
397 | /* Responsive Button Layouts */
398 | .theme-buttons-container {
399 | position: relative;
400 | z-index: 1000;
401 | }
402 |
403 | .action-buttons-container {
404 | width: 100%;
405 | }
406 |
407 | .action-buttons-container .btn-neon {
408 | flex: 1;
409 | min-width: 0;
410 | text-align: center;
411 | }
412 |
413 | .download-primary {
414 | min-width: 100px;
415 | flex-shrink: 0;
416 | }
417 |
418 | /* Header spacing adjustments for mobile */
419 | .header-title-section {
420 | margin-top: 1rem;
421 | }
422 |
423 | /* Responsive adjustments */
424 | @media (max-width: 768px) {
425 | h1 {
426 | font-size: 2rem;
427 | letter-spacing: 2px;
428 | }
429 |
430 | .btn-neon {
431 | padding: 8px 16px;
432 | font-size: 0.85rem;
433 | }
434 |
435 | .theme-option {
436 | min-width: 60px;
437 | padding: 8px 12px;
438 | font-size: 0.7rem;
439 | }
440 |
441 | .theme-buttons-container {
442 | margin-bottom: 1rem;
443 | }
444 |
445 | .header-title-section {
446 | margin-top: 0;
447 | }
448 |
449 | .action-buttons-container {
450 | grid-template-columns: 1fr 1fr;
451 | gap: 0.5rem;
452 | }
453 | }
454 |
455 | @media (max-width: 480px) {
456 | .theme-option {
457 | min-width: 50px;
458 | padding: 6px 8px;
459 | font-size: 0.65rem;
460 | }
461 |
462 | .btn-neon {
463 | padding: 6px 12px;
464 | font-size: 0.8rem;
465 | }
466 |
467 | .download-primary {
468 | min-width: 80px;
469 | }
470 | }
471 |
472 | /* Animation for loading states */
473 | @keyframes starry-pulse {
474 | 0%, 100% { opacity: 1; }
475 | 50% { opacity: 0.7; }
476 | }
477 |
478 | .loading {
479 | animation: starry-pulse 2s ease-in-out infinite;
480 | }
481 |
482 | /* Close button styling */
483 | .card:hover .opacity-0 {
484 | opacity: 1;
485 | }
486 |
487 | .close-btn {
488 | background: rgba(15, 20, 25, 0.9);
489 | border-radius: 8px;
490 | padding: 6px;
491 | transition: all 0.3s ease;
492 | border: 2px solid var(--starry-deep-blue);
493 | }
494 |
495 | .close-btn:hover {
496 | background: rgba(30, 58, 95, 0.9);
497 | box-shadow: 0 0 12px rgba(255, 215, 0, 0.4);
498 | border-color: var(--starry-cosmic-blue);
499 | }
500 |
501 | /* Footer styling */
502 | footer {
503 | border-top: 2px solid var(--starry-cosmic-blue);
504 | background: rgba(15, 20, 25, 0.9);
505 | box-shadow: 0 -8px 20px rgba(135, 206, 235, 0.3);
506 | }
507 |
508 | footer p {
509 | color: var(--starry-glow);
510 | text-shadow: 0 0 8px var(--starry-cosmic-blue);
511 | }
512 |
513 | /* Van Gogh swirling animation for cards */
514 | @keyframes van-gogh-swirl {
515 | 0%, 100% {
516 | box-shadow: 0 0 15px var(--starry-cosmic-blue);
517 | transform: rotate(0deg);
518 | }
519 | 25% {
520 | box-shadow: 0 0 25px var(--starry-gold), 0 0 35px var(--starry-swirl);
521 | transform: rotate(0.2deg);
522 | }
523 | 50% {
524 | box-shadow: 0 0 20px var(--starry-glow), 0 0 30px var(--starry-moon);
525 | transform: rotate(0deg);
526 | }
527 | 75% {
528 | box-shadow: 0 0 25px var(--starry-accent), 0 0 35px var(--starry-cosmic-blue);
529 | transform: rotate(-0.2deg);
530 | }
531 | }
532 |
533 | .card:hover {
534 | animation: van-gogh-swirl 4s ease-in-out infinite;
535 | }
--------------------------------------------------------------------------------
/static/js/app.js:
--------------------------------------------------------------------------------
1 | // Theme Management
2 | function themeManager() {
3 | return {
4 | currentTheme: localStorage.getItem('theme') || 'vaporwave',
5 |
6 | init() {
7 | this.applyTheme(this.currentTheme);
8 | },
9 |
10 | toggleTheme() {
11 | this.currentTheme = this.currentTheme === 'vaporwave' ? 'matrix' : 'vaporwave';
12 | this.applyTheme(this.currentTheme);
13 | localStorage.setItem('theme', this.currentTheme);
14 | },
15 |
16 | applyTheme(theme) {
17 | const existingThemeLink = document.getElementById('theme-css');
18 |
19 | if (existingThemeLink) {
20 | const newHref = theme === 'matrix' ? '/static/css/matrix.css' : '/static/css/vaporwave.css';
21 | existingThemeLink.href = newHref;
22 | }
23 | }
24 | }
25 | }
26 |
27 | // Matrix Rain Effect
28 | function createMatrixRain() {
29 | const matrixContainer = document.getElementById('matrix-rain');
30 | if (!matrixContainer) return;
31 |
32 | matrixContainer.innerHTML = '';
33 |
34 | const columnWidth = 20;
35 | const columns = Math.floor(window.innerWidth / columnWidth);
36 |
37 | for (let i = 0; i < columns; i++) {
38 | const column = document.createElement('div');
39 | column.className = 'matrix-column';
40 | column.style.left = i * columnWidth + 'px';
41 | column.style.animationDuration = (Math.random() * 3 + 2) + 's';
42 | column.style.animationDelay = Math.random() * 2 + 's';
43 |
44 | const charCount = 7;
45 | const binaryString = Array.from({length: 7}, () => Math.random() > 0.5 ? '1' : '0');
46 |
47 | for (let j = 0; j < charCount; j++) {
48 | const char = document.createElement('span');
49 | char.className = 'char';
50 | char.textContent = binaryString[j];
51 |
52 | if (j > charCount - 2) {
53 | char.classList.add('fade');
54 | }
55 |
56 | column.appendChild(char);
57 | }
58 |
59 | matrixContainer.appendChild(column);
60 | }
61 |
62 | setInterval(() => {
63 | const columns = matrixContainer.querySelectorAll('.matrix-column');
64 | columns.forEach(column => {
65 | if (Math.random() > 0.98) {
66 | const chars = column.querySelectorAll('.char');
67 | const newBinaryString = Array.from({length: 7}, () => Math.random() > 0.5 ? '1' : '0');
68 | chars.forEach((char, index) => {
69 | char.textContent = newBinaryString[index];
70 | });
71 | }
72 | });
73 | }, 200);
74 | }
75 |
76 | function stopMatrixRain() {
77 | const matrixContainer = document.getElementById('matrix-rain');
78 | if (matrixContainer) {
79 | matrixContainer.innerHTML = '';
80 | }
81 | }
82 |
83 | function createAudioVisualizer() {
84 | let visualizerContainer = document.getElementById('audio-visualizer');
85 | if (!visualizerContainer) {
86 | visualizerContainer = document.createElement('div');
87 | visualizerContainer.id = 'audio-visualizer';
88 | document.body.appendChild(visualizerContainer);
89 | }
90 |
91 | visualizerContainer.innerHTML = '';
92 |
93 | const barWidth = 6;
94 | const barCount = Math.floor(window.innerWidth / (barWidth + 2));
95 |
96 | for (let i = 0; i < barCount; i++) {
97 | const bar = document.createElement('div');
98 | bar.className = 'audio-bar';
99 | bar.style.left = i * (barWidth + 2) + 'px';
100 | bar.style.width = barWidth + 'px';
101 |
102 | bar.style.animationDuration = (Math.random() * 1.5 + 0.5) + 's';
103 | bar.style.animationDelay = Math.random() * 2 + 's';
104 | const hue = (i / barCount) * 360;
105 | bar.style.background = `linear-gradient(to top, hsl(${hue}, 70%, 50%), hsl(${hue + 60}, 80%, 60%))`;
106 | bar.style.boxShadow = `0 0 8px hsl(${hue}, 70%, 50%)`;
107 |
108 | visualizerContainer.appendChild(bar);
109 | }
110 |
111 | setInterval(() => {
112 | const bars = visualizerContainer.querySelectorAll('.audio-bar');
113 | bars.forEach((bar, index) => {
114 | const baseHeight = Math.random() * 80 + 10;
115 | const beatIntensity = Math.sin(Date.now() * 0.001 + index * 0.1) * 30 + 30;
116 | bar.style.height = (baseHeight + beatIntensity) + 'px';
117 | });
118 | }, 150);
119 | }
120 |
121 | function stopAudioVisualizer() {
122 | const visualizerContainer = document.getElementById('audio-visualizer');
123 | if (visualizerContainer) {
124 | visualizerContainer.innerHTML = '';
125 | }
126 | }
127 |
128 | function createCatToys() {
129 | let toyContainer = document.getElementById('cat-toys');
130 | if (!toyContainer) {
131 | toyContainer = document.createElement('div');
132 | toyContainer.id = 'cat-toys';
133 | document.body.appendChild(toyContainer);
134 | }
135 |
136 | // Clear existing toys
137 | toyContainer.innerHTML = '';
138 |
139 | const funObjects = ['🐭', '🧶', '🐟', '🦴', '🏀', '🎾', '🪶', '🧸'];
140 |
141 | // Create initial floating objects
142 | for (let i = 0; i < 8; i++) {
143 | const toy = document.createElement('div');
144 | toy.className = 'cat-toy';
145 | toy.textContent = funObjects[Math.floor(Math.random() * funObjects.length)];
146 | toy.style.left = Math.random() * 100 + '%';
147 | toy.style.animationDuration = (Math.random() * 8 + 6) + 's';
148 | toy.style.animationDelay = Math.random() * 4 + 's';
149 |
150 | toyContainer.appendChild(toy);
151 | }
152 |
153 | // Continuously add new floating objects
154 | setInterval(() => {
155 | if (document.getElementById('cat-toys')) {
156 | const toy = document.createElement('div');
157 | toy.className = 'cat-toy';
158 | toy.textContent = funObjects[Math.floor(Math.random() * funObjects.length)];
159 | toy.style.left = Math.random() * 100 + '%';
160 | toy.style.animationDuration = (Math.random() * 8 + 6) + 's';
161 |
162 | toyContainer.appendChild(toy);
163 |
164 | // Remove object after animation
165 | setTimeout(() => {
166 | if (toy.parentNode) {
167 | toy.parentNode.removeChild(toy);
168 | }
169 | }, 14000);
170 | }
171 | }, 2000);
172 | }
173 |
174 | function stopCatToys() {
175 | const toyContainer = document.getElementById('cat-toys');
176 | if (toyContainer) {
177 | toyContainer.remove();
178 | }
179 | }
180 |
181 | // Starry Night Particles Effect
182 | function createStarryParticles() {
183 | let particleContainer = document.getElementById('starry-particles');
184 | if (!particleContainer) {
185 | particleContainer = document.createElement('div');
186 | particleContainer.id = 'starry-particles';
187 | document.body.appendChild(particleContainer);
188 | }
189 |
190 | // Clear existing particles
191 | particleContainer.innerHTML = '';
192 |
193 | // Create twinkling stars
194 | for (let i = 0; i < 50; i++) {
195 | const particle = document.createElement('div');
196 | particle.className = 'star-particle';
197 | particle.style.left = Math.random() * 100 + '%';
198 | particle.style.top = Math.random() * 100 + '%';
199 | particle.style.animationDuration = (Math.random() * 3 + 2) + 's';
200 | particle.style.animationDelay = Math.random() * 3 + 's';
201 |
202 | // Vary star sizes and intensities
203 | const size = Math.random() * 2 + 1;
204 | particle.style.width = size + 'px';
205 | particle.style.height = size + 'px';
206 |
207 | // Some stars are brighter
208 | if (Math.random() > 0.8) {
209 | particle.style.boxShadow = '0 0 12px #ffd700';
210 | }
211 |
212 | particleContainer.appendChild(particle);
213 | }
214 |
215 | // Add some larger, slower-moving celestial bodies
216 | for (let i = 0; i < 8; i++) {
217 | const celestial = document.createElement('div');
218 | celestial.className = 'star-particle';
219 | celestial.style.left = Math.random() * 100 + '%';
220 | celestial.style.top = Math.random() * 60 + '%'; // Keep in upper part of sky
221 | celestial.style.width = (Math.random() * 4 + 3) + 'px';
222 | celestial.style.height = (Math.random() * 4 + 3) + 'px';
223 | celestial.style.animationDuration = (Math.random() * 6 + 4) + 's';
224 | celestial.style.animationDelay = Math.random() * 4 + 's';
225 | celestial.style.background = '#fff8dc';
226 | celestial.style.boxShadow = '0 0 15px #ffd700, 0 0 25px #fff8dc';
227 |
228 | particleContainer.appendChild(celestial);
229 | }
230 | }
231 |
232 | function stopStarryParticles() {
233 | const particleContainer = document.getElementById('starry-particles');
234 | if (particleContainer) {
235 | particleContainer.innerHTML = '';
236 | }
237 | }
238 |
239 | // No animated carbonation effects - keeping it simple and fast
240 | function createCarbonationBubbles() {
241 | // No animated bubbles - just static background pattern
242 | }
243 |
244 | function stopCarbonationBubbles() {
245 | // No intervals to clear
246 | }
247 |
248 |
249 | // Direct theme selection function
250 | function setTheme(themeName) {
251 |
252 | const themeLink = document.getElementById('theme-css');
253 |
254 | if (themeLink) {
255 | if (themeName === 'matrix') {
256 | themeLink.href = '/static/css/matrix.css';
257 | } else if (themeName === 'kelethin') {
258 | themeLink.href = '/static/css/kelethin.css';
259 | } else if (themeName === 'music') {
260 | themeLink.href = '/static/css/music.css';
261 | } else if (themeName === 'fun') {
262 | themeLink.href = '/static/css/fun.css';
263 | } else if (themeName === 'starry-night') {
264 | themeLink.href = '/static/css/starry-night.css';
265 | } else if (themeName === 'carbonation') {
266 | themeLink.href = '/static/css/carbonation.css';
267 | } else {
268 | themeLink.href = '/static/css/vaporwave.css';
269 | }
270 | }
271 |
272 | // Handle special effects - stop all first
273 | stopMatrixRain();
274 | stopAudioVisualizer();
275 | stopCatToys();
276 | stopStarryParticles();
277 | stopCarbonationBubbles();
278 |
279 | // Start appropriate effect
280 | if (themeName === 'matrix') {
281 | setTimeout(() => createMatrixRain(), 100);
282 | } else if (themeName === 'music') {
283 | setTimeout(() => createAudioVisualizer(), 100);
284 | } else if (themeName === 'fun') {
285 | setTimeout(() => createCatToys(), 100);
286 | } else if (themeName === 'starry-night') {
287 | setTimeout(() => createStarryParticles(), 100);
288 | } else if (themeName === 'carbonation') {
289 | setTimeout(() => createCarbonationBubbles(), 100);
290 | }
291 |
292 | // Update active button state
293 | document.querySelectorAll('.theme-option').forEach(btn => {
294 | btn.classList.remove('active');
295 | });
296 | document.getElementById(`theme-${themeName}`)?.classList.add('active');
297 |
298 | localStorage.setItem('theme', themeName);
299 | }
300 |
301 | // WebSocket Connection for Progress Updates
302 | document.addEventListener('DOMContentLoaded', function() {
303 | // Initialize theme buttons
304 | document.querySelectorAll('.theme-option').forEach(button => {
305 | button.addEventListener('click', function() {
306 | const themeName = this.getAttribute('data-theme');
307 | setTheme(themeName);
308 | });
309 | });
310 |
311 | // Make title and tagline clickable to apply CARBONATION theme
312 | const titleElement = document.getElementById('title-carbonation');
313 | const taglineElement = document.getElementById('tagline-carbonation');
314 |
315 | if (titleElement) {
316 | titleElement.addEventListener('click', function() {
317 | setTheme('carbonation');
318 | });
319 | }
320 |
321 | if (taglineElement) {
322 | taglineElement.addEventListener('click', function() {
323 | setTheme('carbonation');
324 | });
325 | }
326 |
327 | // Apply stored theme on load - CARBONATION is now the default!
328 | const currentTheme = localStorage.getItem('theme') || 'carbonation';
329 | setTheme(currentTheme);
330 |
331 | // Use current hostname for WebSocket connection
332 | const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
333 | const ws = new WebSocket(`${protocol}//${window.location.host}/ws/progress`);
334 |
335 | ws.onopen = function() {
336 | // WebSocket connected
337 | };
338 |
339 | ws.onerror = function(error) {
340 | // WebSocket error handling
341 | };
342 |
343 | ws.onclose = function() {
344 | // WebSocket disconnected
345 | };
346 |
347 | ws.onmessage = function(event) {
348 | const data = JSON.parse(event.data);
349 | const progressElement = document.getElementById(`progress-${data.download_id}`);
350 |
351 | if (progressElement) {
352 | if (data.status === 'downloading') {
353 | progressElement.innerHTML = `
354 |
355 | Downloading: ${data.filename || 'Unknown'}
356 | ${data.percentage}
357 |
358 |
359 | Speed: ${data.speed}
360 | ETA: ${data.eta}
361 |
362 | `;
363 | } else if (data.status === 'completed') {
364 | const downloadDiv = document.getElementById(`download-${data.download_id}`);
365 | if (downloadDiv) {
366 | downloadDiv.className = 'success-neon card p-4 mb-4 relative group';
367 | downloadDiv.innerHTML = `
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
Download completed: ${data.filename}
376 |
377 | `;
378 | }
379 | } else if (data.status === 'error') {
380 | const downloadDiv = document.getElementById(`download-${data.download_id}`);
381 | if (downloadDiv) {
382 | downloadDiv.className = 'error-neon card p-4 mb-4 relative group';
383 | downloadDiv.innerHTML = `
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
Download failed: ${data.error}
392 |
393 | `;
394 | }
395 | }
396 | }
397 | };
398 | });
--------------------------------------------------------------------------------
/app/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Form, Request
2 | from fastapi.responses import HTMLResponse, FileResponse
3 | from fastapi.staticfiles import StaticFiles
4 | import asyncio
5 | import json
6 | import os
7 | from pathlib import Path
8 | import yt_dlp
9 | from typing import Dict, Any
10 | import uuid
11 | import logging
12 | from .options import convert_to_ydl_opts, get_options_by_category, YT_DLP_OPTIONS, OptionType, OptionCategory
13 |
14 | logging.basicConfig(level=logging.INFO)
15 | logger = logging.getLogger(__name__)
16 |
17 | app = FastAPI(title="yt-dlp-co2", description="Modern web interface for yt-dlp")
18 |
19 | # Mount static files
20 | app.mount("/static", StaticFiles(directory="static"), name="static")
21 |
22 | HTML_FILE = Path("index.html")
23 | active_downloads: Dict[str, Dict[str, Any]] = {}
24 | connected_websockets = []
25 | DOWNLOAD_DIR = Path("/app/downloads")
26 | DOWNLOAD_DIR.mkdir(exist_ok=True)
27 |
28 | class WebSocketProgressHook:
29 | def __init__(self, download_id: str):
30 | self.download_id = download_id
31 |
32 | def __call__(self, d):
33 | if d['status'] == 'downloading':
34 | progress_data = {
35 | 'download_id': self.download_id,
36 | 'status': 'downloading',
37 | 'percentage': d.get('_percent_str', '0%'),
38 | 'speed': d.get('_speed_str', 'N/A'),
39 | 'eta': d.get('_eta_str', 'N/A'),
40 | 'filename': d.get('filename', 'Unknown')
41 | }
42 | elif d['status'] == 'finished':
43 | progress_data = {
44 | 'download_id': self.download_id,
45 | 'status': 'completed',
46 | 'filename': d.get('filename', 'Unknown')
47 | }
48 | else:
49 | progress_data = {
50 | 'download_id': self.download_id,
51 | 'status': d['status'],
52 | 'message': str(d)
53 | }
54 |
55 | download_id = self.download_id
56 | if download_id not in active_downloads:
57 | active_downloads[download_id] = {}
58 | active_downloads[download_id]['progress'] = progress_data
59 |
60 | async def broadcast_progress(data):
61 | if connected_websockets:
62 | message = json.dumps(data)
63 | disconnected = []
64 | for websocket in connected_websockets:
65 | try:
66 | await websocket.send_text(message)
67 | except:
68 | disconnected.append(websocket)
69 |
70 | for ws in disconnected:
71 | connected_websockets.remove(ws)
72 |
73 | @app.get("/", response_class=HTMLResponse)
74 | async def home():
75 | return FileResponse(HTML_FILE)
76 |
77 | @app.post("/download", response_class=HTMLResponse)
78 | async def download_video(request: Request):
79 | form_data = await request.form()
80 | url = form_data.get("url")
81 | format_id = form_data.get("format_id")
82 | batch_file = form_data.get("batchfile")
83 |
84 | # Handle batch file upload
85 | if batch_file and hasattr(batch_file, 'read'):
86 | try:
87 | batch_content = await batch_file.read()
88 | urls = [line.strip() for line in batch_content.decode('utf-8').splitlines() if line.strip()]
89 |
90 | if not urls:
91 | return HTMLResponse(content="Batch file contains no valid URLs
", status_code=400)
92 |
93 | # Process batch download
94 | download_id = str(uuid.uuid4())
95 | asyncio.create_task(perform_batch_download(download_id, urls, format_id,
96 | {k: v for k, v in form_data.items() if k not in ["batchfile", "format_id"] and v}))
97 |
98 | html_response = f'''
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
Batch download started: {len(urls)} URLs
110 |
111 |
112 | Processing batch file...
113 |
114 |
'''
115 | return HTMLResponse(content=html_response)
116 |
117 | except Exception as e:
118 | return HTMLResponse(content=f"Batch file error: {str(e)}
", status_code=400)
119 |
120 | if not url:
121 | return HTMLResponse(content="URL is required
", status_code=400)
122 |
123 | # Convert form data to options dict
124 | options_dict = {}
125 | for key, value in form_data.items():
126 | if key not in ["url", "format_id"] and value:
127 | # Handle multi-select values (comma-separated)
128 | if "," in str(value):
129 | options_dict[key] = [v.strip() for v in str(value).split(",")]
130 | else:
131 | options_dict[key] = value
132 |
133 | download_id = str(uuid.uuid4())
134 |
135 | try:
136 | logger.info(f"Creating download task for {download_id}")
137 | task = asyncio.create_task(perform_download(download_id, url, format_id, options_dict))
138 | logger.info(f"Download task created successfully for {download_id}")
139 | except Exception as e:
140 | logger.error(f"Failed to create download task: {e}")
141 | safe_url = str(url).replace('<', '<').replace('>', '>').replace('"', '"')
142 |
143 | # Show options count if any advanced options are set
144 | options_count = len([k for k, v in options_dict.items() if v])
145 | options_text = f" ({options_count} options)" if options_count > 0 else ""
146 |
147 | html_response = f'''
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
Download started{options_text}: {safe_url}
159 |
160 |
161 | Initializing...
162 |
163 |
'''
164 |
165 | return HTMLResponse(content=html_response)
166 |
167 | def get_quality_string(format_info):
168 | """Generate human-readable quality string like the UI formats endpoint"""
169 | if format_info.get('height'):
170 | return f"{format_info['height']}p"
171 | elif format_info.get('abr'):
172 | return f"{format_info['abr']}kbps"
173 | else:
174 | return "unknown"
175 |
176 | async def perform_download(download_id: str, url: str, format_id: str = None, options_dict: Dict[str, Any] = None):
177 | try:
178 | # Extract video info once for all checks
179 | info_opts = {
180 | 'quiet': True,
181 | 'no_warnings': True,
182 | }
183 |
184 | info = None
185 | expected_path = None
186 |
187 | try:
188 | # First get video info to generate human-readable filename
189 | info_opts = {
190 | 'quiet': True,
191 | 'no_warnings': True,
192 | }
193 |
194 | # Add format if specified
195 | if format_id:
196 | info_opts['format'] = format_id
197 |
198 | # Convert and merge user options
199 | if options_dict:
200 | user_opts = convert_to_ydl_opts(options_dict)
201 | info_opts.update(user_opts)
202 |
203 | with yt_dlp.YoutubeDL(info_opts) as ydl:
204 | info = await asyncio.get_event_loop().run_in_executor(None, ydl.extract_info, url, False)
205 |
206 | # Find the selected format to get quality info
207 | selected_format = None
208 | if format_id and 'formats' in info:
209 | for f in info['formats']:
210 | if f['format_id'] == format_id:
211 | selected_format = f
212 | break
213 |
214 | # Generate quality string like the UI does
215 | if selected_format:
216 | quality_str = get_quality_string(selected_format)
217 | # Create filename with readable quality
218 | filename = f"{info.get('title', 'Unknown')} [{quality_str}].{selected_format.get('ext', 'webm')}"
219 | else:
220 | # Fallback to format_id if we can't find the format
221 | ext = info.get('ext', 'webm')
222 | filename = f"{info.get('title', 'Unknown')} [{format_id or 'default'}].{ext}"
223 |
224 | expected_path = DOWNLOAD_DIR / filename
225 |
226 | # Only check if the exact expected file exists - no fuzzy matching
227 | file_found = None
228 | if expected_path.exists():
229 | file_found = expected_path
230 |
231 | if file_found:
232 |
233 | # Create the entry in active_downloads and mark as skipped
234 | active_downloads[download_id] = {
235 | 'url': url,
236 | 'status': 'skipped',
237 | 'format_id': None,
238 | 'options': {}
239 | }
240 |
241 | skip_data = {
242 | 'download_id': download_id,
243 | 'status': 'completed',
244 | 'message': f'Already downloaded',
245 | 'filename': file_found.name
246 | }
247 | await broadcast_progress(skip_data)
248 | logger.info(f"Detected duplicate download {download_id}: {file_found.name}")
249 | return
250 |
251 | except Exception as e:
252 | # If we can't check, proceed with download
253 | logger.warning(f"Could not check for existing files: {str(e)}")
254 |
255 | # Use the same human-readable filename for the actual download
256 | human_readable_template = str(expected_path) if expected_path else str(DOWNLOAD_DIR / '%(title)s [%(format_id)s].%(ext)s')
257 |
258 | # Base options
259 | ydl_opts = {
260 | 'outtmpl': human_readable_template,
261 | 'progress_hooks': [WebSocketProgressHook(download_id)],
262 | 'no_overwrites': True, # This will help us detect duplicates
263 | }
264 |
265 | # Add format if specified
266 | if format_id:
267 | ydl_opts['format'] = format_id
268 |
269 | # Convert and merge user options
270 | if options_dict:
271 | user_opts = convert_to_ydl_opts(options_dict)
272 | ydl_opts.update(user_opts)
273 | logger.info(f"Download {download_id} using options: {list(user_opts.keys())}")
274 |
275 | active_downloads[download_id] = {
276 | 'url': url,
277 | 'status': 'downloading',
278 | 'format_id': format_id,
279 | 'options': options_dict or {}
280 | }
281 |
282 |
283 | with yt_dlp.YoutubeDL(ydl_opts) as ydl:
284 | try:
285 | result = await asyncio.get_event_loop().run_in_executor(None, ydl.download, [url])
286 |
287 | except Exception as download_error:
288 | logger.error(f"Download failed for {download_id}: {download_error}")
289 | raise download_error
290 |
291 | active_downloads[download_id]['status'] = 'completed'
292 |
293 | except Exception as e:
294 | logger.error(f"Download error for {download_id}: {e}")
295 | # Ensure the download entry exists before setting error status
296 | if download_id not in active_downloads:
297 | active_downloads[download_id] = {'url': url, 'status': 'error', 'format_id': format_id, 'options': options_dict or {}}
298 | else:
299 | active_downloads[download_id]['status'] = 'error'
300 | active_downloads[download_id]['error'] = str(e)
301 |
302 | error_data = {
303 | 'download_id': download_id,
304 | 'status': 'error',
305 | 'error': str(e)
306 | }
307 | await broadcast_progress(error_data)
308 |
309 | async def perform_batch_download(download_id: str, urls: list, format_id: str = None, options_dict: Dict[str, Any] = None):
310 | """Process batch download of multiple URLs"""
311 | try:
312 | total_urls = len(urls)
313 | completed = 0
314 |
315 | for i, url in enumerate(urls, 1):
316 | try:
317 | # Base options
318 | ydl_opts = {
319 | 'outtmpl': str(DOWNLOAD_DIR / '%(title)s [%(format_id)s].%(ext)s'),
320 | 'progress_hooks': [WebSocketProgressHook(download_id)],
321 | }
322 |
323 | # Add format if specified
324 | if format_id:
325 | ydl_opts['format'] = format_id
326 |
327 | # Convert and merge user options
328 | if options_dict:
329 | user_opts = convert_to_ydl_opts(options_dict)
330 | ydl_opts.update(user_opts)
331 |
332 | # Update progress
333 | progress_data = {
334 | 'download_id': download_id,
335 | 'status': 'downloading',
336 | 'message': f'Processing URL {i}/{total_urls}: {url[:50]}...',
337 | 'batch_progress': f'{completed}/{total_urls}'
338 | }
339 | await broadcast_progress(progress_data)
340 |
341 | with yt_dlp.YoutubeDL(ydl_opts) as ydl:
342 | await asyncio.get_event_loop().run_in_executor(None, ydl.download, [url])
343 |
344 | completed += 1
345 | logger.info(f"Batch download {download_id}: completed {url}")
346 |
347 | except Exception as e:
348 | logger.error(f"Batch download {download_id}: error with {url}: {e}")
349 | error_data = {
350 | 'download_id': download_id,
351 | 'status': 'warning',
352 | 'message': f'Failed URL {i}/{total_urls}: {str(e)[:100]}'
353 | }
354 | await broadcast_progress(error_data)
355 | continue
356 |
357 | # Final completion status
358 | active_downloads[download_id]['status'] = 'completed'
359 | completion_data = {
360 | 'download_id': download_id,
361 | 'status': 'completed',
362 | 'message': f'Batch completed: {completed}/{total_urls} successful'
363 | }
364 | await broadcast_progress(completion_data)
365 |
366 | except Exception as e:
367 | logger.error(f"Batch download error for {download_id}: {e}")
368 | # Ensure the download entry exists before setting error status
369 | if download_id not in active_downloads:
370 | active_downloads[download_id] = {'url': 'batch', 'status': 'error', 'format_id': format_id, 'options': options_dict or {}}
371 | else:
372 | active_downloads[download_id]['status'] = 'error'
373 | active_downloads[download_id]['error'] = str(e)
374 |
375 | error_data = {
376 | 'download_id': download_id,
377 | 'status': 'error',
378 | 'error': str(e)
379 | }
380 | await broadcast_progress(error_data)
381 |
382 | @app.get("/formats/{url:path}")
383 | async def get_formats(url: str):
384 | try:
385 | ydl_opts = {
386 | 'quiet': True,
387 | 'no_warnings': True,
388 | }
389 |
390 | with yt_dlp.YoutubeDL(ydl_opts) as ydl:
391 | info = ydl.extract_info(url, download=False)
392 |
393 | formats = []
394 | if 'formats' in info:
395 | for f in info['formats']:
396 | if f.get('height'):
397 | quality = f"{f['height']}p"
398 | elif f.get('abr'):
399 | quality = f"{f['abr']}kbps"
400 | else:
401 | quality = "Unknown"
402 |
403 | formats.append({
404 | 'format_id': f['format_id'],
405 | 'ext': f.get('ext', 'unknown'),
406 | 'quality': quality,
407 | 'filesize': f.get('filesize'),
408 | 'vcodec': f.get('vcodec', 'none'),
409 | 'acodec': f.get('acodec', 'none')
410 | })
411 |
412 | return {
413 | 'title': info.get('title', 'Unknown'),
414 | 'duration': info.get('duration'),
415 | 'formats': formats[:20]
416 | }
417 |
418 | except Exception as e:
419 | logger.error(f"Format extraction error: {e}")
420 | return {'error': str(e)}
421 |
422 | @app.get("/info/{url:path}")
423 | async def get_video_info(url: str, info_type: str = "basic"):
424 | """Extract video information without downloading"""
425 | try:
426 | ydl_opts = {
427 | 'quiet': True,
428 | 'no_warnings': True,
429 | }
430 |
431 | with yt_dlp.YoutubeDL(ydl_opts) as ydl:
432 | info = ydl.extract_info(url, download=False)
433 |
434 | if info_type == "formats":
435 | formats = []
436 | if 'formats' in info:
437 | for f in info['formats']:
438 | formats.append({
439 | 'format_id': f['format_id'],
440 | 'ext': f.get('ext', 'unknown'),
441 | 'quality': f"{f['height']}p" if f.get('height') else f"{f.get('abr', 'unknown')}kbps",
442 | 'filesize': f.get('filesize'),
443 | 'vcodec': f.get('vcodec', 'none'),
444 | 'acodec': f.get('acodec', 'none'),
445 | 'fps': f.get('fps'),
446 | 'tbr': f.get('tbr')
447 | })
448 | return {'formats': formats}
449 |
450 | elif info_type == "subtitles":
451 | subs = info.get('subtitles', {})
452 | auto_subs = info.get('automatic_captions', {})
453 | return {
454 | 'subtitles': subs,
455 | 'automatic_captions': auto_subs,
456 | 'available_languages': list(set(list(subs.keys()) + list(auto_subs.keys())))
457 | }
458 |
459 | elif info_type == "thumbnails":
460 | thumbnails = info.get('thumbnails', [])
461 | return {
462 | 'thumbnails': [
463 | {
464 | 'id': t.get('id'),
465 | 'url': t.get('url'),
466 | 'width': t.get('width'),
467 | 'height': t.get('height')
468 | } for t in thumbnails
469 | ]
470 | }
471 |
472 | else: # basic info
473 | return {
474 | 'title': info.get('title', 'Unknown'),
475 | 'uploader': info.get('uploader', 'Unknown'),
476 | 'duration': info.get('duration'),
477 | 'description': info.get('description', ''),
478 | 'view_count': info.get('view_count'),
479 | 'upload_date': info.get('upload_date'),
480 | 'webpage_url': info.get('webpage_url'),
481 | 'thumbnail': info.get('thumbnail'),
482 | 'tags': info.get('tags', []),
483 | 'categories': info.get('categories', [])
484 | }
485 |
486 | except Exception as e:
487 | logger.error(f"Info extraction error: {e}")
488 | return {'error': str(e)}
489 |
490 | @app.get("/search/{query}")
491 | async def search_videos(query: str, search_type: str = "ytsearch", max_results: int = 10):
492 | """Search for videos using yt-dlp search functionality"""
493 | try:
494 | search_query = f"{search_type}{max_results}:{query}"
495 |
496 | ydl_opts = {
497 | 'quiet': True,
498 | 'no_warnings': True,
499 | 'extract_flat': True # Only get basic info, don't extract full details
500 | }
501 |
502 | with yt_dlp.YoutubeDL(ydl_opts) as ydl:
503 | info = ydl.extract_info(search_query, download=False)
504 |
505 | results = []
506 | if info and 'entries' in info:
507 | for entry in info['entries']:
508 | if entry:
509 | results.append({
510 | 'title': entry.get('title', 'Unknown'),
511 | 'url': entry.get('url', ''),
512 | 'id': entry.get('id', ''),
513 | 'duration': entry.get('duration'),
514 | 'uploader': entry.get('uploader', 'Unknown'),
515 | 'view_count': entry.get('view_count'),
516 | 'description': entry.get('description', '')
517 | })
518 |
519 | return {
520 | 'query': query,
521 | 'results': results,
522 | 'total': len(results)
523 | }
524 |
525 | except Exception as e:
526 | logger.error(f"Search error: {e}")
527 | return {'error': str(e)}
528 |
529 | @app.websocket("/ws/progress")
530 | async def websocket_endpoint(websocket: WebSocket):
531 | await websocket.accept()
532 | connected_websockets.append(websocket)
533 |
534 | try:
535 | while True:
536 | for download_id, data in active_downloads.items():
537 | if 'progress' in data:
538 | progress_data = data['progress']
539 | await websocket.send_text(json.dumps(progress_data))
540 | del data['progress']
541 | await asyncio.sleep(0.5)
542 | except WebSocketDisconnect:
543 | if websocket in connected_websockets:
544 | connected_websockets.remove(websocket)
545 |
546 | @app.get("/downloads")
547 | async def list_downloads():
548 | return active_downloads
549 |
550 | @app.get("/options")
551 | async def get_options():
552 | """Return all available yt-dlp options organized by category"""
553 | return {
554 | "categories": get_options_by_category(),
555 | "options": YT_DLP_OPTIONS
556 | }
557 |
558 | @app.post("/save-config")
559 | async def save_configuration(request: Request):
560 | """Save current configuration to file"""
561 | try:
562 | form_data = await request.form()
563 | config_name = form_data.get("config_name", "default")
564 |
565 | # Convert form data to config dict
566 | config_dict = {}
567 | for key, value in form_data.items():
568 | if key != "config_name" and value:
569 | config_dict[key] = value
570 |
571 | # Save to config directory
572 | config_dir = Path("/app/configs")
573 | config_dir.mkdir(exist_ok=True)
574 | config_file = config_dir / f"{config_name}.json"
575 |
576 | import json
577 | with open(config_file, 'w') as f:
578 | json.dump(config_dict, f, indent=2)
579 |
580 | return {"message": f"Configuration saved as {config_name}.json", "success": True}
581 |
582 | except Exception as e:
583 | logger.error(f"Config save error: {e}")
584 | return {"error": str(e), "success": False}
585 |
586 | @app.get("/load-config/{config_name}")
587 | async def load_configuration(config_name: str):
588 | """Load configuration from file"""
589 | try:
590 | config_dir = Path("/app/configs")
591 | config_file = config_dir / f"{config_name}.json"
592 |
593 | if not config_file.exists():
594 | return {"error": f"Configuration {config_name} not found", "success": False}
595 |
596 | import json
597 | with open(config_file, 'r') as f:
598 | config_dict = json.load(f)
599 |
600 | return {"config": config_dict, "success": True}
601 |
602 | except Exception as e:
603 | logger.error(f"Config load error: {e}")
604 | return {"error": str(e), "success": False}
605 |
606 | @app.get("/list-configs")
607 | async def list_configurations():
608 | """List all saved configurations"""
609 | try:
610 | config_dir = Path("/app/configs")
611 | if not config_dir.exists():
612 | return {"configs": [], "success": True}
613 |
614 | configs = []
615 | for config_file in config_dir.glob("*.json"):
616 | configs.append({
617 | "name": config_file.stem,
618 | "modified": config_file.stat().st_mtime
619 | })
620 |
621 | return {"configs": sorted(configs, key=lambda x: x["modified"], reverse=True), "success": True}
622 |
623 | except Exception as e:
624 | logger.error(f"Config list error: {e}")
625 | return {"error": str(e), "success": False}
626 |
627 | if __name__ == "__main__":
628 | import uvicorn
629 | uvicorn.run(app, host="0.0.0.0", port=8000)
--------------------------------------------------------------------------------
/app/options.py:
--------------------------------------------------------------------------------
1 | """
2 | Comprehensive yt-dlp options mapping for web interface
3 | Based on yt-dlp command line options and YoutubeDL parameters
4 | """
5 |
6 | from typing import Dict, List, Any, Union
7 | from enum import Enum
8 |
9 | class OptionType(Enum):
10 | BOOLEAN = "boolean"
11 | STRING = "string"
12 | NUMBER = "number"
13 | SELECT = "select"
14 | MULTI_SELECT = "multi_select"
15 | FILE_PATH = "file_path"
16 | URL = "url"
17 | REGEX = "regex"
18 | TEMPLATE = "template"
19 |
20 | class OptionCategory(Enum):
21 | GENERAL = "General Options"
22 | NETWORK = "Network Options"
23 | VIDEO_SELECTION = "Video Selection"
24 | DOWNLOAD = "Download Options"
25 | FILESYSTEM = "Filesystem Options"
26 | THUMBNAIL = "Thumbnail Options"
27 | VERBOSITY = "Verbosity & Simulation Options"
28 | WORKAROUNDS = "Workarounds"
29 | VIDEO_FORMAT = "Video Format Options"
30 | SUBTITLE = "Subtitle Options"
31 | AUTHENTICATION = "Authentication Options"
32 | POST_PROCESSING = "Post-Processing Options"
33 | SPONSORBLOCK = "SponsorBlock Options"
34 | EXTRACTOR = "Extractor Options"
35 | GEO_RESTRICTION = "Geo Restriction"
36 | COOKIES = "Cookies & Headers"
37 |
38 | # Comprehensive yt-dlp options mapping
39 | YT_DLP_OPTIONS = {
40 | # General Options
41 | "batchfile": {
42 | "category": OptionCategory.GENERAL,
43 | "type": OptionType.FILE_PATH,
44 | "cli": ["-a", "--batch-file"],
45 | "description": "File containing URLs to download (one per line)",
46 | "default": None
47 | },
48 | "default_search": {
49 | "category": OptionCategory.GENERAL,
50 | "type": OptionType.SELECT,
51 | "cli": ["--default-search"],
52 | "description": "Use this prefix for unqualified URLs",
53 | "options": ["auto", "auto_warning", "error", "fixup_error", "ytsearch", "gvsearch"],
54 | "default": "fixup_error"
55 | },
56 | "ignore_config": {
57 | "category": OptionCategory.GENERAL,
58 | "type": OptionType.BOOLEAN,
59 | "cli": ["--ignore-config"],
60 | "description": "Don't load any configuration files",
61 | "default": False
62 | },
63 | "flat_playlist": {
64 | "category": OptionCategory.GENERAL,
65 | "type": OptionType.BOOLEAN,
66 | "cli": ["--flat-playlist"],
67 | "description": "Do not extract playlist entries",
68 | "default": False
69 | },
70 | "live_from_start": {
71 | "category": OptionCategory.GENERAL,
72 | "type": OptionType.BOOLEAN,
73 | "cli": ["--live-from-start"],
74 | "description": "Download livestreams from the start",
75 | "default": False
76 | },
77 | "wait_for_video": {
78 | "category": OptionCategory.GENERAL,
79 | "type": OptionType.STRING,
80 | "cli": ["--wait-for-video"],
81 | "description": "Wait for scheduled streams (MIN[-MAX] seconds)",
82 | "default": None,
83 | "placeholder": "300 or 60-600"
84 | },
85 | "color": {
86 | "category": OptionCategory.GENERAL,
87 | "type": OptionType.SELECT,
88 | "cli": ["--color"],
89 | "description": "Whether to emit color codes in output",
90 | "options": ["always", "auto", "never", "no_color"],
91 | "default": "auto"
92 | },
93 | "compat_opts": {
94 | "category": OptionCategory.GENERAL,
95 | "type": OptionType.STRING,
96 | "cli": ["--compat-options"],
97 | "description": "Options for compatibility with youtube-dl",
98 | "default": None,
99 | "placeholder": "youtube-dl-compatibility"
100 | },
101 | "ignore_errors": {
102 | "category": OptionCategory.GENERAL,
103 | "type": OptionType.BOOLEAN,
104 | "cli": ["-i", "--ignore-errors"],
105 | "description": "Continue downloading if extraction errors occur",
106 | "default": False
107 | },
108 | "abort_on_error": {
109 | "category": OptionCategory.GENERAL,
110 | "type": OptionType.BOOLEAN,
111 | "cli": ["--abort-on-error"],
112 | "description": "Abort downloading of remaining videos if extraction errors occur",
113 | "default": False
114 | },
115 | "extract_flat": {
116 | "category": OptionCategory.GENERAL,
117 | "type": OptionType.SELECT,
118 | "cli": ["--flat-playlist"],
119 | "description": "Do not extract videos in playlists, only metadata",
120 | "options": [None, True, False, "in_playlist", "discard_in_playlist"],
121 | "default": None
122 | },
123 | "mark_watched": {
124 | "category": OptionCategory.GENERAL,
125 | "type": OptionType.BOOLEAN,
126 | "cli": ["--mark-watched"],
127 | "description": "Mark videos as watched",
128 | "default": False
129 | },
130 | "no_mark_watched": {
131 | "category": OptionCategory.GENERAL,
132 | "type": OptionType.BOOLEAN,
133 | "cli": ["--no-mark-watched"],
134 | "description": "Do not mark videos as watched",
135 | "default": False
136 | },
137 |
138 | # Video Selection
139 | "playliststart": {
140 | "category": OptionCategory.VIDEO_SELECTION,
141 | "type": OptionType.NUMBER,
142 | "cli": ["--playlist-start"],
143 | "description": "Playlist item to start downloading from",
144 | "default": 1,
145 | "min": 1
146 | },
147 | "playlistend": {
148 | "category": OptionCategory.VIDEO_SELECTION,
149 | "type": OptionType.NUMBER,
150 | "cli": ["--playlist-end"],
151 | "description": "Playlist item to end downloading at",
152 | "default": None,
153 | "min": 1
154 | },
155 | "playlist_items": {
156 | "category": OptionCategory.VIDEO_SELECTION,
157 | "type": OptionType.STRING,
158 | "cli": ["--playlist-items"],
159 | "description": "Specify playlist items to download (e.g. 1,3-5,7)",
160 | "default": None,
161 | "placeholder": "1,3-5,7"
162 | },
163 | "matchtitle": {
164 | "category": OptionCategory.VIDEO_SELECTION,
165 | "type": OptionType.REGEX,
166 | "cli": ["--match-title"],
167 | "description": "Download only videos matching title regex",
168 | "default": None
169 | },
170 | "rejecttitle": {
171 | "category": OptionCategory.VIDEO_SELECTION,
172 | "type": OptionType.REGEX,
173 | "cli": ["--reject-title"],
174 | "description": "Skip videos matching title regex",
175 | "default": None
176 | },
177 | "min_filesize": {
178 | "category": OptionCategory.VIDEO_SELECTION,
179 | "type": OptionType.STRING,
180 | "cli": ["--min-filesize"],
181 | "description": "Minimum file size (e.g. 50k or 44.6m)",
182 | "default": None,
183 | "placeholder": "50k or 44.6m"
184 | },
185 | "max_filesize": {
186 | "category": OptionCategory.VIDEO_SELECTION,
187 | "type": OptionType.STRING,
188 | "cli": ["--max-filesize"],
189 | "description": "Maximum file size (e.g. 50k or 44.6m)",
190 | "default": None,
191 | "placeholder": "50k or 44.6m"
192 | },
193 | "date": {
194 | "category": OptionCategory.VIDEO_SELECTION,
195 | "type": OptionType.STRING,
196 | "cli": ["--date"],
197 | "description": "Download only videos uploaded on this date (YYYYMMDD)",
198 | "default": None,
199 | "placeholder": "20240101"
200 | },
201 | "datebefore": {
202 | "category": OptionCategory.VIDEO_SELECTION,
203 | "type": OptionType.STRING,
204 | "cli": ["--datebefore"],
205 | "description": "Download only videos uploaded before this date (YYYYMMDD)",
206 | "default": None,
207 | "placeholder": "20240101"
208 | },
209 | "dateafter": {
210 | "category": OptionCategory.VIDEO_SELECTION,
211 | "type": OptionType.STRING,
212 | "cli": ["--dateafter"],
213 | "description": "Download only videos uploaded after this date (YYYYMMDD)",
214 | "default": None,
215 | "placeholder": "20240101"
216 | },
217 | "min_views": {
218 | "category": OptionCategory.VIDEO_SELECTION,
219 | "type": OptionType.NUMBER,
220 | "cli": ["--min-views"],
221 | "description": "Minimum view count for downloaded videos",
222 | "default": None,
223 | "min": 0
224 | },
225 | "max_views": {
226 | "category": OptionCategory.VIDEO_SELECTION,
227 | "type": OptionType.NUMBER,
228 | "cli": ["--max-views"],
229 | "description": "Maximum view count for downloaded videos",
230 | "default": None,
231 | "min": 0
232 | },
233 |
234 | # Download Options
235 | "concurrent_fragments": {
236 | "category": OptionCategory.DOWNLOAD,
237 | "type": OptionType.NUMBER,
238 | "cli": ["-N", "--concurrent-fragments"],
239 | "description": "Number of fragments to download simultaneously",
240 | "default": 1,
241 | "min": 1,
242 | "max": 32
243 | },
244 | "limit_rate": {
245 | "category": OptionCategory.DOWNLOAD,
246 | "type": OptionType.STRING,
247 | "cli": ["-r", "--limit-rate"],
248 | "description": "Maximum download rate (e.g. 50K or 4.2M)",
249 | "default": None,
250 | "placeholder": "50K or 4.2M"
251 | },
252 | "throttled_rate": {
253 | "category": OptionCategory.DOWNLOAD,
254 | "type": OptionType.STRING,
255 | "cli": ["--throttled-rate"],
256 | "description": "Minimum download rate below which throttling is assumed",
257 | "default": None,
258 | "placeholder": "100K"
259 | },
260 | "retries": {
261 | "category": OptionCategory.DOWNLOAD,
262 | "type": OptionType.NUMBER,
263 | "cli": ["-R", "--retries"],
264 | "description": "Number of download retries",
265 | "default": 10,
266 | "min": 0
267 | },
268 | "file_access_retries": {
269 | "category": OptionCategory.DOWNLOAD,
270 | "type": OptionType.NUMBER,
271 | "cli": ["--file-access-retries"],
272 | "description": "Number of file access retries",
273 | "default": 3,
274 | "min": 0
275 | },
276 | "fragment_retries": {
277 | "category": OptionCategory.DOWNLOAD,
278 | "type": OptionType.NUMBER,
279 | "cli": ["--fragment-retries"],
280 | "description": "Number of fragment download retries",
281 | "default": 10,
282 | "min": 0
283 | },
284 | "retry_sleep_linear": {
285 | "category": OptionCategory.DOWNLOAD,
286 | "type": OptionType.NUMBER,
287 | "cli": ["--retry-sleep"],
288 | "description": "Time to sleep between retries (linear component)",
289 | "default": None,
290 | "min": 0
291 | },
292 | "skip_unavailable_fragments": {
293 | "category": OptionCategory.DOWNLOAD,
294 | "type": OptionType.BOOLEAN,
295 | "cli": ["--skip-unavailable-fragments"],
296 | "description": "Skip unavailable fragments for live streams",
297 | "default": True
298 | },
299 | "keep_fragments": {
300 | "category": OptionCategory.DOWNLOAD,
301 | "type": OptionType.BOOLEAN,
302 | "cli": ["--keep-fragments"],
303 | "description": "Keep downloaded fragments after successful download",
304 | "default": False
305 | },
306 |
307 | # Network Options
308 | "proxy": {
309 | "category": OptionCategory.NETWORK,
310 | "type": OptionType.URL,
311 | "cli": ["--proxy"],
312 | "description": "Use specified HTTP/HTTPS/SOCKS proxy",
313 | "default": None,
314 | "placeholder": "http://proxy.example.com:8080"
315 | },
316 | "socket_timeout": {
317 | "category": OptionCategory.NETWORK,
318 | "type": OptionType.NUMBER,
319 | "cli": ["--socket-timeout"],
320 | "description": "Time to wait before giving up in seconds",
321 | "default": None,
322 | "min": 0
323 | },
324 | "source_address": {
325 | "category": OptionCategory.NETWORK,
326 | "type": OptionType.STRING,
327 | "cli": ["--source-address"],
328 | "description": "Client-side IP address to bind to",
329 | "default": None,
330 | "placeholder": "192.168.1.100"
331 | },
332 | "force_ipv4": {
333 | "category": OptionCategory.NETWORK,
334 | "type": OptionType.BOOLEAN,
335 | "cli": ["-4", "--force-ipv4"],
336 | "description": "Make all connections via IPv4",
337 | "default": False
338 | },
339 | "force_ipv6": {
340 | "category": OptionCategory.NETWORK,
341 | "type": OptionType.BOOLEAN,
342 | "cli": ["-6", "--force-ipv6"],
343 | "description": "Make all connections via IPv6",
344 | "default": False
345 | },
346 |
347 | # Filesystem Options
348 | "outtmpl": {
349 | "category": OptionCategory.FILESYSTEM,
350 | "type": OptionType.TEMPLATE,
351 | "cli": ["-o", "--output"],
352 | "description": "Output filename template",
353 | "default": "%(title)s.%(ext)s",
354 | "placeholder": "%(uploader)s/%(title)s.%(ext)s"
355 | },
356 | "outtmpl_na_placeholder": {
357 | "category": OptionCategory.FILESYSTEM,
358 | "type": OptionType.STRING,
359 | "cli": ["--output-na-placeholder"],
360 | "description": "Placeholder for unavailable template fields",
361 | "default": "NA"
362 | },
363 | "restrict_filenames": {
364 | "category": OptionCategory.FILESYSTEM,
365 | "type": OptionType.BOOLEAN,
366 | "cli": ["--restrict-filenames"],
367 | "description": "Restrict filenames to ASCII characters",
368 | "default": False
369 | },
370 | "no_restrict_filenames": {
371 | "category": OptionCategory.FILESYSTEM,
372 | "type": OptionType.BOOLEAN,
373 | "cli": ["--no-restrict-filenames"],
374 | "description": "Allow Unicode characters in filenames",
375 | "default": True
376 | },
377 | "windows_filenames": {
378 | "category": OptionCategory.FILESYSTEM,
379 | "type": OptionType.BOOLEAN,
380 | "cli": ["--windows-filenames"],
381 | "description": "Force Windows-compatible filenames",
382 | "default": False
383 | },
384 | "trim_filenames": {
385 | "category": OptionCategory.FILESYSTEM,
386 | "type": OptionType.NUMBER,
387 | "cli": ["--trim-filenames"],
388 | "description": "Limit filename length (excluding extension)",
389 | "default": None,
390 | "min": 1
391 | },
392 | "no_overwrites": {
393 | "category": OptionCategory.FILESYSTEM,
394 | "type": OptionType.BOOLEAN,
395 | "cli": ["-w", "--no-overwrites"],
396 | "description": "Do not overwrite existing files",
397 | "default": False
398 | },
399 | "continue_dl": {
400 | "category": OptionCategory.FILESYSTEM,
401 | "type": OptionType.BOOLEAN,
402 | "cli": ["-c", "--continue"],
403 | "description": "Resume partially downloaded files",
404 | "default": True
405 | },
406 | "no_continue": {
407 | "category": OptionCategory.FILESYSTEM,
408 | "type": OptionType.BOOLEAN,
409 | "cli": ["--no-continue"],
410 | "description": "Do not resume partially downloaded files",
411 | "default": False
412 | },
413 | "no_part": {
414 | "category": OptionCategory.FILESYSTEM,
415 | "type": OptionType.BOOLEAN,
416 | "cli": ["--no-part"],
417 | "description": "Do not use .part files",
418 | "default": False
419 | },
420 | "no_mtime": {
421 | "category": OptionCategory.FILESYSTEM,
422 | "type": OptionType.BOOLEAN,
423 | "cli": ["--no-mtime"],
424 | "description": "Do not use Last-modified header to set file modification time",
425 | "default": False
426 | },
427 | "write_description": {
428 | "category": OptionCategory.FILESYSTEM,
429 | "type": OptionType.BOOLEAN,
430 | "cli": ["--write-description"],
431 | "description": "Write video description to .description file",
432 | "default": False
433 | },
434 | "write_info_json": {
435 | "category": OptionCategory.FILESYSTEM,
436 | "type": OptionType.BOOLEAN,
437 | "cli": ["--write-info-json"],
438 | "description": "Write video metadata to .info.json file",
439 | "default": False
440 | },
441 | "write_annotations": {
442 | "category": OptionCategory.FILESYSTEM,
443 | "type": OptionType.BOOLEAN,
444 | "cli": ["--write-annotations"],
445 | "description": "Write video annotations to .annotations.xml file",
446 | "default": False
447 | },
448 |
449 | # Thumbnail Options
450 | "write_thumbnail": {
451 | "category": OptionCategory.THUMBNAIL,
452 | "type": OptionType.BOOLEAN,
453 | "cli": ["--write-thumbnail"],
454 | "description": "Write thumbnail image to disk",
455 | "default": False
456 | },
457 | "write_all_thumbnails": {
458 | "category": OptionCategory.THUMBNAIL,
459 | "type": OptionType.BOOLEAN,
460 | "cli": ["--write-all-thumbnails"],
461 | "description": "Write all thumbnail image formats to disk",
462 | "default": False
463 | },
464 | "list_thumbnails": {
465 | "category": OptionCategory.THUMBNAIL,
466 | "type": OptionType.BOOLEAN,
467 | "cli": ["--list-thumbnails"],
468 | "description": "List available thumbnail formats and exit",
469 | "default": False
470 | },
471 |
472 | # Verbosity Options
473 | "quiet": {
474 | "category": OptionCategory.VERBOSITY,
475 | "type": OptionType.BOOLEAN,
476 | "cli": ["-q", "--quiet"],
477 | "description": "Activate quiet mode",
478 | "default": False
479 | },
480 | "no_warnings": {
481 | "category": OptionCategory.VERBOSITY,
482 | "type": OptionType.BOOLEAN,
483 | "cli": ["--no-warnings"],
484 | "description": "Ignore warnings",
485 | "default": False
486 | },
487 | "simulate": {
488 | "category": OptionCategory.VERBOSITY,
489 | "type": OptionType.BOOLEAN,
490 | "cli": ["-s", "--simulate"],
491 | "description": "Do not download video",
492 | "default": False
493 | },
494 | "skip_download": {
495 | "category": OptionCategory.VERBOSITY,
496 | "type": OptionType.BOOLEAN,
497 | "cli": ["--skip-download"],
498 | "description": "Do not download video but write all related files",
499 | "default": False
500 | },
501 | "print_json": {
502 | "category": OptionCategory.VERBOSITY,
503 | "type": OptionType.BOOLEAN,
504 | "cli": ["-j", "--print-json"],
505 | "description": "Output progress info as JSON",
506 | "default": False
507 | },
508 |
509 | # Video Format Options
510 | "format": {
511 | "category": OptionCategory.VIDEO_FORMAT,
512 | "type": OptionType.STRING,
513 | "cli": ["-f", "--format"],
514 | "description": "Video format code",
515 | "default": "best/bestvideo[height<=?1080]+bestaudio/best",
516 | "placeholder": "best[height<=480]"
517 | },
518 | "format_sort": {
519 | "category": OptionCategory.VIDEO_FORMAT,
520 | "type": OptionType.STRING,
521 | "cli": ["-S", "--format-sort"],
522 | "description": "Sort formats by given field(s)",
523 | "default": None,
524 | "placeholder": "height,tbr,lang"
525 | },
526 | "format_sort_force": {
527 | "category": OptionCategory.VIDEO_FORMAT,
528 | "type": OptionType.BOOLEAN,
529 | "cli": ["--format-sort-force"],
530 | "description": "Force given format_sort",
531 | "default": False
532 | },
533 | "no_format_sort_force": {
534 | "category": OptionCategory.VIDEO_FORMAT,
535 | "type": OptionType.BOOLEAN,
536 | "cli": ["--no-format-sort-force"],
537 | "description": "Do not force given format_sort",
538 | "default": True
539 | },
540 | "video_multistreams": {
541 | "category": OptionCategory.VIDEO_FORMAT,
542 | "type": OptionType.BOOLEAN,
543 | "cli": ["--video-multistreams"],
544 | "description": "Allow multiple video streams to be merged",
545 | "default": False
546 | },
547 | "audio_multistreams": {
548 | "category": OptionCategory.VIDEO_FORMAT,
549 | "type": OptionType.BOOLEAN,
550 | "cli": ["--audio-multistreams"],
551 | "description": "Allow multiple audio streams to be merged",
552 | "default": False
553 | },
554 | "prefer_free_formats": {
555 | "category": OptionCategory.VIDEO_FORMAT,
556 | "type": OptionType.BOOLEAN,
557 | "cli": ["--prefer-free-formats"],
558 | "description": "Prefer free video formats over non-free formats",
559 | "default": False
560 | },
561 | "no_prefer_free_formats": {
562 | "category": OptionCategory.VIDEO_FORMAT,
563 | "type": OptionType.BOOLEAN,
564 | "cli": ["--no-prefer-free-formats"],
565 | "description": "Do not prefer free video formats",
566 | "default": True
567 | },
568 | "check_formats": {
569 | "category": OptionCategory.VIDEO_FORMAT,
570 | "type": OptionType.SELECT,
571 | "cli": ["--check-formats"],
572 | "description": "Check if formats are actually downloadable",
573 | "options": [None, "selected"],
574 | "default": None
575 | },
576 | "check_all_formats": {
577 | "category": OptionCategory.VIDEO_FORMAT,
578 | "type": OptionType.BOOLEAN,
579 | "cli": ["--check-all-formats"],
580 | "description": "Check all formats for download availability",
581 | "default": False
582 | },
583 | "no_check_formats": {
584 | "category": OptionCategory.VIDEO_FORMAT,
585 | "type": OptionType.BOOLEAN,
586 | "cli": ["--no-check-formats"],
587 | "description": "Do not check if formats are downloadable",
588 | "default": True
589 | },
590 | "list_formats": {
591 | "category": OptionCategory.VIDEO_FORMAT,
592 | "type": OptionType.BOOLEAN,
593 | "cli": ["-F", "--list-formats"],
594 | "description": "List available formats and exit",
595 | "default": False
596 | },
597 | "youtube_skip_dash_manifest": {
598 | "category": OptionCategory.VIDEO_FORMAT,
599 | "type": OptionType.BOOLEAN,
600 | "cli": ["--youtube-skip-dash-manifest"],
601 | "description": "Do not download DASH manifest on YouTube videos",
602 | "default": False
603 | },
604 | "youtube_skip_hls_manifest": {
605 | "category": OptionCategory.VIDEO_FORMAT,
606 | "type": OptionType.BOOLEAN,
607 | "cli": ["--youtube-skip-hls-manifest"],
608 | "description": "Do not download HLS manifest on YouTube videos",
609 | "default": False
610 | },
611 |
612 | # Subtitle Options
613 | "writesubtitles": {
614 | "category": OptionCategory.SUBTITLE,
615 | "type": OptionType.BOOLEAN,
616 | "cli": ["--write-subs"],
617 | "description": "Write subtitle files",
618 | "default": False
619 | },
620 | "writeautomaticsub": {
621 | "category": OptionCategory.SUBTITLE,
622 | "type": OptionType.BOOLEAN,
623 | "cli": ["--write-auto-subs"],
624 | "description": "Write automatically generated subtitle files",
625 | "default": False
626 | },
627 | "allsubtitles": {
628 | "category": OptionCategory.SUBTITLE,
629 | "type": OptionType.BOOLEAN,
630 | "cli": ["--all-subs"],
631 | "description": "Download all available subtitle files",
632 | "default": False
633 | },
634 | "listsubtitles": {
635 | "category": OptionCategory.SUBTITLE,
636 | "type": OptionType.BOOLEAN,
637 | "cli": ["--list-subs"],
638 | "description": "List available subtitle files and exit",
639 | "default": False
640 | },
641 | "subtitlesformat": {
642 | "category": OptionCategory.SUBTITLE,
643 | "type": OptionType.SELECT,
644 | "cli": ["--sub-format"],
645 | "description": "Subtitle format preference",
646 | "options": ["best", "srt", "ass", "vtt", "lrc"],
647 | "default": "best"
648 | },
649 | "subtitleslangs": {
650 | "category": OptionCategory.SUBTITLE,
651 | "type": OptionType.STRING,
652 | "cli": ["--sub-langs"],
653 | "description": "Languages of subtitles to download (comma separated)",
654 | "default": None,
655 | "placeholder": "en,es,fr"
656 | },
657 |
658 | # Authentication Options
659 | "username": {
660 | "category": OptionCategory.AUTHENTICATION,
661 | "type": OptionType.STRING,
662 | "cli": ["-u", "--username"],
663 | "description": "Login username",
664 | "default": None
665 | },
666 | "password": {
667 | "category": OptionCategory.AUTHENTICATION,
668 | "type": OptionType.STRING,
669 | "cli": ["-p", "--password"],
670 | "description": "Login password",
671 | "default": None,
672 | "sensitive": True
673 | },
674 | "twofactor": {
675 | "category": OptionCategory.AUTHENTICATION,
676 | "type": OptionType.STRING,
677 | "cli": ["-2", "--twofactor"],
678 | "description": "Two-factor authentication code",
679 | "default": None,
680 | "sensitive": True
681 | },
682 | "netrc": {
683 | "category": OptionCategory.AUTHENTICATION,
684 | "type": OptionType.BOOLEAN,
685 | "cli": ["-n", "--netrc"],
686 | "description": "Use .netrc authentication data",
687 | "default": False
688 | },
689 | "netrc_location": {
690 | "category": OptionCategory.AUTHENTICATION,
691 | "type": OptionType.FILE_PATH,
692 | "cli": ["--netrc-location"],
693 | "description": "Location of .netrc authentication data",
694 | "default": None
695 | },
696 | "video_password": {
697 | "category": OptionCategory.AUTHENTICATION,
698 | "type": OptionType.STRING,
699 | "cli": ["--video-password"],
700 | "description": "Video password for protected videos",
701 | "default": None,
702 | "sensitive": True
703 | },
704 | "ap_mso": {
705 | "category": OptionCategory.AUTHENTICATION,
706 | "type": OptionType.STRING,
707 | "cli": ["--ap-mso"],
708 | "description": "Adobe Pass multiple-system operator identifier",
709 | "default": None
710 | },
711 | "ap_username": {
712 | "category": OptionCategory.AUTHENTICATION,
713 | "type": OptionType.STRING,
714 | "cli": ["--ap-username"],
715 | "description": "Adobe Pass username",
716 | "default": None
717 | },
718 | "ap_password": {
719 | "category": OptionCategory.AUTHENTICATION,
720 | "type": OptionType.STRING,
721 | "cli": ["--ap-password"],
722 | "description": "Adobe Pass password",
723 | "default": None,
724 | "sensitive": True
725 | },
726 |
727 | # Post-Processing Options
728 | "extract_audio": {
729 | "category": OptionCategory.POST_PROCESSING,
730 | "type": OptionType.BOOLEAN,
731 | "cli": ["-x", "--extract-audio"],
732 | "description": "Convert video files to audio-only files",
733 | "default": False
734 | },
735 | "audio_format": {
736 | "category": OptionCategory.POST_PROCESSING,
737 | "type": OptionType.SELECT,
738 | "cli": ["--audio-format"],
739 | "description": "Audio format for converted files",
740 | "options": ["best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", "wav", "alac"],
741 | "default": "best"
742 | },
743 | "audio_quality": {
744 | "category": OptionCategory.POST_PROCESSING,
745 | "type": OptionType.SELECT,
746 | "cli": ["--audio-quality"],
747 | "description": "Audio quality for converted files",
748 | "options": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
749 | "default": "5"
750 | },
751 | "recode_video": {
752 | "category": OptionCategory.POST_PROCESSING,
753 | "type": OptionType.SELECT,
754 | "cli": ["--recode-video"],
755 | "description": "Re-encode video to given format",
756 | "options": [None, "mp4", "flv", "ogg", "webm", "mkv", "avi"],
757 | "default": None
758 | },
759 | "postprocessor_args": {
760 | "category": OptionCategory.POST_PROCESSING,
761 | "type": OptionType.STRING,
762 | "cli": ["--postprocessor-args"],
763 | "description": "Give postprocessor arguments",
764 | "default": None,
765 | "placeholder": "-vcodec libx264"
766 | },
767 | "keep_video": {
768 | "category": OptionCategory.POST_PROCESSING,
769 | "type": OptionType.BOOLEAN,
770 | "cli": ["-k", "--keep-video"],
771 | "description": "Keep video after post-processing",
772 | "default": False
773 | },
774 | "no_keep_video": {
775 | "category": OptionCategory.POST_PROCESSING,
776 | "type": OptionType.BOOLEAN,
777 | "cli": ["--no-keep-video"],
778 | "description": "Delete video after post-processing",
779 | "default": True
780 | },
781 | "no_post_overwrites": {
782 | "category": OptionCategory.POST_PROCESSING,
783 | "type": OptionType.BOOLEAN,
784 | "cli": ["--no-post-overwrites"],
785 | "description": "Do not overwrite post-processed files",
786 | "default": False
787 | },
788 | "embed_subs": {
789 | "category": OptionCategory.POST_PROCESSING,
790 | "type": OptionType.BOOLEAN,
791 | "cli": ["--embed-subs"],
792 | "description": "Embed subtitles in video",
793 | "default": False
794 | },
795 | "embed_thumbnail": {
796 | "category": OptionCategory.POST_PROCESSING,
797 | "type": OptionType.BOOLEAN,
798 | "cli": ["--embed-thumbnail"],
799 | "description": "Embed thumbnail in audio/video",
800 | "default": False
801 | },
802 | "embed_metadata": {
803 | "category": OptionCategory.POST_PROCESSING,
804 | "type": OptionType.BOOLEAN,
805 | "cli": ["--embed-metadata"],
806 | "description": "Embed metadata in video/audio files",
807 | "default": False
808 | },
809 | "embed_chapters": {
810 | "category": OptionCategory.POST_PROCESSING,
811 | "type": OptionType.BOOLEAN,
812 | "cli": ["--embed-chapters"],
813 | "description": "Add chapter markers to audio/video",
814 | "default": False
815 | },
816 | "embed_info_json": {
817 | "category": OptionCategory.POST_PROCESSING,
818 | "type": OptionType.BOOLEAN,
819 | "cli": ["--embed-info-json"],
820 | "description": "Embed info.json as attachment",
821 | "default": False
822 | },
823 | "parse_metadata": {
824 | "category": OptionCategory.POST_PROCESSING,
825 | "type": OptionType.STRING,
826 | "cli": ["--parse-metadata"],
827 | "description": "Parse additional metadata from filename/path",
828 | "default": None,
829 | "placeholder": "FROM_FIELD:TO_FIELD"
830 | },
831 | "replace_in_metadata": {
832 | "category": OptionCategory.POST_PROCESSING,
833 | "type": OptionType.STRING,
834 | "cli": ["--replace-in-metadata"],
835 | "description": "Replace text in metadata fields",
836 | "default": None,
837 | "placeholder": "FIELD REGEX REPLACE"
838 | },
839 | "xattrs": {
840 | "category": OptionCategory.POST_PROCESSING,
841 | "type": OptionType.BOOLEAN,
842 | "cli": ["--xattrs"],
843 | "description": "Write metadata to extended attributes",
844 | "default": False
845 | },
846 | "fixup": {
847 | "category": OptionCategory.POST_PROCESSING,
848 | "type": OptionType.SELECT,
849 | "cli": ["--fixup"],
850 | "description": "Automatically correct known faults in downloaded files",
851 | "options": ["never", "warn", "detect_or_warn", "force"],
852 | "default": "detect_or_warn"
853 | },
854 | "ffmpeg_location": {
855 | "category": OptionCategory.POST_PROCESSING,
856 | "type": OptionType.FILE_PATH,
857 | "cli": ["--ffmpeg-location"],
858 | "description": "Location of ffmpeg binary",
859 | "default": None
860 | },
861 | "exec": {
862 | "category": OptionCategory.POST_PROCESSING,
863 | "type": OptionType.STRING,
864 | "cli": ["--exec"],
865 | "description": "Execute command on downloaded file",
866 | "default": None,
867 | "placeholder": "echo Downloaded {}"
868 | },
869 |
870 | # SponsorBlock Options
871 | "sponsorblock_mark": {
872 | "category": OptionCategory.SPONSORBLOCK,
873 | "type": OptionType.MULTI_SELECT,
874 | "cli": ["--sponsorblock-mark"],
875 | "description": "Categories to create chapters for",
876 | "options": ["sponsor", "intro", "outro", "selfpromo", "preview", "filler", "interaction", "music_offtopic", "poi_highlight"],
877 | "default": []
878 | },
879 | "sponsorblock_remove": {
880 | "category": OptionCategory.SPONSORBLOCK,
881 | "type": OptionType.MULTI_SELECT,
882 | "cli": ["--sponsorblock-remove"],
883 | "description": "Categories to remove from video",
884 | "options": ["sponsor", "intro", "outro", "selfpromo", "preview", "filler", "interaction", "music_offtopic", "poi_highlight"],
885 | "default": []
886 | },
887 | "sponsorblock_chapter_title": {
888 | "category": OptionCategory.SPONSORBLOCK,
889 | "type": OptionType.STRING,
890 | "cli": ["--sponsorblock-chapter-title"],
891 | "description": "Template for SponsorBlock chapter titles",
892 | "default": "[SponsorBlock]: %(category_names)l",
893 | "placeholder": "[SponsorBlock]: %(category_names)l"
894 | },
895 | "no_sponsorblock": {
896 | "category": OptionCategory.SPONSORBLOCK,
897 | "type": OptionType.BOOLEAN,
898 | "cli": ["--no-sponsorblock"],
899 | "description": "Disable SponsorBlock",
900 | "default": True
901 | },
902 | "sponsorblock_api": {
903 | "category": OptionCategory.SPONSORBLOCK,
904 | "type": OptionType.URL,
905 | "cli": ["--sponsorblock-api"],
906 | "description": "SponsorBlock API URL",
907 | "default": "https://sponsor.ajay.app",
908 | "placeholder": "https://sponsor.ajay.app"
909 | },
910 |
911 | # Extractor Options
912 | "extractor_args": {
913 | "category": OptionCategory.EXTRACTOR,
914 | "type": OptionType.STRING,
915 | "cli": ["--extractor-args"],
916 | "description": "Pass arguments to extractors",
917 | "default": None,
918 | "placeholder": "youtube:skip=dash"
919 | },
920 | "youtube_include_dash_manifest": {
921 | "category": OptionCategory.EXTRACTOR,
922 | "type": OptionType.BOOLEAN,
923 | "cli": ["--youtube-include-dash-manifest"],
924 | "description": "Download DASH manifest on YouTube",
925 | "default": True
926 | },
927 | "youtube_include_hls_manifest": {
928 | "category": OptionCategory.EXTRACTOR,
929 | "type": OptionType.BOOLEAN,
930 | "cli": ["--youtube-include-hls-manifest"],
931 | "description": "Download HLS manifest on YouTube",
932 | "default": True
933 | },
934 |
935 | # Geo Restriction
936 | "geo_verification_proxy": {
937 | "category": OptionCategory.GEO_RESTRICTION,
938 | "type": OptionType.URL,
939 | "cli": ["--geo-verification-proxy"],
940 | "description": "Use proxy to verify geo location",
941 | "default": None,
942 | "placeholder": "http://proxy.example.com:8080"
943 | },
944 | "geo_bypass": {
945 | "category": OptionCategory.GEO_RESTRICTION,
946 | "type": OptionType.BOOLEAN,
947 | "cli": ["--geo-bypass"],
948 | "description": "Bypass geographic restriction via fake X-Forwarded-For",
949 | "default": False
950 | },
951 | "no_geo_bypass": {
952 | "category": OptionCategory.GEO_RESTRICTION,
953 | "type": OptionType.BOOLEAN,
954 | "cli": ["--no-geo-bypass"],
955 | "description": "Do not bypass geographic restriction",
956 | "default": True
957 | },
958 | "geo_bypass_country": {
959 | "category": OptionCategory.GEO_RESTRICTION,
960 | "type": OptionType.STRING,
961 | "cli": ["--geo-bypass-country"],
962 | "description": "Force bypass using country code",
963 | "default": None,
964 | "placeholder": "US"
965 | },
966 | "geo_bypass_ip_block": {
967 | "category": OptionCategory.GEO_RESTRICTION,
968 | "type": OptionType.STRING,
969 | "cli": ["--geo-bypass-ip-block"],
970 | "description": "Force bypass using IP block",
971 | "default": None,
972 | "placeholder": "1.2.3.4/24"
973 | },
974 |
975 | # Cookies & Headers Options
976 | "cookiefile": {
977 | "category": OptionCategory.COOKIES,
978 | "type": OptionType.FILE_PATH,
979 | "cli": ["--cookies"],
980 | "description": "File to read cookies from",
981 | "default": None
982 | },
983 | "cookiesfrombrowser": {
984 | "category": OptionCategory.COOKIES,
985 | "type": OptionType.STRING,
986 | "cli": ["--cookies-from-browser"],
987 | "description": "Load cookies from browser (BROWSER[+KEYRING][:PROFILE])",
988 | "default": None,
989 | "placeholder": "chrome, firefox, safari, etc."
990 | },
991 | "no_cookies": {
992 | "category": OptionCategory.COOKIES,
993 | "type": OptionType.BOOLEAN,
994 | "cli": ["--no-cookies"],
995 | "description": "Do not read/dump cookies from/to file",
996 | "default": False
997 | },
998 | "user_agent": {
999 | "category": OptionCategory.COOKIES,
1000 | "type": OptionType.STRING,
1001 | "cli": ["--user-agent"],
1002 | "description": "Specify a custom user agent",
1003 | "default": None,
1004 | "placeholder": "Mozilla/5.0 ..."
1005 | },
1006 | "referer": {
1007 | "category": OptionCategory.COOKIES,
1008 | "type": OptionType.URL,
1009 | "cli": ["--referer"],
1010 | "description": "Specify a custom referer",
1011 | "default": None,
1012 | "placeholder": "https://example.com"
1013 | },
1014 | "add_headers": {
1015 | "category": OptionCategory.COOKIES,
1016 | "type": OptionType.STRING,
1017 | "cli": ["--add-headers"],
1018 | "description": "Add custom HTTP headers (FIELD:VALUE)",
1019 | "default": None,
1020 | "placeholder": "Custom-Header: value"
1021 | },
1022 |
1023 | # Workarounds Options
1024 | "encoding": {
1025 | "category": OptionCategory.WORKAROUNDS,
1026 | "type": OptionType.STRING,
1027 | "cli": ["--encoding"],
1028 | "description": "Force the specified encoding",
1029 | "default": None,
1030 | "placeholder": "utf-8"
1031 | },
1032 | "legacy_server_connect": {
1033 | "category": OptionCategory.WORKAROUNDS,
1034 | "type": OptionType.BOOLEAN,
1035 | "cli": ["--legacy-server-connect"],
1036 | "description": "Use legacy server connect method",
1037 | "default": False
1038 | },
1039 | "no_check_certificates": {
1040 | "category": OptionCategory.WORKAROUNDS,
1041 | "type": OptionType.BOOLEAN,
1042 | "cli": ["--no-check-certificates"],
1043 | "description": "Suppress HTTPS certificate validation",
1044 | "default": False
1045 | },
1046 | "prefer_insecure": {
1047 | "category": OptionCategory.WORKAROUNDS,
1048 | "type": OptionType.BOOLEAN,
1049 | "cli": ["--prefer-insecure"],
1050 | "description": "Use an unencrypted connection for extractors that support it",
1051 | "default": False
1052 | },
1053 | "add_ie_names": {
1054 | "category": OptionCategory.WORKAROUNDS,
1055 | "type": OptionType.STRING,
1056 | "cli": ["--add-ie-names"],
1057 | "description": "Add extractor names to filename",
1058 | "default": None
1059 | },
1060 |
1061 | # Sleep Options
1062 | "sleep_interval": {
1063 | "category": OptionCategory.WORKAROUNDS,
1064 | "type": OptionType.NUMBER,
1065 | "cli": ["--sleep-interval"],
1066 | "description": "Sleep between downloads (seconds)",
1067 | "default": None,
1068 | "min": 0
1069 | },
1070 | "max_sleep_interval": {
1071 | "category": OptionCategory.WORKAROUNDS,
1072 | "type": OptionType.NUMBER,
1073 | "cli": ["--max-sleep-interval"],
1074 | "description": "Maximum sleep interval (seconds)",
1075 | "default": None,
1076 | "min": 0
1077 | },
1078 | "sleep_subtitles": {
1079 | "category": OptionCategory.WORKAROUNDS,
1080 | "type": OptionType.NUMBER,
1081 | "cli": ["--sleep-subtitles"],
1082 | "description": "Sleep before subtitle download (seconds)",
1083 | "default": None,
1084 | "min": 0
1085 | },
1086 |
1087 | # Additional Network Options
1088 | "socket_timeout": {
1089 | "category": OptionCategory.NETWORK,
1090 | "type": OptionType.NUMBER,
1091 | "cli": ["--socket-timeout"],
1092 | "description": "Time to wait before giving up, in seconds",
1093 | "default": None,
1094 | "min": 0
1095 | },
1096 | "source_address": {
1097 | "category": OptionCategory.NETWORK,
1098 | "type": OptionType.STRING,
1099 | "cli": ["--source-address"],
1100 | "description": "Client-side IP address to bind to",
1101 | "default": None,
1102 | "placeholder": "192.168.1.100"
1103 | },
1104 | "impersonate": {
1105 | "category": OptionCategory.NETWORK,
1106 | "type": OptionType.STRING,
1107 | "cli": ["--impersonate"],
1108 | "description": "Client to impersonate for requests",
1109 | "default": None,
1110 | "placeholder": "chrome, firefox, safari"
1111 | },
1112 | "force_ipv4": {
1113 | "category": OptionCategory.NETWORK,
1114 | "type": OptionType.BOOLEAN,
1115 | "cli": ["-4", "--force-ipv4"],
1116 | "description": "Make all connections via IPv4",
1117 | "default": False
1118 | },
1119 | "force_ipv6": {
1120 | "category": OptionCategory.NETWORK,
1121 | "type": OptionType.BOOLEAN,
1122 | "cli": ["-6", "--force-ipv6"],
1123 | "description": "Make all connections via IPv6",
1124 | "default": False
1125 | },
1126 | "enable_file_urls": {
1127 | "category": OptionCategory.NETWORK,
1128 | "type": OptionType.BOOLEAN,
1129 | "cli": ["--enable-file-urls"],
1130 | "description": "Enable file:// URLs (disabled by default for security)",
1131 | "default": False
1132 | },
1133 |
1134 | # Additional Video Selection Options
1135 | "min_filesize": {
1136 | "category": OptionCategory.VIDEO_SELECTION,
1137 | "type": OptionType.STRING,
1138 | "cli": ["--min-filesize"],
1139 | "description": "Abort download if filesize is smaller than SIZE",
1140 | "default": None,
1141 | "placeholder": "50k or 44.6M"
1142 | },
1143 | "max_filesize": {
1144 | "category": OptionCategory.VIDEO_SELECTION,
1145 | "type": OptionType.STRING,
1146 | "cli": ["--max-filesize"],
1147 | "description": "Abort download if filesize is larger than SIZE",
1148 | "default": None,
1149 | "placeholder": "50k or 44.6M"
1150 | },
1151 | "date": {
1152 | "category": OptionCategory.VIDEO_SELECTION,
1153 | "type": OptionType.STRING,
1154 | "cli": ["--date"],
1155 | "description": "Download only videos uploaded on this date",
1156 | "default": None,
1157 | "placeholder": "YYYYMMDD or today-2weeks"
1158 | },
1159 | "datebefore": {
1160 | "category": OptionCategory.VIDEO_SELECTION,
1161 | "type": OptionType.STRING,
1162 | "cli": ["--datebefore"],
1163 | "description": "Download only videos uploaded on or before this date",
1164 | "default": None,
1165 | "placeholder": "YYYYMMDD"
1166 | },
1167 | "dateafter": {
1168 | "category": OptionCategory.VIDEO_SELECTION,
1169 | "type": OptionType.STRING,
1170 | "cli": ["--dateafter"],
1171 | "description": "Download only videos uploaded on or after this date",
1172 | "default": None,
1173 | "placeholder": "YYYYMMDD"
1174 | },
1175 | "match_filter": {
1176 | "category": OptionCategory.VIDEO_SELECTION,
1177 | "type": OptionType.STRING,
1178 | "cli": ["--match-filters"],
1179 | "description": "Generic video filter (OUTPUT TEMPLATE field comparisons)",
1180 | "default": None,
1181 | "placeholder": "like_count>?100 & description~='cats'"
1182 | },
1183 | "no_playlist": {
1184 | "category": OptionCategory.VIDEO_SELECTION,
1185 | "type": OptionType.BOOLEAN,
1186 | "cli": ["--no-playlist"],
1187 | "description": "Download only the video, if URL refers to video and playlist",
1188 | "default": False
1189 | },
1190 | "yes_playlist": {
1191 | "category": OptionCategory.VIDEO_SELECTION,
1192 | "type": OptionType.BOOLEAN,
1193 | "cli": ["--yes-playlist"],
1194 | "description": "Download the playlist, if URL refers to video and playlist",
1195 | "default": False
1196 | },
1197 | "download_archive": {
1198 | "category": OptionCategory.VIDEO_SELECTION,
1199 | "type": OptionType.FILE_PATH,
1200 | "cli": ["--download-archive"],
1201 | "description": "Download only videos not listed in archive file",
1202 | "default": None
1203 | },
1204 | "max_downloads": {
1205 | "category": OptionCategory.VIDEO_SELECTION,
1206 | "type": OptionType.NUMBER,
1207 | "cli": ["--max-downloads"],
1208 | "description": "Abort after downloading NUMBER files",
1209 | "default": None,
1210 | "min": 1
1211 | },
1212 | "break_on_existing": {
1213 | "category": OptionCategory.VIDEO_SELECTION,
1214 | "type": OptionType.BOOLEAN,
1215 | "cli": ["--break-on-existing"],
1216 | "description": "Stop download when encountering file in archive",
1217 | "default": False
1218 | },
1219 | "break_per_input": {
1220 | "category": OptionCategory.VIDEO_SELECTION,
1221 | "type": OptionType.BOOLEAN,
1222 | "cli": ["--break-per-input"],
1223 | "description": "Reset max-downloads and break-on-existing per input URL",
1224 | "default": False
1225 | },
1226 | "skip_playlist_after_errors": {
1227 | "category": OptionCategory.VIDEO_SELECTION,
1228 | "type": OptionType.NUMBER,
1229 | "cli": ["--skip-playlist-after-errors"],
1230 | "description": "Number of allowed failures until playlist is skipped",
1231 | "default": None,
1232 | "min": 1
1233 | },
1234 |
1235 | # Additional Download Options
1236 | "concurrent_fragments": {
1237 | "category": OptionCategory.DOWNLOAD,
1238 | "type": OptionType.NUMBER,
1239 | "cli": ["-N", "--concurrent-fragments"],
1240 | "description": "Number of fragments to download concurrently",
1241 | "default": 1,
1242 | "min": 1
1243 | },
1244 | "limit_rate": {
1245 | "category": OptionCategory.DOWNLOAD,
1246 | "type": OptionType.STRING,
1247 | "cli": ["-r", "--limit-rate"],
1248 | "description": "Maximum download rate",
1249 | "default": None,
1250 | "placeholder": "50K or 4.2M"
1251 | },
1252 | "throttled_rate": {
1253 | "category": OptionCategory.DOWNLOAD,
1254 | "type": OptionType.STRING,
1255 | "cli": ["--throttled-rate"],
1256 | "description": "Minimum download rate below which throttling is assumed",
1257 | "default": None,
1258 | "placeholder": "100K"
1259 | },
1260 | "file_access_retries": {
1261 | "category": OptionCategory.DOWNLOAD,
1262 | "type": OptionType.NUMBER,
1263 | "cli": ["--file-access-retries"],
1264 | "description": "Number of times to retry on file access error",
1265 | "default": 3,
1266 | "min": 0
1267 | },
1268 | "retry_sleep": {
1269 | "category": OptionCategory.DOWNLOAD,
1270 | "type": OptionType.STRING,
1271 | "cli": ["--retry-sleep"],
1272 | "description": "Time to sleep between retries",
1273 | "default": None,
1274 | "placeholder": "linear=1::2 or exp=1:20"
1275 | },
1276 | "skip_unavailable_fragments": {
1277 | "category": OptionCategory.DOWNLOAD,
1278 | "type": OptionType.BOOLEAN,
1279 | "cli": ["--skip-unavailable-fragments"],
1280 | "description": "Skip unavailable fragments for DASH/HLS downloads",
1281 | "default": True
1282 | },
1283 | "keep_fragments": {
1284 | "category": OptionCategory.DOWNLOAD,
1285 | "type": OptionType.BOOLEAN,
1286 | "cli": ["--keep-fragments"],
1287 | "description": "Keep downloaded fragments on disk after downloading",
1288 | "default": False
1289 | },
1290 | "buffer_size": {
1291 | "category": OptionCategory.DOWNLOAD,
1292 | "type": OptionType.STRING,
1293 | "cli": ["--buffer-size"],
1294 | "description": "Size of download buffer",
1295 | "default": "1024",
1296 | "placeholder": "1024 or 16K"
1297 | },
1298 | "http_chunk_size": {
1299 | "category": OptionCategory.DOWNLOAD,
1300 | "type": OptionType.STRING,
1301 | "cli": ["--http-chunk-size"],
1302 | "description": "Size of chunk for chunk-based HTTP downloading",
1303 | "default": None,
1304 | "placeholder": "10485760 or 10M"
1305 | },
1306 | "playlist_random": {
1307 | "category": OptionCategory.DOWNLOAD,
1308 | "type": OptionType.BOOLEAN,
1309 | "cli": ["--playlist-random"],
1310 | "description": "Download playlist videos in random order",
1311 | "default": False
1312 | },
1313 | "lazy_playlist": {
1314 | "category": OptionCategory.DOWNLOAD,
1315 | "type": OptionType.BOOLEAN,
1316 | "cli": ["--lazy-playlist"],
1317 | "description": "Process entries as they are received",
1318 | "default": False
1319 | },
1320 | "hls_use_mpegts": {
1321 | "category": OptionCategory.DOWNLOAD,
1322 | "type": OptionType.BOOLEAN,
1323 | "cli": ["--hls-use-mpegts"],
1324 | "description": "Use mpegts container for HLS videos",
1325 | "default": None
1326 | },
1327 | "download_sections": {
1328 | "category": OptionCategory.DOWNLOAD,
1329 | "type": OptionType.STRING,
1330 | "cli": ["--download-sections"],
1331 | "description": "Download only chapters matching regex",
1332 | "default": None,
1333 | "placeholder": "*10:15-inf or intro"
1334 | },
1335 | "external_downloader": {
1336 | "category": OptionCategory.DOWNLOAD,
1337 | "type": OptionType.STRING,
1338 | "cli": ["--downloader"],
1339 | "description": "Name or path of external downloader to use",
1340 | "default": None,
1341 | "placeholder": "aria2c, curl, wget"
1342 | },
1343 | "external_downloader_args": {
1344 | "category": OptionCategory.DOWNLOAD,
1345 | "type": OptionType.STRING,
1346 | "cli": ["--downloader-args"],
1347 | "description": "Arguments to give to external downloader",
1348 | "default": None,
1349 | "placeholder": "aria2c:--max-tries=10"
1350 | },
1351 |
1352 | # Additional Filesystem Options
1353 | "paths": {
1354 | "category": OptionCategory.FILESYSTEM,
1355 | "type": OptionType.STRING,
1356 | "cli": ["-P", "--paths"],
1357 | "description": "The paths where files should be downloaded",
1358 | "default": None,
1359 | "placeholder": "TYPES:PATH"
1360 | },
1361 | "output_na_placeholder": {
1362 | "category": OptionCategory.FILESYSTEM,
1363 | "type": OptionType.STRING,
1364 | "cli": ["--output-na-placeholder"],
1365 | "description": "Placeholder for unavailable fields in output",
1366 | "default": "NA",
1367 | "placeholder": "N/A"
1368 | },
1369 | "restrict_filenames": {
1370 | "category": OptionCategory.FILESYSTEM,
1371 | "type": OptionType.BOOLEAN,
1372 | "cli": ["--restrict-filenames"],
1373 | "description": "Restrict filenames to only ASCII characters",
1374 | "default": False
1375 | },
1376 | "windows_filenames": {
1377 | "category": OptionCategory.FILESYSTEM,
1378 | "type": OptionType.BOOLEAN,
1379 | "cli": ["--windows-filenames"],
1380 | "description": "Force filenames to be Windows-compatible",
1381 | "default": False
1382 | },
1383 | "trim_filenames": {
1384 | "category": OptionCategory.FILESYSTEM,
1385 | "type": OptionType.NUMBER,
1386 | "cli": ["--trim-filenames"],
1387 | "description": "Limit filename length (excluding extension)",
1388 | "default": None,
1389 | "min": 1
1390 | },
1391 | "no_overwrites": {
1392 | "category": OptionCategory.FILESYSTEM,
1393 | "type": OptionType.BOOLEAN,
1394 | "cli": ["-w", "--no-overwrites"],
1395 | "description": "Do not overwrite any files",
1396 | "default": False
1397 | },
1398 | "force_overwrites": {
1399 | "category": OptionCategory.FILESYSTEM,
1400 | "type": OptionType.BOOLEAN,
1401 | "cli": ["--force-overwrites"],
1402 | "description": "Overwrite all video and metadata files",
1403 | "default": False
1404 | },
1405 | "continue_dl": {
1406 | "category": OptionCategory.FILESYSTEM,
1407 | "type": OptionType.BOOLEAN,
1408 | "cli": ["-c", "--continue"],
1409 | "description": "Resume partially downloaded files/fragments",
1410 | "default": True
1411 | },
1412 | "no_part": {
1413 | "category": OptionCategory.FILESYSTEM,
1414 | "type": OptionType.BOOLEAN,
1415 | "cli": ["--no-part"],
1416 | "description": "Do not use .part files",
1417 | "default": False
1418 | },
1419 | "mtime": {
1420 | "category": OptionCategory.FILESYSTEM,
1421 | "type": OptionType.BOOLEAN,
1422 | "cli": ["--mtime"],
1423 | "description": "Use Last-modified header to set file modification time",
1424 | "default": False
1425 | },
1426 | "write_description": {
1427 | "category": OptionCategory.FILESYSTEM,
1428 | "type": OptionType.BOOLEAN,
1429 | "cli": ["--write-description"],
1430 | "description": "Write video description to .description file",
1431 | "default": False
1432 | },
1433 | "write_info_json": {
1434 | "category": OptionCategory.FILESYSTEM,
1435 | "type": OptionType.BOOLEAN,
1436 | "cli": ["--write-info-json"],
1437 | "description": "Write video metadata to .info.json file",
1438 | "default": False
1439 | },
1440 | "write_playlist_metafiles": {
1441 | "category": OptionCategory.FILESYSTEM,
1442 | "type": OptionType.BOOLEAN,
1443 | "cli": ["--write-playlist-metafiles"],
1444 | "description": "Write playlist metadata in addition to video metadata",
1445 | "default": True
1446 | },
1447 | "clean_infojson": {
1448 | "category": OptionCategory.FILESYSTEM,
1449 | "type": OptionType.BOOLEAN,
1450 | "cli": ["--clean-info-json"],
1451 | "description": "Remove some internal metadata from infojson",
1452 | "default": True
1453 | },
1454 | "write_comments": {
1455 | "category": OptionCategory.FILESYSTEM,
1456 | "type": OptionType.BOOLEAN,
1457 | "cli": ["--write-comments"],
1458 | "description": "Retrieve video comments to be placed in infojson",
1459 | "default": False
1460 | },
1461 | "load_info_json": {
1462 | "category": OptionCategory.FILESYSTEM,
1463 | "type": OptionType.FILE_PATH,
1464 | "cli": ["--load-info-json"],
1465 | "description": "JSON file containing video information",
1466 | "default": None
1467 | },
1468 | "cache_dir": {
1469 | "category": OptionCategory.FILESYSTEM,
1470 | "type": OptionType.STRING,
1471 | "cli": ["--cache-dir"],
1472 | "description": "Location in filesystem where yt-dlp can store information permanently",
1473 | "default": None,
1474 | "placeholder": "/path/to/cache"
1475 | },
1476 | "no_cache_dir": {
1477 | "category": OptionCategory.FILESYSTEM,
1478 | "type": OptionType.BOOLEAN,
1479 | "cli": ["--no-cache-dir"],
1480 | "description": "Disable filesystem caching",
1481 | "default": False
1482 | },
1483 | "rm_cache_dir": {
1484 | "category": OptionCategory.FILESYSTEM,
1485 | "type": OptionType.BOOLEAN,
1486 | "cli": ["--rm-cache-dir"],
1487 | "description": "Delete all filesystem cache files",
1488 | "default": False
1489 | },
1490 |
1491 | # Live Stream Options
1492 | "live_from_start": {
1493 | "category": OptionCategory.DOWNLOAD,
1494 | "type": OptionType.BOOLEAN,
1495 | "cli": ["--live-from-start"],
1496 | "description": "Download livestreams from the start",
1497 | "default": False
1498 | },
1499 | "wait_for_video": {
1500 | "category": OptionCategory.DOWNLOAD,
1501 | "type": OptionType.STRING,
1502 | "cli": ["--wait-for-video"],
1503 | "description": "Wait for scheduled streams (MIN[-MAX] seconds)",
1504 | "default": None,
1505 | "placeholder": "300 or 60-600"
1506 | },
1507 | "hls_prefer_native": {
1508 | "category": OptionCategory.DOWNLOAD,
1509 | "type": OptionType.BOOLEAN,
1510 | "cli": ["--hls-prefer-native"],
1511 | "description": "Use native HLS downloader instead of ffmpeg",
1512 | "default": False
1513 | },
1514 | "hls_prefer_ffmpeg": {
1515 | "category": OptionCategory.DOWNLOAD,
1516 | "type": OptionType.BOOLEAN,
1517 | "cli": ["--hls-prefer-ffmpeg"],
1518 | "description": "Use ffmpeg instead of native HLS downloader",
1519 | "default": False
1520 | },
1521 |
1522 | # Download Sections/Chapters
1523 | "download_sections": {
1524 | "category": OptionCategory.DOWNLOAD,
1525 | "type": OptionType.STRING,
1526 | "cli": ["--download-sections"],
1527 | "description": "Download only chapters matching regex or time ranges",
1528 | "default": None,
1529 | "placeholder": "*10:15-inf or intro"
1530 | },
1531 | "remove_chapters": {
1532 | "category": OptionCategory.POST_PROCESSING,
1533 | "type": OptionType.STRING,
1534 | "cli": ["--remove-chapters"],
1535 | "description": "Remove chapters matching regex from video",
1536 | "default": None,
1537 | "placeholder": "sponsor|intro"
1538 | },
1539 |
1540 | # Additional Metadata Options
1541 | "parse_metadata": {
1542 | "category": OptionCategory.POST_PROCESSING,
1543 | "type": OptionType.STRING,
1544 | "cli": ["--parse-metadata"],
1545 | "description": "Parse additional metadata from filename/path",
1546 | "default": None,
1547 | "placeholder": "FROM_FIELD:TO_FIELD"
1548 | },
1549 | "replace_in_metadata": {
1550 | "category": OptionCategory.POST_PROCESSING,
1551 | "type": OptionType.STRING,
1552 | "cli": ["--replace-in-metadata"],
1553 | "description": "Replace text in metadata fields",
1554 | "default": None,
1555 | "placeholder": "FIELD REGEX REPLACE"
1556 | },
1557 |
1558 | # Update and Version Options
1559 | "update": {
1560 | "category": OptionCategory.GENERAL,
1561 | "type": OptionType.BOOLEAN,
1562 | "cli": ["--update"],
1563 | "description": "Update yt-dlp to the latest version",
1564 | "default": False
1565 | },
1566 | "update_self": {
1567 | "category": OptionCategory.GENERAL,
1568 | "type": OptionType.BOOLEAN,
1569 | "cli": ["--update-self"],
1570 | "description": "Update yt-dlp to the latest version (alias)",
1571 | "default": False
1572 | },
1573 | "version": {
1574 | "category": OptionCategory.VERBOSITY,
1575 | "type": OptionType.BOOLEAN,
1576 | "cli": ["--version"],
1577 | "description": "Print program version and exit",
1578 | "default": False
1579 | },
1580 |
1581 | # Archive Options
1582 | "download_archive": {
1583 | "category": OptionCategory.VIDEO_SELECTION,
1584 | "type": OptionType.FILE_PATH,
1585 | "cli": ["--download-archive"],
1586 | "description": "Download only videos not listed in archive file",
1587 | "default": None
1588 | },
1589 | "record_download_archive": {
1590 | "category": OptionCategory.VIDEO_SELECTION,
1591 | "type": OptionType.BOOLEAN,
1592 | "cli": ["--record-download-archive"],
1593 | "description": "Record downloaded videos in archive file",
1594 | "default": False
1595 | }
1596 | }
1597 |
1598 | def get_options_by_category() -> Dict[OptionCategory, Dict[str, Dict]]:
1599 | """Group options by category for UI rendering"""
1600 | grouped = {}
1601 | for key, option in YT_DLP_OPTIONS.items():
1602 | category = option["category"]
1603 | if category not in grouped:
1604 | grouped[category] = {}
1605 | grouped[category][key] = option
1606 | return grouped
1607 |
1608 | def get_default_options() -> Dict[str, Any]:
1609 | """Get default values for all options"""
1610 | return {key: option.get("default") for key, option in YT_DLP_OPTIONS.items()}
1611 |
1612 | def convert_to_ydl_opts(form_data: Dict[str, Any]) -> Dict[str, Any]:
1613 | """Convert form data to yt-dlp options dictionary"""
1614 | ydl_opts = {}
1615 |
1616 | for key, value in form_data.items():
1617 | if key in YT_DLP_OPTIONS and value is not None and value != "":
1618 | option_config = YT_DLP_OPTIONS[key]
1619 |
1620 | # Handle different option types
1621 | if option_config["type"] == OptionType.BOOLEAN:
1622 | if isinstance(value, str):
1623 | ydl_opts[key] = value.lower() in ('true', '1', 'on', 'yes')
1624 | else:
1625 | ydl_opts[key] = bool(value)
1626 | elif option_config["type"] == OptionType.NUMBER:
1627 | try:
1628 | ydl_opts[key] = int(value) if isinstance(value, str) else value
1629 | except ValueError:
1630 | continue
1631 | elif option_config["type"] == OptionType.MULTI_SELECT:
1632 | if isinstance(value, str):
1633 | ydl_opts[key] = [v.strip() for v in value.split(',') if v.strip()]
1634 | elif isinstance(value, list):
1635 | ydl_opts[key] = value
1636 | else:
1637 | ydl_opts[key] = value
1638 |
1639 | return ydl_opts
--------------------------------------------------------------------------------