Taf2023 commited on
Commit
c0a4ca6
·
verified ·
1 Parent(s): e5ae78e

Upload 8 files

Browse files
Files changed (8) hide show
  1. Dockerfile +24 -0
  2. README.md +369 -4
  3. app.py +23 -0
  4. backend.py +489 -0
  5. index.html +231 -0
  6. requirements.txt +30 -0
  7. script.js +921 -0
  8. style.css +1105 -0
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ ffmpeg \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements and install Python dependencies
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application code
15
+ COPY . .
16
+
17
+ # Create download directory
18
+ RUN mkdir -p /app/downloads
19
+
20
+ # Expose port
21
+ EXPOSE 7860
22
+
23
+ # Run the application
24
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
README.md CHANGED
@@ -1,11 +1,376 @@
1
  ---
2
  title: Universal Media Downloader
3
- emoji: 📉
4
  colorFrom: blue
5
- colorTo: green
6
  sdk: docker
 
 
7
  pinned: false
8
- short_description: Universal-Media-Downloader
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Universal Media Downloader
3
+ emoji: 🎥
4
  colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ sdk_version: "1.4.0"
8
+ app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
+ A professional, accessible web application for downloading videos and audio from 1000+ platforms using yt-dlp. Built with accessibility-first design, mobile optimization, and automatic library updates.
13
+
14
+ ## Features
15
+
16
+ ### 🎥 **Universal Platform Support**
17
+ - YouTube, Vimeo, Dailymotion, Twitch
18
+ - TikTok, Instagram, Twitter/X, Facebook
19
+ - Reddit, SoundCloud, Spotify, Bandcamp
20
+ - 1000+ additional platforms via yt-dlp
21
+
22
+ ### ♿ **Accessibility First**
23
+ - Screen reader compatible (ARIA labels, live regions)
24
+ - Keyboard navigation support
25
+ - High contrast design (WCAG AAA compliant)
26
+ - Large touch targets for mobile users
27
+ - Voice announcements for real-time status
28
+
29
+ ### 📱 **Mobile Optimized**
30
+ - Responsive design with touch-friendly interface
31
+ - Progressive Web App (PWA) capabilities
32
+ - Offline mode detection
33
+ - Optimized for all screen sizes
34
+
35
+ ### 🔄 **Smart Features**
36
+ - Automatic yt-dlp updates (24-hour intervals)
37
+ - Real-time download progress tracking
38
+ - Format selection with quality options
39
+ - Download queue management
40
+ - Privacy mode support
41
+ - Download history
42
+
43
+ ### 🛠 **Technical Features**
44
+ - RESTful API backend (Flask)
45
+ - Real-time progress monitoring
46
+ - Error handling and validation
47
+ - Service worker for caching
48
+ - CORS support for cross-origin requests
49
+
50
+ ## Quick Start
51
+
52
+ ### Deploy to Hugging Face Spaces
53
+
54
+ #### Docker Spaces (Recommended)
55
+
56
+ 1. **Create a new Space:**
57
+ - Go to [Hugging Face Spaces](https://huggingface.co/spaces)
58
+ - Click "Create new Space"
59
+ - Choose "Docker" as the SDK
60
+ - Name your space (e.g., `your-username/media-downloader`)
61
+
62
+ 2. **Upload files:**
63
+ - Upload all files from this repository
64
+ - This README.md includes the required Docker configuration
65
+ - The Dockerfile will be used to build the container
66
+
67
+ 3. **Build and Deploy:**
68
+ - The Dockerfile contains all necessary dependencies
69
+ - Container will be automatically built and deployed
70
+ - Access your application at the provided space URL
71
+
72
+ ### Local Development
73
+
74
+ #### Option 1: Direct Python (Development)
75
+ ```bash
76
+ # Clone and install dependencies
77
+ git clone <repository-url>
78
+ cd universal-media-downloader
79
+ pip install -r requirements.txt
80
+
81
+ # Run the Flask application
82
+ python app.py
83
+ ```
84
+
85
+ The application will be available at `http://localhost:7860`
86
+
87
+ #### Option 2: Docker (Production)
88
+ ```bash
89
+ # Build the Docker image
90
+ docker build -t media-downloader .
91
+
92
+ # Run the container
93
+ docker run -p 7860:7860 media-downloader
94
+ ```
95
+
96
+ ## Usage
97
+
98
+ ### Web Interface
99
+
100
+ The Flask application provides a complete web interface for media downloading:
101
+
102
+ 1. **Enter URL**: Paste any media URL in the input field
103
+ 2. **Analyze**: Click "Get Formats" to fetch available options
104
+ 3. **Select Format**: Choose your preferred quality and format
105
+ 4. **Download**: Click "Download" to start the download
106
+ 5. **Monitor**: Track progress and view download history
107
+
108
+ ### API Usage
109
+
110
+ The application provides a RESTful API for programmatic access:
111
+
112
+ ```python
113
+ import requests
114
+
115
+ # Get available formats
116
+ response = requests.post('http://localhost:7860/api/formats',
117
+ json={'url': 'https://www.youtube.com/watch?v=...'})
118
+ formats = response.json()
119
+
120
+ # Start download
121
+ download_response = requests.post('http://localhost:7860/api/download',
122
+ json={
123
+ 'url': 'https://www.youtube.com/watch?v=...',
124
+ 'format_id': 'best'
125
+ })
126
+ ```
127
+
128
+ ## API Documentation
129
+
130
+ ### Endpoints
131
+
132
+ #### `GET /api/health`
133
+ Check API health status and yt-dlp version.
134
+
135
+ **Response:**
136
+ ```json
137
+ {
138
+ "status": "healthy",
139
+ "yt_dlp_version": "2023.11.16",
140
+ "last_update_check": "2025-11-07T10:24:00Z",
141
+ "active_downloads": 2,
142
+ "queue_size": 1
143
+ }
144
+ ```
145
+
146
+ #### `POST /api/formats`
147
+ Extract available formats from a URL.
148
+
149
+ **Request:**
150
+ ```json
151
+ {
152
+ "url": "https://www.youtube.com/watch?v=..."
153
+ }
154
+ ```
155
+
156
+ **Response:**
157
+ ```json
158
+ {
159
+ "success": true,
160
+ "title": "Video Title",
161
+ "uploader": "Channel Name",
162
+ "platform": "YouTube",
163
+ "duration": 180,
164
+ "thumbnail": "https://...",
165
+ "formats": [
166
+ {
167
+ "id": "best",
168
+ "ext": "mp4",
169
+ "vcodec": "h264",
170
+ "acodec": "aac",
171
+ "width": 1920,
172
+ "height": 1080,
173
+ "filesize": 104857600,
174
+ "format_note": "1080p",
175
+ "type": "video"
176
+ }
177
+ ]
178
+ }
179
+ ```
180
+
181
+ #### `POST /api/download`
182
+ Start a download.
183
+
184
+ **Request:**
185
+ ```json
186
+ {
187
+ "url": "https://www.youtube.com/watch?v=...",
188
+ "format_id": "best",
189
+ "download_id": "dl_1234567890"
190
+ }
191
+ ```
192
+
193
+ #### `GET /api/progress/<download_id>`
194
+ Get download progress.
195
+
196
+ **Response:**
197
+ ```json
198
+ {
199
+ "success": true,
200
+ "download_id": "dl_1234567890",
201
+ "progress": {
202
+ "status": "downloading",
203
+ "percentage": 45.7,
204
+ "speed": 1024000,
205
+ "eta": 120,
206
+ "filename": "video.mp4"
207
+ }
208
+ }
209
+ ```
210
+
211
+ #### `POST /api/update`
212
+ Manually trigger yt-dlp update.
213
+
214
+ #### `GET /api/supported-platforms`
215
+ Get list of supported platforms.
216
+
217
+ ## Supported Platforms
218
+
219
+ ### Video Platforms
220
+ - YouTube (including 4K, 8K, live streams)
221
+ - Vimeo (including Vimeo Pro, premium content)
222
+ - Dailymotion
223
+ - Twitch (streams and VODs)
224
+ - TikTok (videos and live streams)
225
+ - Instagram (posts, reels, stories)
226
+ - Twitter/X (videos)
227
+ - Facebook (public videos)
228
+ - Reddit (video posts)
229
+
230
+ ### Audio Platforms
231
+ - SoundCloud
232
+ - Spotify (non-premium limitations apply)
233
+ - Bandcamp
234
+ - Audiomack
235
+ - Mixcloud
236
+
237
+ ### Other Platforms
238
+ - 1000+ additional platforms supported by yt-dlp
239
+
240
+ ## Accessibility Features
241
+
242
+ ### Screen Reader Support
243
+ - Complete ARIA labeling for all interactive elements
244
+ - Live region announcements for status updates
245
+ - Semantic HTML structure
246
+ - Proper heading hierarchy
247
+
248
+ ### Keyboard Navigation
249
+ - Full keyboard accessibility
250
+ - Tab order optimization
251
+ - Escape key support for modals
252
+ - Enter/Space activation for buttons
253
+
254
+ ### Visual Accessibility
255
+ - High contrast color scheme (WCAG AAA compliant)
256
+ - Large touch targets (minimum 44px)
257
+ - Clear focus indicators
258
+ - Scalable fonts and UI elements
259
+
260
+ ### Motor Accessibility
261
+ - Large clickable areas
262
+ - Generous spacing between elements
263
+ - Touch-friendly interface design
264
+
265
+ ## Configuration
266
+
267
+ ### Environment Variables
268
+
269
+ For production deployment, you can set these environment variables:
270
+
271
+ ```bash
272
+ PORT=7860 # Port for the application
273
+ DEBUG=False # Debug mode (set to False in production)
274
+ HF_TOKEN=your_token_here # Optional: Hugging Face token
275
+ ```
276
+
277
+ ### Docker Build Configuration
278
+
279
+ The Dockerfile includes:
280
+ - **Python 3.11 base image** for optimal performance
281
+ - **FFmpeg** for video/audio processing
282
+ - **Gunicorn** WSGI server for production
283
+ - **Port 7860** exposed for HTTP traffic
284
+ - **Automatic dependency installation** from requirements.txt
285
+
286
+ ### Build Process
287
+
288
+ 1. Base image download (python:3.11-slim)
289
+ 2. System dependencies installation (ffmpeg)
290
+ 3. Python packages installation from requirements.txt
291
+ 4. Application code copying
292
+ 5. Download directory creation
293
+ 6. Container startup with Gunicorn
294
+
295
+ ### Auto-Update Settings
296
+
297
+ The application automatically updates yt-dlp every 24 hours to support new platforms and features. You can:
298
+
299
+ - Enable/disable auto-updates in settings
300
+ - Manually trigger updates via the API
301
+ - Monitor update status in the health endpoint
302
+
303
+ ## Troubleshooting
304
+
305
+ ### Common Issues
306
+
307
+ 1. **"Connection failed"**
308
+ - Check if the backend service is running
309
+ - Verify the API base URL
310
+ - Check network connectivity
311
+
312
+ 2. **"yt-dlp update failed"**
313
+ - Manually update via API endpoint
314
+ - Check internet connection
315
+ - Verify package installation
316
+
317
+ 3. **"Download failed"**
318
+ - Verify the URL is valid and accessible
319
+ - Check if the platform is supported
320
+ - Try a different format option
321
+
322
+ ### Logs and Debugging
323
+
324
+ - **Application logs**: Check the terminal output
325
+ - **Browser console**: Use F12 developer tools
326
+ - **API logs**: Monitor network requests in dev tools
327
+
328
+ ## Technical Architecture
329
+
330
+ ### Frontend
331
+ - **HTML5**: Semantic structure with accessibility features
332
+ - **CSS3**: Modern styling with CSS Grid and Flexbox
333
+ - **JavaScript ES6+**: Progressive enhancement
334
+ - **PWA**: Service worker for offline capabilities
335
+
336
+ ### Backend
337
+ - **Flask**: RESTful API framework
338
+ - **yt-dlp**: Video/audio extraction engine
339
+ - **APScheduler**: Scheduled task management
340
+ - **CORS**: Cross-origin resource sharing
341
+
342
+ ### Deployment
343
+ - **Hugging Face Spaces**: Primary deployment target
344
+ - **Docker**: Alternative deployment option
345
+ - **Gunicorn**: Production WSGI server
346
+
347
+ ## Contributing
348
+
349
+ 1. Fork the repository
350
+ 2. Create a feature branch
351
+ 3. Make your changes
352
+ 4. Test thoroughly
353
+ 5. Submit a pull request
354
+
355
+ ## License
356
+
357
+ This project is open source and available under the MIT License.
358
+
359
+ ## Support
360
+
361
+ For issues and questions:
362
+ - Check the troubleshooting section
363
+ - Review API documentation
364
+ - Submit an issue on GitHub
365
+
366
+ ## Acknowledgments
367
+
368
+ - **yt-dlp**: The core download engine
369
+ - **Flask**: Web framework
370
+ - **Gradio**: User interface framework
371
+ - **Inter Font**: Typography
372
+ - **Accessibility Guidelines**: WCAG compliance
373
+
374
+ ---
375
+
376
+ **⚠️ Disclaimer**: This tool is for educational and personal use only. Please respect content creators' rights and platform terms of service.
app.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Entry point for Hugging Face Spaces deployment
4
+ """
5
+
6
+ import os
7
+ import sys
8
+
9
+ # Add current directory to Python path
10
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
+
12
+ # Import and run the backend
13
+ from backend import app, ytdlp_manager
14
+
15
+ if __name__ == '__main__':
16
+ port = int(os.environ.get('PORT', 7860))
17
+ debug = os.environ.get('DEBUG', 'False').lower() == 'true'
18
+
19
+ print(f"Starting Universal Media Downloader on port {port}")
20
+ print(f"Debug mode: {debug}")
21
+ print(f"yt-dlp version: {ytdlp_manager}")
22
+
23
+ app.run(host='0.0.0.0', port=port, debug=debug)
backend.py ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Universal Media Downloader Backend API
4
+ Built with Flask and yt-dlp for platform-agnostic media downloading
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import time
11
+ import threading
12
+ import subprocess
13
+ from datetime import datetime
14
+ from flask import Flask, request, jsonify, send_file, send_from_directory
15
+ from flask_cors import CORS
16
+ from werkzeug.exceptions import BadRequest, InternalServerError
17
+ import logging
18
+
19
+ # Configure logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
23
+ handlers=[
24
+ logging.FileHandler('app.log'),
25
+ logging.StreamHandler(sys.stdout)
26
+ ]
27
+ )
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Initialize Flask app
31
+ app = Flask(__name__)
32
+ CORS(app)
33
+
34
+ # Global variables
35
+ yt_dlp_version = None
36
+ last_update_check = None
37
+ download_queue = []
38
+ download_history = []
39
+ active_downloads = {}
40
+
41
+ class YTDLPManager:
42
+ """Manages yt-dlp operations with automatic updates"""
43
+
44
+ def __init__(self):
45
+ self.ensure_ytdlp_installed()
46
+ self.update_yt_dlp()
47
+
48
+ @staticmethod
49
+ def format_file_size(bytes_size):
50
+ """Format file size in human readable format"""
51
+ if not bytes_size or bytes_size == 0:
52
+ return "Unknown"
53
+
54
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
55
+ if bytes_size < 1024.0:
56
+ return f"{bytes_size:.1f} {unit}"
57
+ bytes_size /= 1024.0
58
+ return f"{bytes_size:.1f} PB"
59
+
60
+ @staticmethod
61
+ def format_time(seconds):
62
+ """Format time duration in human readable format"""
63
+ if not seconds:
64
+ return "Unknown"
65
+
66
+ hours = int(seconds // 3600)
67
+ minutes = int((seconds % 3600) // 60)
68
+ secs = int(seconds % 60)
69
+
70
+ if hours > 0:
71
+ return f"{hours}:{minutes:02d}:{secs:02d}"
72
+ else:
73
+ return f"{minutes}:{secs:02d}"
74
+
75
+ def ensure_ytdlp_installed(self):
76
+ """Ensure yt-dlp is installed"""
77
+ try:
78
+ import yt_dlp
79
+ logger.info("yt-dlp is available")
80
+ except ImportError:
81
+ logger.info("Installing yt-dlp...")
82
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'yt-dlp'])
83
+ logger.info("yt-dlp installed successfully")
84
+
85
+ def update_yt_dlp(self):
86
+ """Update yt-dlp to latest version"""
87
+ global yt_dlp_version, last_update_check
88
+
89
+ try:
90
+ logger.info("Checking for yt-dlp updates...")
91
+ result = subprocess.run([
92
+ sys.executable, '-m', 'pip', 'install', '--upgrade', 'yt-dlp'
93
+ ], capture_output=True, text=True, timeout=300)
94
+
95
+ if result.returncode == 0:
96
+ # Get version info
97
+ version_result = subprocess.run([
98
+ sys.executable, '-m', 'yt_dlp', '--version'
99
+ ], capture_output=True, text=True)
100
+
101
+ if version_result.returncode == 0:
102
+ yt_dlp_version = version_result.stdout.strip()
103
+ last_update_check = datetime.now()
104
+ logger.info(f"yt-dlp updated to version: {yt_dlp_version}")
105
+ return True
106
+ else:
107
+ logger.warning("Could not get yt-dlp version after update")
108
+ else:
109
+ logger.warning(f"yt-dlp update failed: {result.stderr}")
110
+
111
+ except Exception as e:
112
+ logger.error(f"Error updating yt-dlp: {e}")
113
+
114
+ return False
115
+
116
+ def get_formats(self, url):
117
+ """Extract available formats from URL"""
118
+ try:
119
+ import yt_dlp
120
+
121
+ ydl_opts = {
122
+ 'quiet': True,
123
+ 'no_warnings': True,
124
+ 'extract_flat': False,
125
+ }
126
+
127
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
128
+ info = ydl.extract_info(url, download=False)
129
+
130
+ formats = []
131
+ if 'formats' in info:
132
+ for fmt in info['formats']:
133
+ if fmt.get('vcodec') != 'none' or fmt.get('acodec') != 'none':
134
+ format_info = {
135
+ 'id': fmt.get('format_id', 'unknown'),
136
+ 'ext': fmt.get('ext', 'unknown'),
137
+ 'vcodec': fmt.get('vcodec', 'none'),
138
+ 'acodec': fmt.get('acodec', 'none'),
139
+ 'width': fmt.get('width'),
140
+ 'height': fmt.get('height'),
141
+ 'fps': fmt.get('fps'),
142
+ 'filesize': fmt.get('filesize') or fmt.get('filesize_approx'),
143
+ 'format_note': fmt.get('format_note', ''),
144
+ 'url': fmt.get('url', ''),
145
+ 'type': 'video' if fmt.get('vcodec') != 'none' else 'audio'
146
+ }
147
+ formats.append(format_info)
148
+
149
+ # Also include direct download if available
150
+ if info.get('url'):
151
+ formats.append({
152
+ 'id': 'direct',
153
+ 'ext': info.get('ext', 'mp4'),
154
+ 'vcodec': info.get('vcodec', 'none'),
155
+ 'acodec': info.get('acodec', 'none'),
156
+ 'width': info.get('width'),
157
+ 'height': info.get('height'),
158
+ 'fps': info.get('fps'),
159
+ 'filesize': info.get('filesize'),
160
+ 'format_note': 'Direct',
161
+ 'url': info.get('url'),
162
+ 'type': 'video' if info.get('vcodec') != 'none' else 'audio'
163
+ })
164
+
165
+ return {
166
+ 'success': True,
167
+ 'title': info.get('title', 'Unknown Title'),
168
+ 'uploader': info.get('uploader', 'Unknown Uploader'),
169
+ 'duration': info.get('duration'),
170
+ 'thumbnail': info.get('thumbnail'),
171
+ 'formats': formats,
172
+ 'platform': self.detect_platform(url),
173
+ 'view_count': info.get('view_count'),
174
+ 'like_count': info.get('like_count')
175
+ }
176
+
177
+ except Exception as e:
178
+ logger.error(f"Error extracting formats from {url}: {e}")
179
+ return {
180
+ 'success': False,
181
+ 'error': str(e),
182
+ 'message': 'Failed to extract formats. Please check the URL and try again.'
183
+ }
184
+
185
+ def detect_platform(self, url):
186
+ """Detect the platform from URL"""
187
+ platforms = {
188
+ 'youtube.com': 'YouTube',
189
+ 'youtu.be': 'YouTube',
190
+ 'vimeo.com': 'Vimeo',
191
+ 'dailymotion.com': 'Dailymotion',
192
+ 'twitch.tv': 'Twitch',
193
+ 'tiktok.com': 'TikTok',
194
+ 'instagram.com': 'Instagram',
195
+ 'twitter.com': 'Twitter',
196
+ 'x.com': 'Twitter',
197
+ 'facebook.com': 'Facebook',
198
+ 'reddit.com': 'Reddit',
199
+ 'soundcloud.com': 'SoundCloud',
200
+ 'spotify.com': 'Spotify',
201
+ 'bandcamp.com': 'Bandcamp'
202
+ }
203
+
204
+ url_lower = url.lower()
205
+ for domain, platform in platforms.items():
206
+ if domain in url_lower:
207
+ return platform
208
+
209
+ return 'Unknown'
210
+
211
+ def start_download(self, url, format_id, download_id):
212
+ """Start downloading with progress tracking"""
213
+ try:
214
+ import yt_dlp
215
+
216
+ # Create output directory
217
+ output_dir = 'downloads'
218
+ os.makedirs(output_dir, exist_ok=True)
219
+
220
+ # Setup progress hook
221
+ def progress_hook(d):
222
+ if d['status'] == 'downloading':
223
+ if 'total_bytes' in d:
224
+ percentage = d['downloaded_bytes'] / d['total_bytes'] * 100
225
+ elif 'total_bytes_estimate' in d:
226
+ percentage = d['downloaded_bytes'] / d['total_bytes_estimate'] * 100
227
+ else:
228
+ percentage = 0
229
+
230
+ active_downloads[download_id] = {
231
+ 'status': 'downloading',
232
+ 'percentage': percentage,
233
+ 'speed': d.get('speed', 0),
234
+ 'eta': d.get('eta', 0),
235
+ 'filename': d.get('filename', ''),
236
+ 'downloaded_bytes': d.get('downloaded_bytes', 0),
237
+ 'total_bytes': d.get('total_bytes', 0)
238
+ }
239
+ elif d['status'] == 'finished':
240
+ active_downloads[download_id] = {
241
+ 'status': 'finished',
242
+ 'filename': d.get('filename', ''),
243
+ 'completed': True
244
+ }
245
+
246
+ ydl_opts = {
247
+ 'outtmpl': f'{output_dir}/%(title)s.%(ext)s',
248
+ 'format': format_id if format_id != 'direct' else 'best',
249
+ 'progress_hooks': [progress_hook],
250
+ }
251
+
252
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
253
+ ydl.download([url])
254
+
255
+ # Move to history
256
+ if download_id in active_downloads:
257
+ download_info = active_downloads[download_id].copy()
258
+ download_info.update({
259
+ 'url': url,
260
+ 'format_id': format_id,
261
+ 'download_id': download_id,
262
+ 'start_time': datetime.now().isoformat()
263
+ })
264
+ download_history.append(download_info)
265
+ del active_downloads[download_id]
266
+
267
+ except Exception as e:
268
+ logger.error(f"Download error: {e}")
269
+ active_downloads[download_id] = {
270
+ 'status': 'error',
271
+ 'error': str(e)
272
+ }
273
+
274
+ # Initialize manager
275
+ ytdlp_manager = YTDLPManager()
276
+
277
+ # Background update checker
278
+ def periodic_update_check():
279
+ """Check for updates every 24 hours"""
280
+ while True:
281
+ try:
282
+ time.sleep(24 * 60 * 60) # 24 hours
283
+ ytdlp_manager.update_yt_dlp()
284
+ except Exception as e:
285
+ logger.error(f"Periodic update check failed: {e}")
286
+
287
+ # Start background thread for updates
288
+ update_thread = threading.Thread(target=periodic_update_check, daemon=True)
289
+ update_thread.start()
290
+
291
+ # Frontend Routes
292
+
293
+ @app.route('/', methods=['GET'])
294
+ def serve_frontend():
295
+ """Serve the main frontend page"""
296
+ return send_from_directory('.', 'index.html')
297
+
298
+ @app.route('/style.css', methods=['GET'])
299
+ def serve_css():
300
+ """Serve CSS file"""
301
+ return send_from_directory('.', 'style.css')
302
+
303
+ @app.route('/script.js', methods=['GET'])
304
+ def serve_js():
305
+ """Serve JavaScript file"""
306
+ return send_from_directory('.', 'script.js')
307
+
308
+ # API Routes
309
+
310
+ @app.route('/api/health', methods=['GET'])
311
+ def health_check():
312
+ """Health check endpoint"""
313
+ return jsonify({
314
+ 'status': 'healthy',
315
+ 'yt_dlp_version': yt_dlp_version,
316
+ 'last_update_check': last_update_check.isoformat() if last_update_check else None,
317
+ 'active_downloads': len(active_downloads),
318
+ 'queue_size': len(download_queue)
319
+ })
320
+
321
+ @app.route('/api/formats', methods=['POST'])
322
+ def get_formats():
323
+ """Extract available formats from URL"""
324
+ try:
325
+ data = request.get_json()
326
+ if not data or 'url' not in data:
327
+ raise BadRequest("URL is required")
328
+
329
+ url = data['url'].strip()
330
+ if not url:
331
+ raise BadRequest("URL cannot be empty")
332
+
333
+ # Basic URL validation
334
+ if not url.startswith(('http://', 'https://')):
335
+ raise BadRequest("URL must start with http:// or https://")
336
+
337
+ logger.info(f"Extracting formats from: {url}")
338
+ result = ytdlp_manager.get_formats(url)
339
+
340
+ return jsonify(result)
341
+
342
+ except BadRequest as e:
343
+ return jsonify({'success': False, 'error': str(e)}), 400
344
+ except Exception as e:
345
+ logger.error(f"Format extraction error: {e}")
346
+ return jsonify({
347
+ 'success': False,
348
+ 'error': 'Internal server error',
349
+ 'message': 'An error occurred while processing your request.'
350
+ }), 500
351
+
352
+ @app.route('/api/download', methods=['POST'])
353
+ def start_download():
354
+ """Start a download"""
355
+ try:
356
+ data = request.get_json()
357
+ if not data or 'url' not in data or 'format_id' not in data:
358
+ raise BadRequest("URL and format_id are required")
359
+
360
+ url = data['url'].strip()
361
+ format_id = data['format_id']
362
+ download_id = data.get('download_id', f"dl_{int(time.time())}")
363
+
364
+ if not url.startswith(('http://', 'https://')):
365
+ raise BadRequest("Invalid URL format")
366
+
367
+ logger.info(f"Starting download: {url} with format: {format_id}")
368
+
369
+ # Start download in background thread
370
+ download_thread = threading.Thread(
371
+ target=ytdlp_manager.start_download,
372
+ args=(url, format_id, download_id)
373
+ )
374
+ download_thread.start()
375
+
376
+ return jsonify({
377
+ 'success': True,
378
+ 'download_id': download_id,
379
+ 'message': 'Download started'
380
+ })
381
+
382
+ except BadRequest as e:
383
+ return jsonify({'success': False, 'error': str(e)}), 400
384
+ except Exception as e:
385
+ logger.error(f"Download start error: {e}")
386
+ return jsonify({
387
+ 'success': False,
388
+ 'error': 'Internal server error'
389
+ }), 500
390
+
391
+ @app.route('/api/progress/<download_id>', methods=['GET'])
392
+ def get_download_progress(download_id):
393
+ """Get download progress"""
394
+ if download_id in active_downloads:
395
+ return jsonify({
396
+ 'success': True,
397
+ 'download_id': download_id,
398
+ 'progress': active_downloads[download_id]
399
+ })
400
+ else:
401
+ return jsonify({
402
+ 'success': False,
403
+ 'message': 'Download not found'
404
+ }), 404
405
+
406
+ @app.route('/api/downloads/active', methods=['GET'])
407
+ def get_active_downloads():
408
+ """Get all active downloads"""
409
+ return jsonify({
410
+ 'success': True,
411
+ 'downloads': list(active_downloads.values())
412
+ })
413
+
414
+ @app.route('/api/downloads/history', methods=['GET'])
415
+ def get_download_history():
416
+ """Get download history"""
417
+ limit = request.args.get('limit', 50, type=int)
418
+ return jsonify({
419
+ 'success': True,
420
+ 'history': download_history[-limit:]
421
+ })
422
+
423
+ @app.route('/api/update', methods=['POST'])
424
+ def manual_update():
425
+ """Manually trigger yt-dlp update"""
426
+ try:
427
+ success = ytdlp_manager.update_yt_dlp()
428
+ return jsonify({
429
+ 'success': success,
430
+ 'version': yt_dlp_version,
431
+ 'message': 'yt-dlp updated successfully' if success else 'Update check completed'
432
+ })
433
+ except Exception as e:
434
+ return jsonify({
435
+ 'success': False,
436
+ 'error': str(e)
437
+ }), 500
438
+
439
+ @app.route('/api/supported-platforms', methods=['GET'])
440
+ def get_supported_platforms():
441
+ """Get list of supported platforms"""
442
+ platforms = [
443
+ {'name': 'YouTube', 'domains': ['youtube.com', 'youtu.be']},
444
+ {'name': 'Vimeo', 'domains': ['vimeo.com']},
445
+ {'name': 'Dailymotion', 'domains': ['dailymotion.com']},
446
+ {'name': 'Twitch', 'domains': ['twitch.tv']},
447
+ {'name': 'TikTok', 'domains': ['tiktok.com']},
448
+ {'name': 'Instagram', 'domains': ['instagram.com']},
449
+ {'name': 'Twitter', 'domains': ['twitter.com', 'x.com']},
450
+ {'name': 'Facebook', 'domains': ['facebook.com']},
451
+ {'name': 'Reddit', 'domains': ['reddit.com']},
452
+ {'name': 'SoundCloud', 'domains': ['soundcloud.com']},
453
+ {'name': 'Spotify', 'domains': ['spotify.com']},
454
+ {'name': 'Bandcamp', 'domains': ['bandcamp.com']}
455
+ ]
456
+
457
+ return jsonify({
458
+ 'success': True,
459
+ 'platforms': platforms,
460
+ 'yt_dlp_version': yt_dlp_version
461
+ })
462
+
463
+ @app.route('/api/file/<filename>', methods=['GET'])
464
+ def download_file(filename):
465
+ """Serve downloaded files"""
466
+ try:
467
+ file_path = os.path.join('downloads', filename)
468
+ if os.path.exists(file_path):
469
+ return send_file(file_path, as_attachment=True)
470
+ else:
471
+ return jsonify({'error': 'File not found'}), 404
472
+ except Exception as e:
473
+ return jsonify({'error': str(e)}), 500
474
+
475
+ # Error handlers
476
+ @app.errorhandler(404)
477
+ def not_found(error):
478
+ return jsonify({'error': 'Endpoint not found'}), 404
479
+
480
+ @app.errorhandler(500)
481
+ def internal_error(error):
482
+ return jsonify({'error': 'Internal server error'}), 500
483
+
484
+ if __name__ == '__main__':
485
+ port = int(os.environ.get('PORT', 5000))
486
+ debug = os.environ.get('DEBUG', 'False').lower() == 'true'
487
+
488
+ logger.info(f"Starting Universal Media Downloader API on port {port}")
489
+ app.run(host='0.0.0.0', port=port, debug=debug)
index.html ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Professional media downloader with yt-dlp. Download from 1000+ platforms with advanced format selection and progress tracking.">
7
+ <title>Universal Media Downloader</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
12
+
13
+ <!-- PWA Meta Tags -->
14
+ <meta name="theme-color" content="#06B6D4">
15
+ <meta name="apple-mobile-web-app-capable" content="yes">
16
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
17
+ <meta name="apple-mobile-web-app-title" content="Media Downloader">
18
+
19
+ <!-- Service Worker -->
20
+ <link rel="manifest" href="/manifest.json">
21
+ </head>
22
+ <body>
23
+ <div class="app-container">
24
+ <!-- Header -->
25
+ <header class="app-header">
26
+ <div class="header-content">
27
+ <h1 class="app-title">Downloader</h1>
28
+ <div class="header-controls">
29
+ <button id="settings-btn" class="icon-btn" aria-label="Settings">
30
+ <svg viewBox="0 0 24 24" class="icon">
31
+ <path d="M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"/>
32
+ </svg>
33
+ </button>
34
+ <button id="update-btn" class="icon-btn" aria-label="Check for updates" title="Update yt-dlp">
35
+ <svg viewBox="0 0 24 24" class="icon">
36
+ <path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,6H13V11.59L17.59,16.59L16.17,18L11,12.83V6M12,4A1,1 0 0,1 13,5A1,1 0 0,1 12,6A1,1 0 0,1 11,5A1,1 0 0,1 12,4M7,12A1,1 0 0,1 8,11A1,1 0 0,1 7,10A1,1 0 0,1 6,11A1,1 0 0,1 7,12M17,12A1,1 0 0,1 16,11A1,1 0 0,1 17,10A1,1 0 0,1 18,11A1,1 0 0,1 17,12Z"/>
37
+ </svg>
38
+ </button>
39
+ </div>
40
+ </div>
41
+ </header>
42
+
43
+ <!-- Main Content -->
44
+ <main class="app-main">
45
+ <!-- URL Input Section -->
46
+ <section class="input-section">
47
+ <form id="download-form" class="url-form" aria-describedby="form-help">
48
+ <div class="input-group">
49
+ <label for="url-input" class="input-label">Media URL</label>
50
+ <div class="url-input-container">
51
+ <input
52
+ type="url"
53
+ id="url-input"
54
+ name="url"
55
+ placeholder="https://www.youtube.com/watch?v=..."
56
+ required
57
+ aria-describedby="form-help"
58
+ autocomplete="url"
59
+ inputmode="url"
60
+ class="url-input"
61
+ >
62
+ <button type="submit" id="analyze-btn" class="analyze-btn" aria-label="Analyze URL and show available formats">
63
+ <span class="btn-text">Analyze</span>
64
+ <span class="btn-loading" aria-hidden="true">
65
+ <svg class="spinner" viewBox="0 0 24 24">
66
+ <circle class="path" cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
67
+ </svg>
68
+ </span>
69
+ </button>
70
+ </div>
71
+ </div>
72
+ <p id="form-help" class="help-text">Enter a video, audio, or playlist URL from supported platforms</p>
73
+ </form>
74
+ </section>
75
+
76
+ <!-- Status and Progress -->
77
+ <section class="status-section" aria-labelledby="status-title">
78
+ <h2 id="status-title" class="section-title">Status</h2>
79
+ <div id="status-container" class="status-container" aria-live="assertive" role="status">
80
+ <div class="status-item">
81
+ <span class="status-icon">ℹ️</span>
82
+ <span class="status-text">Ready to analyze URLs. Enter a link to begin.</span>
83
+ </div>
84
+ </div>
85
+ <div id="progress-section" class="progress-section" style="display: none;">
86
+ <div class="progress-header">
87
+ <span id="progress-title" class="progress-title">Downloading...</span>
88
+ <span id="progress-speed" class="progress-speed"></span>
89
+ </div>
90
+ <div class="progress-bar-container">
91
+ <progress id="progress-bar" value="0" max="100" class="progress-bar"></progress>
92
+ <span id="progress-percentage" class="progress-percentage">0%</span>
93
+ </div>
94
+ </div>
95
+ </section>
96
+
97
+ <!-- Media Info Section -->
98
+ <section id="media-info-section" class="media-info-section" style="display: none;">
99
+ <h2 class="section-title">Media Information</h2>
100
+ <div id="media-info" class="media-info">
101
+ <div class="media-thumbnail">
102
+ <img id="media-thumb" alt="" style="display: none;">
103
+ <div id="thumb-placeholder" class="thumb-placeholder">
104
+ <svg class="placeholder-icon" viewBox="0 0 24 24">
105
+ <path d="M8.5,13.5L11,16.51L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z"/>
106
+ </svg>
107
+ </div>
108
+ </div>
109
+ <div class="media-details">
110
+ <h3 id="media-title" class="media-title"></h3>
111
+ <div class="media-meta">
112
+ <span id="media-uploader" class="media-uploader"></span>
113
+ <span id="media-platform" class="platform-badge"></span>
114
+ <span id="media-duration" class="duration"></span>
115
+ </div>
116
+ <div class="media-stats">
117
+ <span id="media-views" class="stat-item"></span>
118
+ <span id="media-likes" class="stat-item"></span>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </section>
123
+
124
+ <!-- Download Options -->
125
+ <section id="formats-section" class="formats-section" style="display: none;">
126
+ <div class="section-header">
127
+ <h2 class="section-title">Available Formats</h2>
128
+ <div class="format-filters">
129
+ <button class="filter-btn active" data-filter="all" aria-label="Show all formats">All</button>
130
+ <button class="filter-btn" data-filter="video" aria-label="Show video formats only">Video</button>
131
+ <button class="filter-btn" data-filter="audio" aria-label="Show audio formats only">Audio</button>
132
+ </div>
133
+ </div>
134
+ <div id="format-list" class="format-list" role="list" aria-label="Available download formats">
135
+ <!-- Format options will be populated here -->
136
+ </div>
137
+ </section>
138
+
139
+ <!-- Download Queue -->
140
+ <section class="queue-section">
141
+ <div class="section-header">
142
+ <h2 class="section-title">Download Queue</h2>
143
+ <button id="clear-queue-btn" class="secondary-btn" aria-label="Clear all queued downloads">Clear All</button>
144
+ </div>
145
+ <div id="queue-list" class="queue-list" role="list" aria-label="Active downloads">
146
+ <!-- Active downloads will appear here -->
147
+ </div>
148
+ </section>
149
+
150
+ <!-- Error Section -->
151
+ <section id="error-section" class="error-section" style="display: none;">
152
+ <h2 class="section-title">Error</h2>
153
+ <div id="error-messages" class="error-container" role="alert" aria-live="assertive">
154
+ <!-- Error messages -->
155
+ </div>
156
+ </section>
157
+
158
+ <!-- Success Section -->
159
+ <section id="success-section" class="success-section" style="display: none;">
160
+ <h2 class="section-title">Success</h2>
161
+ <div id="success-messages" class="success-container" role="status" aria-live="polite">
162
+ <!-- Success messages -->
163
+ </div>
164
+ </section>
165
+ </main>
166
+
167
+ <!-- Bottom Navigation (Mobile) -->
168
+ <nav class="bottom-nav" role="navigation" aria-label="Main navigation">
169
+ <button class="nav-item active" data-tab="download" aria-label="Download section">
170
+ <svg class="nav-icon" viewBox="0 0 24 24">
171
+ <path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
172
+ </svg>
173
+ <span class="nav-label">Download</span>
174
+ </button>
175
+ <button class="nav-item" data-tab="history" aria-label="Download history">
176
+ <svg class="nav-icon" viewBox="0 0 24 24">
177
+ <path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3"/>
178
+ </svg>
179
+ <span class="nav-label">History</span>
180
+ </button>
181
+ <button class="nav-item" data-tab="settings" aria-label="Settings">
182
+ <svg class="nav-icon" viewBox="0 0 24 24">
183
+ <path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/>
184
+ </svg>
185
+ <span class="nav-label">Settings</span>
186
+ </button>
187
+ </nav>
188
+ </div>
189
+
190
+ <!-- Settings Modal -->
191
+ <div id="settings-modal" class="modal" style="display: none;">
192
+ <div class="modal-content">
193
+ <div class="modal-header">
194
+ <h3>Settings</h3>
195
+ <button class="modal-close" aria-label="Close settings">&times;</button>
196
+ </div>
197
+ <div class="modal-body">
198
+ <div class="setting-group">
199
+ <label class="setting-label">Auto-update yt-dlp</label>
200
+ <label class="toggle">
201
+ <input type="checkbox" id="auto-update" checked>
202
+ <span class="toggle-slider"></span>
203
+ </label>
204
+ </div>
205
+ <div class="setting-group">
206
+ <label class="setting-label">Privacy mode</label>
207
+ <label class="toggle">
208
+ <input type="checkbox" id="privacy-mode">
209
+ <span class="toggle-slider"></span>
210
+ </label>
211
+ </div>
212
+ <div class="setting-group">
213
+ <label class="setting-label">Default download quality</label>
214
+ <select id="default-quality" class="setting-select">
215
+ <option value="best">Best available</option>
216
+ <option value="1080p">1080p</option>
217
+ <option value="720p">720p</option>
218
+ <option value="480p">480p</option>
219
+ <option value="360p">360p</option>
220
+ </select>
221
+ </div>
222
+ </div>
223
+ <div class="modal-footer">
224
+ <button class="primary-btn" id="save-settings">Save Settings</button>
225
+ </div>
226
+ </div>
227
+ </div>
228
+
229
+ <script src="script.js"></script>
230
+ </body>
231
+ </html>
requirements.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Universal Media Downloader - Python Dependencies
2
+
3
+ # Core Flask framework
4
+ Flask==3.0.0
5
+ Flask-CORS==4.0.0
6
+
7
+ # yt-dlp for media downloading
8
+ yt-dlp==2023.11.16
9
+
10
+ # Gradio for Hugging Face Spaces
11
+ gradio==4.15.0
12
+
13
+ # Additional utilities
14
+ requests==2.31.0
15
+ urllib3==2.1.0
16
+ Werkzeug==3.0.1
17
+
18
+ # Production server
19
+ gunicorn==21.2.0
20
+
21
+ # For Hugging Face Spaces deployment
22
+ transformers==4.36.0
23
+ torch==2.1.0
24
+ tokenizers==0.15.0
25
+
26
+ # Development dependencies (optional)
27
+ # pytest==7.4.3
28
+ # pytest-cov==4.1.0
29
+ # black==23.11.0
30
+ # flake8==6.1.0
script.js ADDED
@@ -0,0 +1,921 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Universal Media Downloader - Enhanced JavaScript
2
+ // Professional media downloader with API integration
3
+
4
+ class UniversalMediaDownloader {
5
+ constructor() {
6
+ this.apiBaseUrl = 'http://localhost:5000/api';
7
+ this.currentFormats = [];
8
+ this.currentMediaInfo = null;
9
+ this.isProcessing = false;
10
+ this.downloadQueue = [];
11
+ this.settings = this.loadSettings();
12
+ this.activeDownloads = new Map();
13
+ this.progressIntervals = new Map();
14
+
15
+ this.initializeApp();
16
+ }
17
+
18
+ initializeApp() {
19
+ this.setupEventListeners();
20
+ this.setupKeyboardNavigation();
21
+ this.checkApiHealth();
22
+ this.setupServiceWorker();
23
+ this.loadDownloadHistory();
24
+ this.announceAppReady();
25
+ }
26
+
27
+ // Settings Management
28
+ loadSettings() {
29
+ const defaultSettings = {
30
+ autoUpdate: true,
31
+ privacyMode: false,
32
+ defaultQuality: 'best',
33
+ theme: 'dark'
34
+ };
35
+
36
+ try {
37
+ const saved = localStorage.getItem('downloader-settings');
38
+ return { ...defaultSettings, ...(saved ? JSON.parse(saved) : {}) };
39
+ } catch {
40
+ return defaultSettings;
41
+ }
42
+ }
43
+
44
+ saveSettings() {
45
+ try {
46
+ localStorage.setItem('downloader-settings', JSON.stringify(this.settings));
47
+ this.announceToScreenReader('Settings saved successfully');
48
+ } catch (error) {
49
+ console.error('Failed to save settings:', error);
50
+ }
51
+ }
52
+
53
+ // API Communication
54
+ async checkApiHealth() {
55
+ try {
56
+ const response = await fetch(`${this.apiBaseUrl}/health`);
57
+ const data = await response.json();
58
+
59
+ if (data.status === 'healthy') {
60
+ this.updateStatus('API connected and ready', 'success');
61
+ this.updateApiInfo(data);
62
+ } else {
63
+ throw new Error('API not healthy');
64
+ }
65
+ } catch (error) {
66
+ console.error('API health check failed:', error);
67
+ this.showError('Unable to connect to download service. Please refresh the page.');
68
+ }
69
+ }
70
+
71
+ async getFormats(url) {
72
+ try {
73
+ const response = await fetch(`${this.apiBaseUrl}/formats`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ },
78
+ body: JSON.stringify({ url })
79
+ });
80
+
81
+ const data = await response.json();
82
+
83
+ if (!response.ok) {
84
+ throw new Error(data.message || data.error || 'Failed to get formats');
85
+ }
86
+
87
+ return data;
88
+ } catch (error) {
89
+ console.error('Get formats error:', error);
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ async startDownload(url, formatId) {
95
+ try {
96
+ const downloadId = `dl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
97
+
98
+ const response = await fetch(`${this.apiBaseUrl}/download`, {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ body: JSON.stringify({
104
+ url,
105
+ format_id: formatId,
106
+ download_id: downloadId
107
+ })
108
+ });
109
+
110
+ const data = await response.json();
111
+
112
+ if (!response.ok) {
113
+ throw new Error(data.message || data.error || 'Failed to start download');
114
+ }
115
+
116
+ // Start monitoring progress
117
+ this.startProgressMonitoring(downloadId);
118
+
119
+ return { ...data, downloadId };
120
+ } catch (error) {
121
+ console.error('Start download error:', error);
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ async getProgress(downloadId) {
127
+ try {
128
+ const response = await fetch(`${this.apiBaseUrl}/progress/${downloadId}`);
129
+ const data = await response.json();
130
+ return data;
131
+ } catch (error) {
132
+ console.error('Get progress error:', error);
133
+ return null;
134
+ }
135
+ }
136
+
137
+ async getSupportedPlatforms() {
138
+ try {
139
+ const response = await fetch(`${this.apiBaseUrl}/supported-platforms`);
140
+ const data = await response.json();
141
+ return data;
142
+ } catch (error) {
143
+ console.error('Get platforms error:', error);
144
+ return null;
145
+ }
146
+ }
147
+
148
+ async updateYtdlp() {
149
+ try {
150
+ this.updateStatus('Checking for yt-dlp updates...', 'info');
151
+
152
+ const response = await fetch(`${this.apiBaseUrl}/update`, {
153
+ method: 'POST'
154
+ });
155
+
156
+ const data = await response.json();
157
+
158
+ if (data.success) {
159
+ this.updateStatus(`yt-dlp updated to version: ${data.version}`, 'success');
160
+ this.announceToScreenReader(`yt-dlp updated to version ${data.version}`);
161
+ } else {
162
+ this.updateStatus(data.message || 'Update check completed', 'info');
163
+ }
164
+
165
+ return data;
166
+ } catch (error) {
167
+ console.error('Update yt-dlp error:', error);
168
+ this.showError('Failed to update yt-dlp');
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ // Progress Monitoring
174
+ startProgressMonitoring(downloadId) {
175
+ if (this.progressIntervals.has(downloadId)) {
176
+ clearInterval(this.progressIntervals.get(downloadId));
177
+ }
178
+
179
+ const interval = setInterval(async () => {
180
+ const progress = await this.getProgress(downloadId);
181
+
182
+ if (progress && progress.success) {
183
+ this.updateDownloadProgress(progress);
184
+
185
+ if (progress.progress.status === 'finished' || progress.progress.status === 'error') {
186
+ clearInterval(interval);
187
+ this.progressIntervals.delete(downloadId);
188
+
189
+ if (progress.progress.status === 'finished') {
190
+ this.onDownloadComplete(downloadId, progress.progress);
191
+ } else {
192
+ this.onDownloadError(downloadId, progress.progress);
193
+ }
194
+ }
195
+ }
196
+ }, 1000);
197
+
198
+ this.progressIntervals.set(downloadId, interval);
199
+ }
200
+
201
+ updateDownloadProgress(progressData) {
202
+ const { downloadId, progress } = progressData;
203
+
204
+ // Update queue item
205
+ const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`);
206
+ if (queueItem) {
207
+ this.updateQueueItem(queueItem, progress);
208
+ }
209
+
210
+ // Update global progress
211
+ if (progress.status === 'downloading') {
212
+ this.showProgress(progress.percentage, progress.speed, progress.eta);
213
+ }
214
+ }
215
+
216
+ // UI Updates
217
+ updateStatus(message, type = 'info') {
218
+ const statusContainer = document.getElementById('status-container');
219
+ const statusIcon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
220
+
221
+ statusContainer.innerHTML = `
222
+ <div class="status-item">
223
+ <span class="status-icon">${statusIcon}</span>
224
+ <span class="status-text">${message}</span>
225
+ </div>
226
+ `;
227
+
228
+ this.announceToScreenReader(message);
229
+ }
230
+
231
+ showError(message) {
232
+ const errorSection = document.getElementById('error-section');
233
+ const errorContainer = document.getElementById('error-messages');
234
+
235
+ errorContainer.innerHTML = `
236
+ <div class="error-item">
237
+ <span class="error-icon">❌</span>
238
+ <span class="error-text">${message}</span>
239
+ </div>
240
+ `;
241
+
242
+ errorSection.style.display = 'block';
243
+ this.announceToScreenReader(`Error: ${message}`);
244
+ }
245
+
246
+ hideError() {
247
+ const errorSection = document.getElementById('error-section');
248
+ errorSection.style.display = 'none';
249
+ }
250
+
251
+ showProgress(percentage, speed = null, eta = null) {
252
+ const progressSection = document.getElementById('progress-section');
253
+ const progressBar = document.getElementById('progress-bar');
254
+ const progressPercentage = document.getElementById('progress-percentage');
255
+ const progressSpeed = document.getElementById('progress-speed');
256
+ const progressTitle = document.getElementById('progress-title');
257
+
258
+ progressSection.style.display = 'block';
259
+ progressBar.value = percentage;
260
+ progressPercentage.textContent = `${Math.round(percentage)}%`;
261
+
262
+ if (speed) {
263
+ progressSpeed.textContent = this.formatSpeed(speed);
264
+ }
265
+
266
+ if (eta) {
267
+ progressTitle.textContent = `Downloading... ETA: ${this.formatTime(eta)}`;
268
+ }
269
+ }
270
+
271
+ hideProgress() {
272
+ const progressSection = document.getElementById('progress-section');
273
+ progressSection.style.display = 'none';
274
+ }
275
+
276
+ displayMediaInfo(info) {
277
+ const section = document.getElementById('media-info-section');
278
+ const title = document.getElementById('media-title');
279
+ const uploader = document.getElementById('media-uploader');
280
+ const platform = document.getElementById('media-platform');
281
+ const duration = document.getElementById('media-duration');
282
+ const views = document.getElementById('media-views');
283
+ const likes = document.getElementById('media-likes');
284
+ const thumb = document.getElementById('media-thumb');
285
+ const placeholder = document.getElementById('thumb-placeholder');
286
+
287
+ // Update content
288
+ title.textContent = info.title || 'Unknown Title';
289
+ uploader.textContent = info.uploader || 'Unknown Uploader';
290
+ platform.textContent = info.platform || 'Unknown';
291
+ platform.className = `platform-badge platform-${(info.platform || '').toLowerCase()}`;
292
+ duration.textContent = info.duration ? this.formatTime(info.duration) : '';
293
+ views.textContent = info.view_count ? `${this.formatNumber(info.view_count)} views` : '';
294
+ likes.textContent = info.like_count ? `${this.formatNumber(info.like_count)} likes` : '';
295
+
296
+ // Handle thumbnail
297
+ if (info.thumbnail) {
298
+ thumb.src = info.thumbnail;
299
+ thumb.alt = `Thumbnail for ${info.title}`;
300
+ thumb.style.display = 'block';
301
+ placeholder.style.display = 'none';
302
+ } else {
303
+ thumb.style.display = 'none';
304
+ placeholder.style.display = 'flex';
305
+ }
306
+
307
+ section.style.display = 'block';
308
+ }
309
+
310
+ displayFormats(formats) {
311
+ const section = document.getElementById('formats-section');
312
+ const formatList = document.getElementById('format-list');
313
+
314
+ // Clear existing formats
315
+ formatList.innerHTML = '';
316
+
317
+ // Sort formats by quality and type
318
+ const sortedFormats = this.sortFormats(formats);
319
+
320
+ sortedFormats.forEach((format, index) => {
321
+ const formatElement = this.createFormatElement(format, index);
322
+ formatList.appendChild(formatElement);
323
+ });
324
+
325
+ section.style.display = 'block';
326
+ this.currentFormats = sortedFormats;
327
+ }
328
+
329
+ createFormatElement(format, index) {
330
+ const element = document.createElement('div');
331
+ element.className = 'format-item';
332
+ element.setAttribute('role', 'listitem');
333
+ element.setAttribute('data-format-id', format.id);
334
+ element.setAttribute('data-format-type', format.type);
335
+ element.setAttribute('tabindex', '0');
336
+
337
+ const fileSize = format.filesize ? this.formatFileSize(format.filesize) : 'Unknown size';
338
+ const quality = format.width && format.height ? `${format.height}p` : format.format_note || 'Unknown';
339
+ const codec = format.vcodec !== 'none' ? format.vcodec.split('.')[0] : format.acodec.split('.')[0];
340
+
341
+ element.innerHTML = `
342
+ <div class="format-info">
343
+ <div class="format-title">${this.getFormatTitle(format)}</div>
344
+ <div class="format-details">${format.ext.toUpperCase()} • ${quality} • ${codec} • ${fileSize}</div>
345
+ </div>
346
+ <div class="format-actions">
347
+ <button class="primary-btn download-btn" data-format-id="${format.id}" aria-label="Download ${this.getFormatTitle(format)}">
348
+ <span class="btn-text">Download</span>
349
+ </button>
350
+ </div>
351
+ `;
352
+
353
+ // Add event listeners
354
+ element.addEventListener('click', (e) => {
355
+ if (!e.target.closest('.download-btn')) {
356
+ this.selectFormat(format);
357
+ }
358
+ });
359
+
360
+ element.addEventListener('keydown', (e) => {
361
+ if (e.key === 'Enter' || e.key === ' ') {
362
+ e.preventDefault();
363
+ this.selectFormat(format);
364
+ }
365
+ });
366
+
367
+ const downloadBtn = element.querySelector('.download-btn');
368
+ downloadBtn.addEventListener('click', (e) => {
369
+ e.stopPropagation();
370
+ this.handleDownload(format, downloadBtn);
371
+ });
372
+
373
+ return element;
374
+ }
375
+
376
+ createQueueItem(progress) {
377
+ const element = document.createElement('div');
378
+ element.className = 'queue-item';
379
+ element.setAttribute('data-download-id', progress.download_id);
380
+
381
+ const fileName = progress.filename ? progress.filename.split('/').pop() : 'Downloading...';
382
+
383
+ element.innerHTML = `
384
+ <div class="queue-thumbnail">
385
+ <svg class="placeholder-icon" viewBox="0 0 24 24">
386
+ <path d="M8.5,13.5L11,16.51L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z"/>
387
+ </svg>
388
+ </div>
389
+ <div class="queue-info">
390
+ <div class="queue-title">${fileName}</div>
391
+ <div class="queue-url">${progress.url || 'Downloading...'}</div>
392
+ </div>
393
+ <div class="queue-progress">
394
+ <progress class="progress-bar" value="0" max="100"></progress>
395
+ <div class="queue-status">Starting...</div>
396
+ </div>
397
+ `;
398
+
399
+ return element;
400
+ }
401
+
402
+ updateQueueItem(element, progress) {
403
+ const progressBar = element.querySelector('.progress-bar');
404
+ const statusText = element.querySelector('.queue-status');
405
+
406
+ if (progress.status === 'downloading') {
407
+ progressBar.value = progress.percentage || 0;
408
+ statusText.textContent = `${Math.round(progress.percentage || 0)}%`;
409
+ } else if (progress.status === 'finished') {
410
+ element.classList.add('completed');
411
+ statusText.textContent = 'Complete';
412
+ progressBar.value = 100;
413
+ } else if (progress.status === 'error') {
414
+ element.classList.add('error');
415
+ statusText.textContent = 'Error';
416
+ }
417
+ }
418
+
419
+ // Event Handlers
420
+ setupEventListeners() {
421
+ // Form submission
422
+ const form = document.getElementById('download-form');
423
+ form.addEventListener('submit', (e) => this.handleFormSubmit(e));
424
+
425
+ // URL input
426
+ const urlInput = document.getElementById('url-input');
427
+ urlInput.addEventListener('input', () => this.validateURL());
428
+ urlInput.addEventListener('keypress', (e) => {
429
+ if (e.key === 'Enter') {
430
+ e.preventDefault();
431
+ this.handleFormSubmit(e);
432
+ }
433
+ });
434
+
435
+ // Format filters
436
+ const filterBtns = document.querySelectorAll('.filter-btn');
437
+ filterBtns.forEach(btn => {
438
+ btn.addEventListener('click', (e) => this.handleFilterClick(e));
439
+ });
440
+
441
+ // Settings modal
442
+ const settingsBtn = document.getElementById('settings-btn');
443
+ const settingsModal = document.getElementById('settings-modal');
444
+ const modalClose = document.querySelector('.modal-close');
445
+ const saveSettingsBtn = document.getElementById('save-settings');
446
+
447
+ settingsBtn.addEventListener('click', () => this.openSettings());
448
+ modalClose.addEventListener('click', () => this.closeSettings());
449
+ settingsModal.addEventListener('click', (e) => {
450
+ if (e.target === settingsModal) this.closeSettings();
451
+ });
452
+ saveSettingsBtn.addEventListener('click', () => this.saveSettingsAndClose());
453
+
454
+ // Update button
455
+ const updateBtn = document.getElementById('update-btn');
456
+ updateBtn.addEventListener('click', () => this.handleUpdateYtdlp());
457
+
458
+ // Clear queue button
459
+ const clearQueueBtn = document.getElementById('clear-queue-btn');
460
+ clearQueueBtn.addEventListener('click', () => this.clearQueue());
461
+
462
+ // Bottom navigation
463
+ const navItems = document.querySelectorAll('.nav-item');
464
+ navItems.forEach(item => {
465
+ item.addEventListener('click', (e) => this.handleNavClick(e));
466
+ });
467
+
468
+ // Settings inputs
469
+ this.setupSettingsInputs();
470
+ }
471
+
472
+ setupSettingsInputs() {
473
+ const autoUpdate = document.getElementById('auto-update');
474
+ const privacyMode = document.getElementById('privacy-mode');
475
+ const defaultQuality = document.getElementById('default-quality');
476
+
477
+ autoUpdate.checked = this.settings.autoUpdate;
478
+ privacyMode.checked = this.settings.privacyMode;
479
+ defaultQuality.value = this.settings.defaultQuality;
480
+ }
481
+
482
+ async handleFormSubmit(event) {
483
+ event.preventDefault();
484
+
485
+ if (this.isProcessing) {
486
+ return;
487
+ }
488
+
489
+ this.hideError();
490
+ this.hideProgress();
491
+
492
+ const url = document.getElementById('url-input').value.trim();
493
+
494
+ if (!this.isValidURL(url)) {
495
+ this.showError('Please enter a valid URL. Make sure it starts with http:// or https://');
496
+ return;
497
+ }
498
+
499
+ this.setLoadingState(true);
500
+ this.updateStatus('Analyzing URL and fetching available formats...', 'info');
501
+
502
+ try {
503
+ const result = await this.getFormats(url);
504
+
505
+ if (result.success) {
506
+ this.currentMediaInfo = {
507
+ title: result.title,
508
+ uploader: result.uploader,
509
+ platform: result.platform,
510
+ duration: result.duration,
511
+ thumbnail: result.thumbnail,
512
+ view_count: result.view_count,
513
+ like_count: result.like_count
514
+ };
515
+
516
+ this.displayMediaInfo(this.currentMediaInfo);
517
+ this.displayFormats(result.formats);
518
+ this.updateStatus(`Found ${result.formats.length} available formats for ${result.platform}. Select your preferred option below.`, 'success');
519
+
520
+ // Add to queue
521
+ this.addToQueue(result);
522
+ } else {
523
+ throw new Error(result.message || result.error || 'Failed to analyze URL');
524
+ }
525
+
526
+ } catch (error) {
527
+ console.error('Form submission error:', error);
528
+ this.showError(error.message || 'Failed to analyze URL. Please check if the URL is valid and try again.');
529
+ } finally {
530
+ this.setLoadingState(false);
531
+ }
532
+ }
533
+
534
+ async handleDownload(format, button) {
535
+ if (this.isProcessing) {
536
+ return;
537
+ }
538
+
539
+ try {
540
+ this.setDownloadButtonState(button, 'downloading');
541
+ this.updateStatus(`Starting download: ${this.getFormatTitle(format)}`, 'info');
542
+
543
+ const result = await this.startDownload(
544
+ document.getElementById('url-input').value.trim(),
545
+ format.id
546
+ );
547
+
548
+ // Add to active downloads
549
+ this.activeDownloads.set(result.downloadId, { format, startTime: Date.now() });
550
+
551
+ // Add to queue display
552
+ const queueList = document.getElementById('queue-list');
553
+ const queueItem = this.createQueueItem({
554
+ download_id: result.downloadId,
555
+ filename: this.getFormatTitle(format),
556
+ url: document.getElementById('url-input').value.trim()
557
+ });
558
+ queueList.appendChild(queueItem);
559
+
560
+ this.updateStatus(`Download started: ${this.getFormatTitle(format)}`, 'success');
561
+ this.announceToScreenReader(`Download started: ${this.getFormatTitle(format)}`);
562
+
563
+ } catch (error) {
564
+ console.error('Download error:', error);
565
+ this.setDownloadButtonState(button, 'error');
566
+ this.showError(`Download failed: ${error.message}`);
567
+ }
568
+ }
569
+
570
+ async handleUpdateYtdlp() {
571
+ const updateBtn = document.getElementById('update-btn');
572
+ updateBtn.disabled = true;
573
+ updateBtn.style.opacity = '0.6';
574
+
575
+ try {
576
+ await this.updateYtdlp();
577
+ } catch (error) {
578
+ console.error('Update error:', error);
579
+ } finally {
580
+ updateBtn.disabled = false;
581
+ updateBtn.style.opacity = '1';
582
+ }
583
+ }
584
+
585
+ handleFilterClick(event) {
586
+ const filter = event.target.dataset.filter;
587
+ const buttons = document.querySelectorAll('.filter-btn');
588
+
589
+ // Update active filter
590
+ buttons.forEach(btn => btn.classList.remove('active'));
591
+ event.target.classList.add('active');
592
+
593
+ // Filter formats
594
+ const formatItems = document.querySelectorAll('.format-item');
595
+ formatItems.forEach(item => {
596
+ const formatType = item.dataset.formatType;
597
+ if (filter === 'all' || formatType === filter) {
598
+ item.style.display = 'flex';
599
+ } else {
600
+ item.style.display = 'none';
601
+ }
602
+ });
603
+ }
604
+
605
+ handleNavClick(event) {
606
+ const tab = event.currentTarget.dataset.tab;
607
+ const items = document.querySelectorAll('.nav-item');
608
+
609
+ items.forEach(item => item.classList.remove('active'));
610
+ event.currentTarget.classList.add('active');
611
+
612
+ // Show/hide relevant sections based on tab
613
+ this.showTabContent(tab);
614
+ }
615
+
616
+ showTabContent(tab) {
617
+ // This would show/hide different content areas
618
+ // For now, we'll just announce the tab change
619
+ this.announceToScreenReader(`Switched to ${tab} tab`);
620
+ }
621
+
622
+ selectFormat(format) {
623
+ // Highlight selected format
624
+ const formatItems = document.querySelectorAll('.format-item');
625
+ formatItems.forEach(item => item.classList.remove('selected'));
626
+
627
+ const selectedItem = document.querySelector(`[data-format-id="${format.id}"]`);
628
+ if (selectedItem) {
629
+ selectedItem.classList.add('selected');
630
+ this.announceToScreenReader(`Selected ${this.getFormatTitle(format)}`);
631
+ }
632
+ }
633
+
634
+ // Utility Methods
635
+ isValidURL(string) {
636
+ try {
637
+ new URL(string);
638
+ return true;
639
+ } catch (_) {
640
+ return false;
641
+ }
642
+ }
643
+
644
+ validateURL() {
645
+ const urlInput = document.getElementById('url-input');
646
+ const url = urlInput.value.trim();
647
+
648
+ if (url === '') {
649
+ urlInput.setCustomValidity('');
650
+ return true;
651
+ }
652
+
653
+ if (!this.isValidURL(url)) {
654
+ urlInput.setCustomValidity('Please enter a valid URL');
655
+ urlInput.reportValidity();
656
+ return false;
657
+ }
658
+
659
+ urlInput.setCustomValidity('');
660
+ return true;
661
+ }
662
+
663
+ setLoadingState(isLoading) {
664
+ this.isProcessing = isLoading;
665
+ const button = document.getElementById('analyze-btn');
666
+ const urlInput = document.getElementById('url-input');
667
+
668
+ if (isLoading) {
669
+ button.classList.add('loading');
670
+ button.disabled = true;
671
+ urlInput.disabled = true;
672
+ } else {
673
+ button.classList.remove('loading');
674
+ button.disabled = false;
675
+ urlInput.disabled = false;
676
+ urlInput.focus();
677
+ }
678
+ }
679
+
680
+ setDownloadButtonState(button, state) {
681
+ button.disabled = state === 'downloading';
682
+ button.classList.remove('downloading', 'error');
683
+
684
+ if (state === 'downloading') {
685
+ button.classList.add('downloading');
686
+ button.innerHTML = '<span class="btn-loading">Downloading...</span>';
687
+ } else if (state === 'error') {
688
+ button.classList.add('error');
689
+ button.innerHTML = '<span class="btn-text">Error</span>';
690
+ } else {
691
+ button.innerHTML = '<span class="btn-text">Download</span>';
692
+ }
693
+ }
694
+
695
+ addToQueue(result) {
696
+ // Add media to download queue for easy access
697
+ this.downloadQueue.push({
698
+ url: document.getElementById('url-input').value.trim(),
699
+ title: result.title,
700
+ platform: result.platform,
701
+ formats: result.formats,
702
+ timestamp: Date.now()
703
+ });
704
+ }
705
+
706
+ clearQueue() {
707
+ this.downloadQueue = [];
708
+ const queueList = document.getElementById('queue-list');
709
+ queueList.innerHTML = '';
710
+ this.announceToScreenReader('Download queue cleared');
711
+ }
712
+
713
+ onDownloadComplete(downloadId, progress) {
714
+ this.setDownloadCompleted(downloadId);
715
+ this.updateStatus('Download completed successfully!', 'success');
716
+ this.announceToScreenReader('Download completed successfully');
717
+ this.hideProgress();
718
+ }
719
+
720
+ onDownloadError(downloadId, progress) {
721
+ this.setDownloadError(downloadId);
722
+ this.updateStatus('Download failed. Please try again.', 'error');
723
+ this.announceToScreenReader('Download failed');
724
+ this.hideProgress();
725
+ }
726
+
727
+ setDownloadCompleted(downloadId) {
728
+ const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`);
729
+ if (queueItem) {
730
+ queueItem.classList.add('completed');
731
+ const statusText = queueItem.querySelector('.queue-status');
732
+ if (statusText) statusText.textContent = 'Complete';
733
+ }
734
+ }
735
+
736
+ setDownloadError(downloadId) {
737
+ const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`);
738
+ if (queueItem) {
739
+ queueItem.classList.add('error');
740
+ const statusText = queueItem.querySelector('.queue-status');
741
+ if (statusText) statusText.textContent = 'Error';
742
+ }
743
+ }
744
+
745
+ // Settings Modal
746
+ openSettings() {
747
+ const modal = document.getElementById('settings-modal');
748
+ modal.style.display = 'flex';
749
+ this.announceToScreenReader('Settings opened');
750
+ }
751
+
752
+ closeSettings() {
753
+ const modal = document.getElementById('settings-modal');
754
+ modal.style.display = 'none';
755
+ this.announceToScreenReader('Settings closed');
756
+ }
757
+
758
+ saveSettingsAndClose() {
759
+ const autoUpdate = document.getElementById('auto-update').checked;
760
+ const privacyMode = document.getElementById('privacy-mode').checked;
761
+ const defaultQuality = document.getElementById('default-quality').value;
762
+
763
+ this.settings = {
764
+ ...this.settings,
765
+ autoUpdate,
766
+ privacyMode,
767
+ defaultQuality
768
+ };
769
+
770
+ this.saveSettings();
771
+ this.closeSettings();
772
+ }
773
+
774
+ // Formatting Utilities
775
+ getFormatTitle(format) {
776
+ const type = format.type === 'video' ? 'Video' : 'Audio';
777
+ const quality = format.height ? `${format.height}p` : format.format_note || 'Best';
778
+ return `${type} - ${quality}`;
779
+ }
780
+
781
+ sortFormats(formats) {
782
+ // Sort by type (video first), then by quality
783
+ return formats.sort((a, b) => {
784
+ if (a.type !== b.type) {
785
+ return a.type === 'video' ? -1 : 1;
786
+ }
787
+ if (a.height && b.height) {
788
+ return b.height - a.height;
789
+ }
790
+ return 0;
791
+ });
792
+ }
793
+
794
+ formatFileSize(bytes) {
795
+ if (!bytes) return 'Unknown';
796
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
797
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
798
+ return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
799
+ }
800
+
801
+ formatSpeed(bytesPerSec) {
802
+ if (!bytesPerSec) return '';
803
+ return this.formatFileSize(bytesPerSec) + '/s';
804
+ }
805
+
806
+ formatTime(seconds) {
807
+ if (!seconds) return '';
808
+ const hours = Math.floor(seconds / 3600);
809
+ const minutes = Math.floor((seconds % 3600) / 60);
810
+ const secs = Math.floor(seconds % 60);
811
+
812
+ if (hours > 0) {
813
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
814
+ }
815
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
816
+ }
817
+
818
+ formatNumber(num) {
819
+ if (num >= 1000000) {
820
+ return (num / 1000000).toFixed(1) + 'M';
821
+ } else if (num >= 1000) {
822
+ return (num / 1000).toFixed(1) + 'K';
823
+ }
824
+ return num.toString();
825
+ }
826
+
827
+ updateApiInfo(data) {
828
+ // Update any UI elements showing API info
829
+ console.log('API Info:', data);
830
+ }
831
+
832
+ // Accessibility
833
+ announceToScreenReader(message) {
834
+ const announcement = document.createElement('div');
835
+ announcement.setAttribute('aria-live', 'polite');
836
+ announcement.setAttribute('aria-atomic', 'true');
837
+ announcement.className = 'sr-only';
838
+ announcement.textContent = message;
839
+
840
+ document.body.appendChild(announcement);
841
+
842
+ setTimeout(() => {
843
+ document.body.removeChild(announcement);
844
+ }, 1000);
845
+ }
846
+
847
+ setupKeyboardNavigation() {
848
+ document.addEventListener('keydown', (e) => {
849
+ // Global keyboard shortcuts
850
+ if (e.ctrlKey || e.metaKey) {
851
+ switch (e.key) {
852
+ case 'u':
853
+ e.preventDefault();
854
+ document.getElementById('url-input').focus();
855
+ break;
856
+ case 's':
857
+ e.preventDefault();
858
+ this.openSettings();
859
+ break;
860
+ }
861
+ }
862
+
863
+ if (e.key === 'Escape') {
864
+ this.closeSettings();
865
+ }
866
+ });
867
+ }
868
+
869
+ // Service Worker
870
+ setupServiceWorker() {
871
+ if ('serviceWorker' in navigator) {
872
+ navigator.serviceWorker.register('/sw.js')
873
+ .then(registration => {
874
+ console.log('Service Worker registered:', registration);
875
+ })
876
+ .catch(error => {
877
+ console.log('Service Worker registration failed:', error);
878
+ });
879
+ }
880
+ }
881
+
882
+ loadDownloadHistory() {
883
+ // Load download history from localStorage
884
+ try {
885
+ const history = localStorage.getItem('download-history');
886
+ if (history) {
887
+ const parsed = JSON.parse(history);
888
+ console.log('Loaded download history:', parsed.length, 'items');
889
+ }
890
+ } catch (error) {
891
+ console.error('Failed to load download history:', error);
892
+ }
893
+ }
894
+
895
+ announceAppReady() {
896
+ setTimeout(() => {
897
+ this.announceToScreenReader('Universal Media Downloader ready. Enter a URL to begin downloading.');
898
+ document.getElementById('url-input').focus();
899
+ }, 1000);
900
+ }
901
+ }
902
+
903
+ // Initialize the application
904
+ document.addEventListener('DOMContentLoaded', () => {
905
+ window.downloader = new UniversalMediaDownloader();
906
+ });
907
+
908
+ // Global error handling
909
+ window.addEventListener('error', (event) => {
910
+ console.error('Global error:', event.error);
911
+ if (window.downloader) {
912
+ window.downloader.showError('An unexpected error occurred. Please refresh the page and try again.');
913
+ }
914
+ });
915
+
916
+ window.addEventListener('unhandledrejection', (event) => {
917
+ console.error('Unhandled promise rejection:', event.reason);
918
+ if (window.downloader) {
919
+ window.downloader.showError('A background process failed. Please check your connection and try again.');
920
+ }
921
+ });
style.css ADDED
@@ -0,0 +1,1105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Universal Media Downloader - Enhanced Styles */
2
+ /* Based on Dark Mode First Design with High Contrast */
3
+
4
+ /* CSS Custom Properties */
5
+ :root {
6
+ /* Colors - Dark Mode First */
7
+ --primary-100: #A5F3FC;
8
+ --primary-500: #06B6D4;
9
+ --primary-700: #0E7490;
10
+ --bg-page: #000000;
11
+ --bg-surface: #141414;
12
+ --border-default: #262626;
13
+ --text-primary: #E4E4E7;
14
+ --text-secondary: #A3A3A3;
15
+ --text-heading: #FAFAFA;
16
+ --success: #22C55E;
17
+ --warning: #FBBF24;
18
+ --error: #F43F5E;
19
+
20
+ /* Typography */
21
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
22
+ --font-mono: 'JetBrains Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
23
+
24
+ /* Spacing (4px Grid System) */
25
+ --space-xs: 8px;
26
+ --space-sm: 12px;
27
+ --space-md: 16px;
28
+ --space-lg: 24px;
29
+ --space-xl: 32px;
30
+ --space-xxl: 48px;
31
+ --space-xxxl: 64px;
32
+
33
+ /* Border Radius */
34
+ --radius-sm: 8px;
35
+ --radius-md: 12px;
36
+
37
+ /* Transitions */
38
+ --transition-fast: 150ms ease-out;
39
+ --transition-normal: 200ms ease-out;
40
+ --transition-slow: 300ms cubic-bezier(0.25, 1, 0.5, 1);
41
+
42
+ /* Shadows */
43
+ --shadow-focus: 0 0 0 3px rgba(6, 182, 212, 0.4);
44
+ --shadow-hover: 0 0 12px 0 rgba(6, 182, 212, 0.2);
45
+ }
46
+
47
+ /* Reset and Base Styles */
48
+ * {
49
+ margin: 0;
50
+ padding: 0;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ html {
55
+ font-size: 16px;
56
+ scroll-behavior: smooth;
57
+ }
58
+
59
+ body {
60
+ font-family: var(--font-family);
61
+ font-size: 16px;
62
+ line-height: 1.5;
63
+ color: var(--text-primary);
64
+ background-color: var(--bg-page);
65
+ min-height: 100vh;
66
+ -webkit-font-smoothing: antialiased;
67
+ -moz-osx-font-smoothing: grayscale;
68
+ overflow-x: hidden;
69
+ }
70
+
71
+ /* Screen Reader Only */
72
+ .sr-only {
73
+ position: absolute;
74
+ width: 1px;
75
+ height: 1px;
76
+ padding: 0;
77
+ margin: -1px;
78
+ overflow: hidden;
79
+ clip: rect(0, 0, 0, 0);
80
+ white-space: nowrap;
81
+ border: 0;
82
+ }
83
+
84
+ /* App Container */
85
+ .app-container {
86
+ min-height: 100vh;
87
+ display: flex;
88
+ flex-direction: column;
89
+ max-width: 1280px;
90
+ margin: 0 auto;
91
+ padding: var(--space-xl);
92
+ }
93
+
94
+ /* Header */
95
+ .app-header {
96
+ background: var(--bg-surface);
97
+ border: 1px solid var(--border-default);
98
+ border-radius: var(--radius-md);
99
+ padding: var(--space-lg);
100
+ margin-bottom: var(--space-xl);
101
+ }
102
+
103
+ .header-content {
104
+ display: flex;
105
+ justify-content: space-between;
106
+ align-items: center;
107
+ }
108
+
109
+ .app-title {
110
+ font-size: 48px;
111
+ font-weight: 700;
112
+ line-height: 1.2;
113
+ color: var(--text-heading);
114
+ margin: 0;
115
+ }
116
+
117
+ .header-controls {
118
+ display: flex;
119
+ gap: var(--space-sm);
120
+ }
121
+
122
+ .icon-btn {
123
+ width: 48px;
124
+ height: 48px;
125
+ background: transparent;
126
+ border: 1px solid var(--border-default);
127
+ border-radius: var(--radius-sm);
128
+ color: var(--text-secondary);
129
+ cursor: pointer;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ transition: all var(--transition-normal);
134
+ }
135
+
136
+ .icon-btn:hover {
137
+ border-color: var(--primary-500);
138
+ color: var(--primary-500);
139
+ box-shadow: var(--shadow-hover);
140
+ }
141
+
142
+ .icon-btn:focus {
143
+ outline: none;
144
+ box-shadow: var(--shadow-focus);
145
+ }
146
+
147
+ .icon {
148
+ width: 20px;
149
+ height: 20px;
150
+ fill: currentColor;
151
+ }
152
+
153
+ /* Main Content */
154
+ .app-main {
155
+ flex: 1;
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: var(--space-xl);
159
+ padding-bottom: 80px; /* Space for bottom nav on mobile */
160
+ }
161
+
162
+ /* Section Styles */
163
+ .section-title {
164
+ font-size: 24px;
165
+ font-weight: 600;
166
+ line-height: 1.3;
167
+ color: var(--text-heading);
168
+ margin-bottom: var(--space-md);
169
+ }
170
+
171
+ .section-header {
172
+ display: flex;
173
+ justify-content: space-between;
174
+ align-items: center;
175
+ margin-bottom: var(--space-md);
176
+ }
177
+
178
+ /* Input Section */
179
+ .input-section {
180
+ background: var(--bg-surface);
181
+ border: 1px solid var(--border-default);
182
+ border-radius: var(--radius-md);
183
+ padding: var(--space-lg);
184
+ }
185
+
186
+ .url-form {
187
+ margin: 0;
188
+ }
189
+
190
+ .input-group {
191
+ margin-bottom: var(--space-sm);
192
+ }
193
+
194
+ .input-label {
195
+ display: block;
196
+ font-size: 16px;
197
+ font-weight: 500;
198
+ color: var(--text-heading);
199
+ margin-bottom: var(--space-xs);
200
+ }
201
+
202
+ .url-input-container {
203
+ display: flex;
204
+ gap: var(--space-sm);
205
+ align-items: stretch;
206
+ }
207
+
208
+ .url-input {
209
+ flex: 1;
210
+ height: 56px;
211
+ background: var(--bg-page);
212
+ border: 1px solid var(--border-default);
213
+ border-radius: var(--radius-md);
214
+ color: var(--text-primary);
215
+ font-size: 16px;
216
+ padding: 0 var(--space-md);
217
+ transition: all var(--transition-normal);
218
+ }
219
+
220
+ .url-input::placeholder {
221
+ color: var(--text-secondary);
222
+ font-family: var(--font-mono);
223
+ }
224
+
225
+ .url-input:focus {
226
+ outline: none;
227
+ border-color: var(--primary-500);
228
+ box-shadow: var(--shadow-focus);
229
+ }
230
+
231
+ .analyze-btn {
232
+ min-width: 120px;
233
+ height: 56px;
234
+ background: var(--primary-500);
235
+ color: var(--text-heading);
236
+ border: none;
237
+ border-radius: var(--radius-md);
238
+ font-size: 16px;
239
+ font-weight: 500;
240
+ cursor: pointer;
241
+ transition: all var(--transition-slow);
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ gap: var(--space-xs);
246
+ position: relative;
247
+ overflow: hidden;
248
+ }
249
+
250
+ .analyze-btn:hover {
251
+ background: var(--primary-700);
252
+ transform: translateY(-2px);
253
+ box-shadow: var(--shadow-hover);
254
+ }
255
+
256
+ .analyze-btn:active {
257
+ transform: translateY(0);
258
+ background: var(--primary-700);
259
+ }
260
+
261
+ .analyze-btn:focus {
262
+ outline: none;
263
+ box-shadow: var(--shadow-focus);
264
+ }
265
+
266
+ .analyze-btn:disabled {
267
+ background: var(--border-default);
268
+ cursor: not-allowed;
269
+ transform: none;
270
+ box-shadow: none;
271
+ }
272
+
273
+ .btn-loading {
274
+ display: none;
275
+ }
276
+
277
+ .analyze-btn.loading .btn-text {
278
+ display: none;
279
+ }
280
+
281
+ .analyze-btn.loading .btn-loading {
282
+ display: flex;
283
+ align-items: center;
284
+ }
285
+
286
+ .spinner {
287
+ width: 20px;
288
+ height: 20px;
289
+ animation: spin 1s linear infinite;
290
+ }
291
+
292
+ .path {
293
+ stroke-dasharray: 90, 150;
294
+ stroke-dashoffset: 0;
295
+ stroke-linecap: round;
296
+ animation: dash 1.5s ease-in-out infinite;
297
+ }
298
+
299
+ @keyframes spin {
300
+ 100% {
301
+ transform: rotate(360deg);
302
+ }
303
+ }
304
+
305
+ @keyframes dash {
306
+ 0% {
307
+ stroke-dasharray: 1, 150;
308
+ stroke-dashoffset: 0;
309
+ }
310
+ 50% {
311
+ stroke-dasharray: 90, 150;
312
+ stroke-dashoffset: -35;
313
+ }
314
+ 100% {
315
+ stroke-dashoffset: -124;
316
+ }
317
+ }
318
+
319
+ .help-text {
320
+ font-size: 14px;
321
+ color: var(--text-secondary);
322
+ line-height: 1.4;
323
+ }
324
+
325
+ /* Status Section */
326
+ .status-section {
327
+ background: var(--bg-surface);
328
+ border: 1px solid var(--border-default);
329
+ border-radius: var(--radius-md);
330
+ padding: var(--space-lg);
331
+ }
332
+
333
+ .status-container {
334
+ min-height: 60px;
335
+ }
336
+
337
+ .status-item {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: var(--space-sm);
341
+ }
342
+
343
+ .status-icon {
344
+ font-size: 18px;
345
+ }
346
+
347
+ .status-text {
348
+ color: var(--text-primary);
349
+ line-height: 1.5;
350
+ }
351
+
352
+ /* Progress Section */
353
+ .progress-section {
354
+ margin-top: var(--space-md);
355
+ }
356
+
357
+ .progress-header {
358
+ display: flex;
359
+ justify-content: space-between;
360
+ align-items: center;
361
+ margin-bottom: var(--space-sm);
362
+ }
363
+
364
+ .progress-title {
365
+ font-size: 16px;
366
+ font-weight: 500;
367
+ color: var(--text-primary);
368
+ }
369
+
370
+ .progress-speed {
371
+ font-size: 14px;
372
+ color: var(--text-secondary);
373
+ font-family: var(--font-mono);
374
+ }
375
+
376
+ .progress-bar-container {
377
+ position: relative;
378
+ display: flex;
379
+ align-items: center;
380
+ gap: var(--space-sm);
381
+ }
382
+
383
+ .progress-bar {
384
+ flex: 1;
385
+ height: 8px;
386
+ background: var(--border-default);
387
+ border-radius: 4px;
388
+ overflow: hidden;
389
+ }
390
+
391
+ .progress-bar::-webkit-progress-bar {
392
+ background: var(--border-default);
393
+ border-radius: 4px;
394
+ }
395
+
396
+ .progress-bar::-webkit-progress-value {
397
+ background: var(--primary-500);
398
+ border-radius: 4px;
399
+ transition: width var(--transition-normal);
400
+ }
401
+
402
+ .progress-bar::-moz-progress-bar {
403
+ background: var(--primary-500);
404
+ border-radius: 4px;
405
+ }
406
+
407
+ .progress-percentage {
408
+ font-size: 14px;
409
+ font-weight: 500;
410
+ color: var(--text-primary);
411
+ min-width: 40px;
412
+ text-align: right;
413
+ }
414
+
415
+ /* Media Info Section */
416
+ .media-info-section {
417
+ background: var(--bg-surface);
418
+ border: 1px solid var(--border-default);
419
+ border-radius: var(--radius-md);
420
+ padding: var(--space-lg);
421
+ }
422
+
423
+ .media-info {
424
+ display: flex;
425
+ gap: var(--space-lg);
426
+ align-items: flex-start;
427
+ }
428
+
429
+ .media-thumbnail {
430
+ flex-shrink: 0;
431
+ width: 120px;
432
+ height: 80px;
433
+ background: var(--bg-page);
434
+ border: 1px solid var(--border-default);
435
+ border-radius: var(--radius-sm);
436
+ overflow: hidden;
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ }
441
+
442
+ .media-thumbnail img {
443
+ width: 100%;
444
+ height: 100%;
445
+ object-fit: cover;
446
+ }
447
+
448
+ .placeholder-icon {
449
+ width: 32px;
450
+ height: 32px;
451
+ fill: var(--text-secondary);
452
+ }
453
+
454
+ .media-details {
455
+ flex: 1;
456
+ min-width: 0;
457
+ }
458
+
459
+ .media-title {
460
+ font-size: 18px;
461
+ font-weight: 600;
462
+ line-height: 1.4;
463
+ color: var(--text-heading);
464
+ margin-bottom: var(--space-xs);
465
+ word-wrap: break-word;
466
+ }
467
+
468
+ .media-meta {
469
+ display: flex;
470
+ flex-wrap: wrap;
471
+ gap: var(--space-sm);
472
+ margin-bottom: var(--space-xs);
473
+ align-items: center;
474
+ }
475
+
476
+ .media-uploader {
477
+ font-size: 14px;
478
+ color: var(--text-secondary);
479
+ }
480
+
481
+ .platform-badge {
482
+ background: var(--primary-500);
483
+ color: var(--text-heading);
484
+ padding: 2px 8px;
485
+ border-radius: 12px;
486
+ font-size: 12px;
487
+ font-weight: 500;
488
+ text-transform: uppercase;
489
+ }
490
+
491
+ .duration {
492
+ font-size: 14px;
493
+ color: var(--text-secondary);
494
+ font-family: var(--font-mono);
495
+ }
496
+
497
+ .media-stats {
498
+ display: flex;
499
+ gap: var(--space-md);
500
+ }
501
+
502
+ .stat-item {
503
+ font-size: 14px;
504
+ color: var(--text-secondary);
505
+ }
506
+
507
+ /* Formats Section */
508
+ .formats-section {
509
+ background: var(--bg-surface);
510
+ border: 1px solid var(--border-default);
511
+ border-radius: var(--radius-md);
512
+ padding: var(--space-lg);
513
+ }
514
+
515
+ .format-filters {
516
+ display: flex;
517
+ gap: var(--space-xs);
518
+ }
519
+
520
+ .filter-btn {
521
+ padding: var(--space-xs) var(--space-sm);
522
+ background: transparent;
523
+ border: 1px solid var(--border-default);
524
+ border-radius: var(--radius-sm);
525
+ color: var(--text-secondary);
526
+ font-size: 14px;
527
+ cursor: pointer;
528
+ transition: all var(--transition-normal);
529
+ }
530
+
531
+ .filter-btn:hover {
532
+ border-color: var(--primary-500);
533
+ color: var(--primary-500);
534
+ }
535
+
536
+ .filter-btn.active {
537
+ background: var(--primary-500);
538
+ color: var(--text-heading);
539
+ border-color: var(--primary-500);
540
+ }
541
+
542
+ .format-list {
543
+ display: flex;
544
+ flex-direction: column;
545
+ gap: var(--space-sm);
546
+ margin-top: var(--space-md);
547
+ }
548
+
549
+ .format-item {
550
+ background: var(--bg-page);
551
+ border: 1px solid var(--border-default);
552
+ border-radius: var(--radius-md);
553
+ padding: var(--space-md);
554
+ cursor: pointer;
555
+ transition: all var(--transition-normal);
556
+ display: flex;
557
+ justify-content: space-between;
558
+ align-items: center;
559
+ min-height: 72px;
560
+ }
561
+
562
+ .format-item:hover {
563
+ background: rgba(255, 255, 255, 0.05);
564
+ border-color: var(--primary-500);
565
+ transform: translateY(-1px);
566
+ }
567
+
568
+ .format-item:focus {
569
+ outline: none;
570
+ border-color: var(--primary-500);
571
+ box-shadow: var(--shadow-focus);
572
+ }
573
+
574
+ .format-info {
575
+ flex: 1;
576
+ min-width: 0;
577
+ }
578
+
579
+ .format-title {
580
+ font-size: 16px;
581
+ font-weight: 500;
582
+ color: var(--text-heading);
583
+ line-height: 1.3;
584
+ margin-bottom: 2px;
585
+ word-wrap: break-word;
586
+ }
587
+
588
+ .format-details {
589
+ font-size: 14px;
590
+ color: var(--text-secondary);
591
+ line-height: 1.4;
592
+ font-family: var(--font-mono);
593
+ }
594
+
595
+ .format-actions {
596
+ display: flex;
597
+ gap: var(--space-xs);
598
+ align-items: center;
599
+ }
600
+
601
+ .format-item.downloading {
602
+ background: var(--primary-500);
603
+ color: var(--text-heading);
604
+ border-color: var(--primary-500);
605
+ }
606
+
607
+ .format-item.completed {
608
+ background: var(--success);
609
+ color: var(--bg-page);
610
+ border-color: var(--success);
611
+ }
612
+
613
+ .format-item.error {
614
+ background: var(--error);
615
+ color: var(--text-heading);
616
+ border-color: var(--error);
617
+ }
618
+
619
+ /* Queue Section */
620
+ .queue-section {
621
+ background: var(--bg-surface);
622
+ border: 1px solid var(--border-default);
623
+ border-radius: var(--radius-md);
624
+ padding: var(--space-lg);
625
+ }
626
+
627
+ .queue-list {
628
+ display: flex;
629
+ flex-direction: column;
630
+ gap: var(--space-sm);
631
+ }
632
+
633
+ .queue-item {
634
+ background: var(--bg-page);
635
+ border: 1px solid var(--border-default);
636
+ border-radius: var(--radius-md);
637
+ padding: var(--space-md);
638
+ display: flex;
639
+ align-items: center;
640
+ gap: var(--space-md);
641
+ min-height: 80px;
642
+ }
643
+
644
+ .queue-thumbnail {
645
+ width: 64px;
646
+ height: 40px;
647
+ background: var(--border-default);
648
+ border-radius: var(--radius-sm);
649
+ flex-shrink: 0;
650
+ display: flex;
651
+ align-items: center;
652
+ justify-content: center;
653
+ }
654
+
655
+ .queue-thumbnail img {
656
+ width: 100%;
657
+ height: 100%;
658
+ object-fit: cover;
659
+ border-radius: var(--radius-sm);
660
+ }
661
+
662
+ .queue-info {
663
+ flex: 1;
664
+ min-width: 0;
665
+ }
666
+
667
+ .queue-title {
668
+ font-size: 14px;
669
+ font-weight: 500;
670
+ color: var(--text-heading);
671
+ line-height: 1.3;
672
+ margin-bottom: 2px;
673
+ word-wrap: break-word;
674
+ }
675
+
676
+ .queue-url {
677
+ font-size: 12px;
678
+ color: var(--text-secondary);
679
+ font-family: var(--font-mono);
680
+ word-wrap: break-word;
681
+ overflow: hidden;
682
+ text-overflow: ellipsis;
683
+ }
684
+
685
+ .queue-progress {
686
+ width: 120px;
687
+ text-align: center;
688
+ }
689
+
690
+ .queue-progress .progress-bar {
691
+ height: 4px;
692
+ margin-bottom: 4px;
693
+ }
694
+
695
+ .queue-status {
696
+ font-size: 12px;
697
+ color: var(--text-secondary);
698
+ }
699
+
700
+ /* Error and Success Sections */
701
+ .error-section,
702
+ .success-section {
703
+ background: rgba(244, 63, 94, 0.1);
704
+ border: 1px solid var(--error);
705
+ border-radius: var(--radius-md);
706
+ padding: var(--space-lg);
707
+ }
708
+
709
+ .success-section {
710
+ background: rgba(34, 197, 94, 0.1);
711
+ border-color: var(--success);
712
+ }
713
+
714
+ .error-container,
715
+ .success-container {
716
+ min-height: 60px;
717
+ }
718
+
719
+ .error-item,
720
+ .success-item {
721
+ display: flex;
722
+ align-items: center;
723
+ gap: var(--space-sm);
724
+ margin-bottom: var(--space-sm);
725
+ }
726
+
727
+ .error-item:last-child,
728
+ .success-item:last-child {
729
+ margin-bottom: 0;
730
+ }
731
+
732
+ .error-icon {
733
+ color: var(--error);
734
+ font-size: 18px;
735
+ }
736
+
737
+ .success-icon {
738
+ color: var(--success);
739
+ font-size: 18px;
740
+ }
741
+
742
+ .error-text,
743
+ .success-text {
744
+ font-size: 16px;
745
+ line-height: 1.5;
746
+ }
747
+
748
+ /* Secondary Button */
749
+ .secondary-btn {
750
+ padding: var(--space-xs) var(--space-md);
751
+ background: transparent;
752
+ border: 1px solid var(--border-default);
753
+ border-radius: var(--radius-sm);
754
+ color: var(--text-primary);
755
+ font-size: 14px;
756
+ cursor: pointer;
757
+ transition: all var(--transition-normal);
758
+ }
759
+
760
+ .secondary-btn:hover {
761
+ border-color: var(--text-primary);
762
+ color: var(--text-heading);
763
+ }
764
+
765
+ /* Bottom Navigation */
766
+ .bottom-nav {
767
+ position: fixed;
768
+ bottom: 0;
769
+ left: 0;
770
+ right: 0;
771
+ background: var(--bg-surface);
772
+ border-top: 1px solid var(--border-default);
773
+ display: none;
774
+ padding: var(--space-sm) var(--space-md);
775
+ gap: var(--space-sm);
776
+ z-index: 1000;
777
+ }
778
+
779
+ .nav-item {
780
+ flex: 1;
781
+ background: transparent;
782
+ border: none;
783
+ color: var(--text-secondary);
784
+ cursor: pointer;
785
+ padding: var(--space-sm);
786
+ border-radius: var(--radius-sm);
787
+ transition: all var(--transition-normal);
788
+ display: flex;
789
+ flex-direction: column;
790
+ align-items: center;
791
+ gap: 4px;
792
+ }
793
+
794
+ .nav-item.active {
795
+ background: var(--primary-500);
796
+ color: var(--text-heading);
797
+ }
798
+
799
+ .nav-icon {
800
+ width: 20px;
801
+ height: 20px;
802
+ fill: currentColor;
803
+ }
804
+
805
+ .nav-label {
806
+ font-size: 12px;
807
+ font-weight: 500;
808
+ }
809
+
810
+ /* Settings Modal */
811
+ .modal {
812
+ position: fixed;
813
+ top: 0;
814
+ left: 0;
815
+ width: 100%;
816
+ height: 100%;
817
+ background: rgba(0, 0, 0, 0.8);
818
+ display: flex;
819
+ align-items: center;
820
+ justify-content: center;
821
+ z-index: 2000;
822
+ padding: var(--space-md);
823
+ }
824
+
825
+ .modal-content {
826
+ background: var(--bg-surface);
827
+ border: 1px solid var(--border-default);
828
+ border-radius: var(--radius-md);
829
+ max-width: 500px;
830
+ width: 100%;
831
+ max-height: 90vh;
832
+ overflow-y: auto;
833
+ }
834
+
835
+ .modal-header {
836
+ display: flex;
837
+ justify-content: space-between;
838
+ align-items: center;
839
+ padding: var(--space-lg);
840
+ border-bottom: 1px solid var(--border-default);
841
+ }
842
+
843
+ .modal-header h3 {
844
+ font-size: 20px;
845
+ font-weight: 600;
846
+ color: var(--text-heading);
847
+ }
848
+
849
+ .modal-close {
850
+ background: none;
851
+ border: none;
852
+ color: var(--text-secondary);
853
+ font-size: 24px;
854
+ cursor: pointer;
855
+ padding: 0;
856
+ width: 32px;
857
+ height: 32px;
858
+ display: flex;
859
+ align-items: center;
860
+ justify-content: center;
861
+ border-radius: 50%;
862
+ transition: all var(--transition-normal);
863
+ }
864
+
865
+ .modal-close:hover {
866
+ background: var(--border-default);
867
+ color: var(--text-primary);
868
+ }
869
+
870
+ .modal-body {
871
+ padding: var(--space-lg);
872
+ }
873
+
874
+ .setting-group {
875
+ display: flex;
876
+ justify-content: space-between;
877
+ align-items: center;
878
+ margin-bottom: var(--space-lg);
879
+ }
880
+
881
+ .setting-label {
882
+ font-size: 16px;
883
+ color: var(--text-primary);
884
+ }
885
+
886
+ .toggle {
887
+ position: relative;
888
+ display: inline-block;
889
+ width: 48px;
890
+ height: 24px;
891
+ }
892
+
893
+ .toggle input {
894
+ opacity: 0;
895
+ width: 0;
896
+ height: 0;
897
+ }
898
+
899
+ .toggle-slider {
900
+ position: absolute;
901
+ cursor: pointer;
902
+ top: 0;
903
+ left: 0;
904
+ right: 0;
905
+ bottom: 0;
906
+ background: var(--border-default);
907
+ border-radius: 24px;
908
+ transition: var(--transition-normal);
909
+ }
910
+
911
+ .toggle-slider:before {
912
+ position: absolute;
913
+ content: "";
914
+ height: 18px;
915
+ width: 18px;
916
+ left: 3px;
917
+ bottom: 3px;
918
+ background: var(--text-primary);
919
+ border-radius: 50%;
920
+ transition: var(--transition-normal);
921
+ }
922
+
923
+ .toggle input:checked + .toggle-slider {
924
+ background: var(--primary-500);
925
+ }
926
+
927
+ .toggle input:checked + .toggle-slider:before {
928
+ transform: translateX(24px);
929
+ background: var(--text-heading);
930
+ }
931
+
932
+ .setting-select {
933
+ background: var(--bg-page);
934
+ border: 1px solid var(--border-default);
935
+ border-radius: var(--radius-sm);
936
+ color: var(--text-primary);
937
+ padding: var(--space-xs) var(--space-sm);
938
+ font-size: 14px;
939
+ cursor: pointer;
940
+ }
941
+
942
+ .setting-select:focus {
943
+ outline: none;
944
+ border-color: var(--primary-500);
945
+ box-shadow: var(--shadow-focus);
946
+ }
947
+
948
+ .modal-footer {
949
+ padding: var(--space-lg);
950
+ border-top: 1px solid var(--border-default);
951
+ display: flex;
952
+ justify-content: flex-end;
953
+ }
954
+
955
+ .primary-btn {
956
+ padding: var(--space-sm) var(--space-lg);
957
+ background: var(--primary-500);
958
+ color: var(--text-heading);
959
+ border: none;
960
+ border-radius: var(--radius-sm);
961
+ font-size: 16px;
962
+ font-weight: 500;
963
+ cursor: pointer;
964
+ transition: all var(--transition-normal);
965
+ }
966
+
967
+ .primary-btn:hover {
968
+ background: var(--primary-700);
969
+ transform: translateY(-1px);
970
+ box-shadow: var(--shadow-hover);
971
+ }
972
+
973
+ .primary-btn:focus {
974
+ outline: none;
975
+ box-shadow: var(--shadow-focus);
976
+ }
977
+
978
+ /* Responsive Design */
979
+ @media (max-width: 767px) {
980
+ .app-container {
981
+ padding: var(--space-md);
982
+ }
983
+
984
+ .app-title {
985
+ font-size: 32px;
986
+ }
987
+
988
+ .header-content {
989
+ flex-direction: column;
990
+ gap: var(--space-md);
991
+ text-align: center;
992
+ }
993
+
994
+ .url-input-container {
995
+ flex-direction: column;
996
+ }
997
+
998
+ .analyze-btn {
999
+ width: 100%;
1000
+ }
1001
+
1002
+ .section-header {
1003
+ flex-direction: column;
1004
+ align-items: flex-start;
1005
+ gap: var(--space-sm);
1006
+ }
1007
+
1008
+ .media-info {
1009
+ flex-direction: column;
1010
+ text-align: center;
1011
+ }
1012
+
1013
+ .media-thumbnail {
1014
+ align-self: center;
1015
+ }
1016
+
1017
+ .format-item {
1018
+ flex-direction: column;
1019
+ align-items: flex-start;
1020
+ gap: var(--space-sm);
1021
+ }
1022
+
1023
+ .format-actions {
1024
+ align-self: flex-end;
1025
+ }
1026
+
1027
+ .queue-item {
1028
+ flex-direction: column;
1029
+ align-items: flex-start;
1030
+ gap: var(--space-sm);
1031
+ }
1032
+
1033
+ .queue-progress {
1034
+ width: 100%;
1035
+ }
1036
+
1037
+ .bottom-nav {
1038
+ display: flex;
1039
+ }
1040
+
1041
+ .app-main {
1042
+ padding-bottom: 100px; /* More space for bottom nav */
1043
+ }
1044
+ }
1045
+
1046
+ @media (min-width: 768px) {
1047
+ .bottom-nav {
1048
+ display: none;
1049
+ }
1050
+
1051
+ .app-main {
1052
+ padding-bottom: 0;
1053
+ }
1054
+ }
1055
+
1056
+ /* High Contrast Mode */
1057
+ @media (prefers-contrast: high) {
1058
+ :root {
1059
+ --border-default: #666666;
1060
+ --text-secondary: #CCCCCC;
1061
+ }
1062
+
1063
+ .url-input,
1064
+ .analyze-btn,
1065
+ .format-item,
1066
+ .queue-item {
1067
+ border-width: 2px;
1068
+ }
1069
+ }
1070
+
1071
+ /* Reduced Motion */
1072
+ @media (prefers-reduced-motion: reduce) {
1073
+ * {
1074
+ animation-duration: 0.01ms !important;
1075
+ animation-iteration-count: 1 !important;
1076
+ transition-duration: 0.01ms !important;
1077
+ }
1078
+
1079
+ .spinner {
1080
+ animation: none;
1081
+ }
1082
+
1083
+ .path {
1084
+ animation: none;
1085
+ }
1086
+ }
1087
+
1088
+ /* Print Styles */
1089
+ @media print {
1090
+ .app-header,
1091
+ .bottom-nav,
1092
+ .icon-btn,
1093
+ .analyze-btn {
1094
+ display: none;
1095
+ }
1096
+
1097
+ .app-container {
1098
+ padding: 0;
1099
+ }
1100
+
1101
+ body {
1102
+ background: white;
1103
+ color: black;
1104
+ }
1105
+ }