├── 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 | carbonatedWater.org Logo 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 | 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 | 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 | 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 | 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 --------------------------------------------------------------------------------