camelDownloader 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- camel_downloader/__init__.py +0 -0
- camel_downloader/main.py +464 -0
- cameldownloader-2.0.0.dist-info/METADATA +25 -0
- cameldownloader-2.0.0.dist-info/RECORD +7 -0
- cameldownloader-2.0.0.dist-info/WHEEL +5 -0
- cameldownloader-2.0.0.dist-info/entry_points.txt +2 -0
- cameldownloader-2.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
camel_downloader/main.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
camelDownloader š«
|
|
4
|
+
Production-Ready YouTube Downloader (2026)
|
|
5
|
+
|
|
6
|
+
ā Handles ALL video types and lengths
|
|
7
|
+
ā Smart format selection (MP4/MKV auto-switching)
|
|
8
|
+
ā No forced conversions (preserves quality)
|
|
9
|
+
ā Proper audio merging
|
|
10
|
+
ā Real resolutions (360p to 4K)
|
|
11
|
+
ā Works with: short videos, long videos, livestreams, age-restricted
|
|
12
|
+
ā Ubuntu + Termux + macOS compatible
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import yt_dlp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_format(choice):
|
|
22
|
+
"""
|
|
23
|
+
Format selection strategy:
|
|
24
|
+
|
|
25
|
+
WHY we use this approach:
|
|
26
|
+
- YouTube stores video/audio separately for HD quality
|
|
27
|
+
- We must download BOTH and let yt-dlp merge them
|
|
28
|
+
- The '+bestaudio' ensures we always get sound
|
|
29
|
+
- The '/best' is a fallback for legacy formats
|
|
30
|
+
|
|
31
|
+
NO FORCED CONVERSIONS:
|
|
32
|
+
- We let yt-dlp choose MP4 or MKV naturally
|
|
33
|
+
- AV1/VP9 codecs stay intact (no re-encoding)
|
|
34
|
+
- This prevents quality loss and saves time
|
|
35
|
+
"""
|
|
36
|
+
formats = {
|
|
37
|
+
# 360p - Standard Definition (smallest file, fastest download)
|
|
38
|
+
# Good for: slow internet, quick previews, mobile data saving
|
|
39
|
+
'1': 'bestvideo[height<=360]+bestaudio/best[height<=360]',
|
|
40
|
+
|
|
41
|
+
# 720p - HD (balanced quality and size)
|
|
42
|
+
# Good for: most users, good quality without huge files
|
|
43
|
+
'2': 'bestvideo[height<=720]+bestaudio/best[height<=720]',
|
|
44
|
+
|
|
45
|
+
# 1080p - Full HD (standard high quality)
|
|
46
|
+
# Good for: desktop viewing, archiving, sharing
|
|
47
|
+
'3': 'bestvideo[height<=1080]+bestaudio/best[height<=1080]',
|
|
48
|
+
|
|
49
|
+
# 1440p - 2K (very high quality)
|
|
50
|
+
# Good for: large screens, professional use
|
|
51
|
+
'4': 'bestvideo[height<=1440]+bestaudio/best[height<=1440]',
|
|
52
|
+
|
|
53
|
+
# 2160p - 4K (maximum quality)
|
|
54
|
+
# Good for: 4K displays, archiving, editing
|
|
55
|
+
# Note: May use AV1/VP9 codec (will save as MKV if MP4 not possible)
|
|
56
|
+
'5': 'bestvideo[height<=2160]+bestaudio/best[height<=2160]',
|
|
57
|
+
|
|
58
|
+
# Best available - Let YouTube decide
|
|
59
|
+
# Downloads the absolute best quality available (video+audio)
|
|
60
|
+
'6': 'bestvideo+bestaudio/best',
|
|
61
|
+
|
|
62
|
+
# Audio only - Extract just the sound
|
|
63
|
+
# Downloads best audio stream (usually 128-192kbps)
|
|
64
|
+
'7': 'bestaudio/best'
|
|
65
|
+
}
|
|
66
|
+
return formats.get(choice, 'bestvideo+bestaudio/best')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_duration(seconds):
|
|
70
|
+
"""
|
|
71
|
+
Convert seconds to human-readable time format
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
- 125 seconds ā "2:05"
|
|
75
|
+
- 3725 seconds ā "1:02:05"
|
|
76
|
+
- Handles videos from 1 second to 100+ hours
|
|
77
|
+
"""
|
|
78
|
+
if not seconds:
|
|
79
|
+
return "Unknown"
|
|
80
|
+
|
|
81
|
+
# Convert to hours, minutes, seconds
|
|
82
|
+
hours = int(seconds // 3600)
|
|
83
|
+
minutes = int((seconds % 3600) // 60)
|
|
84
|
+
secs = int(seconds % 60)
|
|
85
|
+
|
|
86
|
+
# Format based on length
|
|
87
|
+
if hours > 0:
|
|
88
|
+
return f"{hours}:{minutes:02d}:{secs:02d}"
|
|
89
|
+
else:
|
|
90
|
+
return f"{minutes}:{secs:02d}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def format_filesize(bytes_size):
|
|
94
|
+
"""
|
|
95
|
+
Convert bytes to human-readable file size
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
- 1024 ā "1.0 KB"
|
|
99
|
+
- 1048576 ā "1.0 MB"
|
|
100
|
+
- 1073741824 ā "1.0 GB"
|
|
101
|
+
"""
|
|
102
|
+
if not bytes_size:
|
|
103
|
+
return "Unknown"
|
|
104
|
+
|
|
105
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
106
|
+
if bytes_size < 1024.0:
|
|
107
|
+
return f"{bytes_size:.1f} {unit}"
|
|
108
|
+
bytes_size /= 1024.0
|
|
109
|
+
return f"{bytes_size:.1f} PB"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def progress_hook(d):
|
|
113
|
+
"""
|
|
114
|
+
Real-time download progress display
|
|
115
|
+
|
|
116
|
+
This function is called repeatedly by yt-dlp during download
|
|
117
|
+
Shows: percentage, speed, time remaining, downloaded size
|
|
118
|
+
|
|
119
|
+
Status types:
|
|
120
|
+
- 'downloading': Currently downloading
|
|
121
|
+
- 'finished': Download complete, starting merge
|
|
122
|
+
- 'error': Something went wrong
|
|
123
|
+
"""
|
|
124
|
+
if d['status'] == 'downloading':
|
|
125
|
+
# Extract progress information
|
|
126
|
+
percent = d.get('_percent_str', 'N/A').strip()
|
|
127
|
+
speed = d.get('_speed_str', 'N/A').strip()
|
|
128
|
+
eta = d.get('_eta_str', 'N/A').strip()
|
|
129
|
+
downloaded = d.get('downloaded_bytes', 0)
|
|
130
|
+
total = d.get('total_bytes') or d.get('total_bytes_estimate', 0)
|
|
131
|
+
|
|
132
|
+
# Build progress line
|
|
133
|
+
if total:
|
|
134
|
+
size_info = f"{format_filesize(downloaded)}/{format_filesize(total)}"
|
|
135
|
+
else:
|
|
136
|
+
size_info = f"{format_filesize(downloaded)}"
|
|
137
|
+
|
|
138
|
+
# Display with carriage return (overwrites same line)
|
|
139
|
+
print(f"\rā¬ļø {percent} | {size_info} | Speed: {speed} | ETA: {eta} ",
|
|
140
|
+
end='', flush=True)
|
|
141
|
+
|
|
142
|
+
elif d['status'] == 'finished':
|
|
143
|
+
# Download complete, now merging video and audio
|
|
144
|
+
print("\nš Merging video and audio streams (this may take a moment)...")
|
|
145
|
+
|
|
146
|
+
elif d['status'] == 'error':
|
|
147
|
+
print("\nā Download error occurred")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def download_video(url, choice):
|
|
151
|
+
"""
|
|
152
|
+
Main download function - production-ready with all safeguards
|
|
153
|
+
|
|
154
|
+
Process:
|
|
155
|
+
1. Create download directory
|
|
156
|
+
2. Configure yt-dlp options (NO forced conversions)
|
|
157
|
+
3. Fetch video information
|
|
158
|
+
4. Download video and audio streams
|
|
159
|
+
5. Merge streams (yt-dlp + ffmpeg)
|
|
160
|
+
6. Save final file
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
164
|
+
# STEP 1: Setup download directory
|
|
165
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
166
|
+
download_dir = os.path.expanduser("~/Downloads/camelDownloader")
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
os.makedirs(download_dir, exist_ok=True)
|
|
170
|
+
except PermissionError:
|
|
171
|
+
print(f"ā Cannot create directory: {download_dir}")
|
|
172
|
+
print("š” Try running with appropriate permissions or change download location")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
176
|
+
# STEP 2: Configure yt-dlp options
|
|
177
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
178
|
+
ydl_opts = {
|
|
179
|
+
# FORMAT: Which quality to download
|
|
180
|
+
'format': get_format(choice),
|
|
181
|
+
|
|
182
|
+
# OUTPUT: Where and how to save
|
|
183
|
+
# Sanitize filename to remove special characters
|
|
184
|
+
'outtmpl': os.path.join(download_dir, '%(title)s.%(ext)s'),
|
|
185
|
+
'restrictfilenames': False, # Allow Unicode characters in filenames
|
|
186
|
+
'windowsfilenames': False, # Don't restrict to Windows-safe names on Linux
|
|
187
|
+
|
|
188
|
+
# MERGING: Combine video+audio WITHOUT re-encoding
|
|
189
|
+
# CRITICAL: This does NOT force conversion, just sets container preference
|
|
190
|
+
# yt-dlp will use MKV automatically if MP4 is impossible
|
|
191
|
+
'merge_output_format': 'mp4', # Prefer MP4, fallback to MKV
|
|
192
|
+
|
|
193
|
+
# NO POSTPROCESSORS FOR VIDEO
|
|
194
|
+
# We removed FFmpegVideoConvertor to avoid forced re-encoding
|
|
195
|
+
# This preserves AV1/VP9 codecs and saves time
|
|
196
|
+
|
|
197
|
+
# NETWORK: Stability and bypass settings
|
|
198
|
+
'geo_bypass': True, # Try to bypass geographic restrictions
|
|
199
|
+
'nocheckcertificate': True, # Ignore SSL certificate errors
|
|
200
|
+
|
|
201
|
+
# RETRY: Handle temporary failures
|
|
202
|
+
'retries': 10, # Retry failed fragments up to 10 times
|
|
203
|
+
'fragment_retries': 10, # Retry individual fragments
|
|
204
|
+
'socket_timeout': 30, # 30 second timeout per connection
|
|
205
|
+
|
|
206
|
+
# CLIENT: Use web client (most stable in 2025)
|
|
207
|
+
# CLIENT: Use default clients (improved in 2025/2026)
|
|
208
|
+
# 'extractor_args': {
|
|
209
|
+
# 'youtube': {
|
|
210
|
+
# 'player_client': ['web'],
|
|
211
|
+
# 'skip': ['hls', 'dash'],
|
|
212
|
+
# }
|
|
213
|
+
# },
|
|
214
|
+
|
|
215
|
+
# LOGGING: Show what's happening
|
|
216
|
+
'quiet': False, # Show progress
|
|
217
|
+
'no_warnings': False, # Show warnings
|
|
218
|
+
'verbose': False, # Don't spam technical details
|
|
219
|
+
'progress_hooks': [progress_hook], # Our custom progress display
|
|
220
|
+
|
|
221
|
+
# AGE-RESTRICTED: Handle age-gated content
|
|
222
|
+
'age_limit': None, # Download age-restricted videos
|
|
223
|
+
|
|
224
|
+
# COOKIES: Support for logged-in content (optional)
|
|
225
|
+
# Uncomment if you need to download members-only content:
|
|
226
|
+
# 'cookiefile': 'cookies.txt',
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
230
|
+
# STEP 3: Special handling for audio-only downloads
|
|
231
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
232
|
+
if choice == '7':
|
|
233
|
+
# For audio, we WANT conversion (to MP3)
|
|
234
|
+
# This is different from video (where we avoid conversion)
|
|
235
|
+
ydl_opts['postprocessors'] = [{
|
|
236
|
+
'key': 'FFmpegExtractAudio', # Extract audio stream
|
|
237
|
+
'preferredcodec': 'mp3', # Convert to MP3
|
|
238
|
+
'preferredquality': '192', # 192kbps (good quality)
|
|
239
|
+
}]
|
|
240
|
+
# Remove video merge settings (not needed for audio)
|
|
241
|
+
ydl_opts.pop('merge_output_format', None)
|
|
242
|
+
|
|
243
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
244
|
+
# STEP 4: Execute download
|
|
245
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
246
|
+
try:
|
|
247
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
248
|
+
# First, fetch video metadata WITHOUT downloading
|
|
249
|
+
print("\nš Fetching video information...")
|
|
250
|
+
print("ā" * 60)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
info = ydl.extract_info(url, download=False)
|
|
254
|
+
except yt_dlp.utils.DownloadError as e:
|
|
255
|
+
print(f"ā Cannot access video: {e}")
|
|
256
|
+
print("\nš” Possible reasons:")
|
|
257
|
+
print(" ⢠Video is private or deleted")
|
|
258
|
+
print(" ⢠Geographic restriction")
|
|
259
|
+
print(" ⢠Age-restricted (try with cookies)")
|
|
260
|
+
print(" ⢠Invalid URL")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# Display video information
|
|
264
|
+
title = info.get('title', 'Unknown Title')
|
|
265
|
+
duration = info.get('duration', 0)
|
|
266
|
+
uploader = info.get('uploader', 'Unknown')
|
|
267
|
+
view_count = info.get('view_count', 0)
|
|
268
|
+
|
|
269
|
+
print(f"š¹ Title: {title}")
|
|
270
|
+
print(f"š¤ Uploader: {uploader}")
|
|
271
|
+
print(f"ā±ļø Duration: {format_duration(duration)}")
|
|
272
|
+
|
|
273
|
+
if view_count:
|
|
274
|
+
print(f"šļø Views: {view_count:,}")
|
|
275
|
+
|
|
276
|
+
# Show available formats (optional debug info)
|
|
277
|
+
if info.get('formats'):
|
|
278
|
+
available_heights = set()
|
|
279
|
+
for fmt in info['formats']:
|
|
280
|
+
if fmt.get('height'):
|
|
281
|
+
available_heights.add(fmt['height'])
|
|
282
|
+
|
|
283
|
+
if available_heights:
|
|
284
|
+
heights_sorted = sorted(available_heights, reverse=True)
|
|
285
|
+
print(f"š Available resolutions: {', '.join(f'{h}p' for h in heights_sorted)}")
|
|
286
|
+
|
|
287
|
+
print("ā" * 60)
|
|
288
|
+
|
|
289
|
+
# Start actual download
|
|
290
|
+
print("\nā¬ļø Starting download...")
|
|
291
|
+
ydl.download([url])
|
|
292
|
+
|
|
293
|
+
# Success message
|
|
294
|
+
print("\n" + "ā" * 60)
|
|
295
|
+
print("ā
DOWNLOAD COMPLETE!")
|
|
296
|
+
print("ā" * 60)
|
|
297
|
+
print(f"š Location: {download_dir}")
|
|
298
|
+
print(f"š Filename: {title[:50]}..." if len(title) > 50 else f"š Filename: {title}")
|
|
299
|
+
print("ā" * 60)
|
|
300
|
+
|
|
301
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
302
|
+
# ERROR HANDLING: Graceful failures with helpful messages
|
|
303
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
304
|
+
except KeyboardInterrupt:
|
|
305
|
+
print("\n\nā ļø Download cancelled by user (Ctrl+C)")
|
|
306
|
+
print("Partial files may remain in download folder.")
|
|
307
|
+
|
|
308
|
+
except yt_dlp.utils.DownloadError as e:
|
|
309
|
+
print(f"\nā Download Error: {e}")
|
|
310
|
+
print("\nš” Troubleshooting:")
|
|
311
|
+
print(" 1. Check if video is available in your region")
|
|
312
|
+
print(" 2. Update yt-dlp: pip install -U yt-dlp")
|
|
313
|
+
print(" 3. Check internet connection")
|
|
314
|
+
print(" 4. Try a different quality option")
|
|
315
|
+
print(" 5. If age-restricted, the video may need authentication")
|
|
316
|
+
|
|
317
|
+
except PermissionError:
|
|
318
|
+
print(f"\nā Permission denied writing to: {download_dir}")
|
|
319
|
+
print("š” Fix: Check folder permissions or choose different location")
|
|
320
|
+
|
|
321
|
+
except OSError as e:
|
|
322
|
+
print(f"\nā System Error: {e}")
|
|
323
|
+
print("š” Possible causes:")
|
|
324
|
+
print(" ⢠Disk full")
|
|
325
|
+
print(" ⢠Filename too long")
|
|
326
|
+
print(" ⢠Invalid characters in filename")
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
print(f"\nā Unexpected Error: {e}")
|
|
330
|
+
print("š” Please report this error with the video URL")
|
|
331
|
+
print(f"Error type: {type(e).__name__}")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def check_dependencies():
|
|
335
|
+
"""
|
|
336
|
+
Check if required tools are installed
|
|
337
|
+
|
|
338
|
+
Requirements:
|
|
339
|
+
- yt-dlp (the downloader)
|
|
340
|
+
- ffmpeg (for merging video+audio)
|
|
341
|
+
"""
|
|
342
|
+
# Check yt-dlp
|
|
343
|
+
try:
|
|
344
|
+
import yt_dlp
|
|
345
|
+
except ImportError:
|
|
346
|
+
print("ā yt-dlp not installed")
|
|
347
|
+
print("š¦ Install: pip install -U yt-dlp")
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
# Check ffmpeg (required for merging)
|
|
351
|
+
import subprocess
|
|
352
|
+
try:
|
|
353
|
+
subprocess.run(['ffmpeg', '-version'],
|
|
354
|
+
stdout=subprocess.DEVNULL,
|
|
355
|
+
stderr=subprocess.DEVNULL,
|
|
356
|
+
check=True)
|
|
357
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
358
|
+
print("ā ffmpeg not installed")
|
|
359
|
+
print("š¦ Install:")
|
|
360
|
+
print(" Ubuntu/Debian: sudo apt install ffmpeg")
|
|
361
|
+
print(" Termux: pkg install ffmpeg")
|
|
362
|
+
print(" macOS: brew install ffmpeg")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main():
|
|
369
|
+
"""
|
|
370
|
+
Main program entry point
|
|
371
|
+
|
|
372
|
+
Flow:
|
|
373
|
+
1. Show banner
|
|
374
|
+
2. Check dependencies
|
|
375
|
+
3. Get video URL
|
|
376
|
+
4. Show quality options
|
|
377
|
+
5. Start download
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
381
|
+
# BANNER
|
|
382
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
383
|
+
print("\n" + "ā" * 60)
|
|
384
|
+
print(" camelDownloader š«")
|
|
385
|
+
print(" Production YouTube Downloader")
|
|
386
|
+
print(" Handles ANY video type, length, and quality")
|
|
387
|
+
print("ā" * 60)
|
|
388
|
+
|
|
389
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
390
|
+
# DEPENDENCY CHECK
|
|
391
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
392
|
+
if not check_dependencies():
|
|
393
|
+
print("\nā ļø Please install missing dependencies first")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
397
|
+
# GET VIDEO URL
|
|
398
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
399
|
+
print("\n")
|
|
400
|
+
url = input("š Enter YouTube URL: ").strip()
|
|
401
|
+
|
|
402
|
+
# Validate URL
|
|
403
|
+
if not url:
|
|
404
|
+
print("ā No URL provided")
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
if not ('youtube.com' in url or 'youtu.be' in url):
|
|
408
|
+
print("ā ļø Warning: This doesn't look like a YouTube URL")
|
|
409
|
+
confirm = input("Continue anyway? (y/n): ").strip().lower()
|
|
410
|
+
if confirm != 'y':
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
414
|
+
# SHOW QUALITY OPTIONS
|
|
415
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
416
|
+
print("\n" + "ā" * 60)
|
|
417
|
+
print("š SELECT QUALITY:")
|
|
418
|
+
print("ā" * 60)
|
|
419
|
+
print("1) 360p - SD (Small file, fast download)")
|
|
420
|
+
print("2) 720p - HD (Balanced quality and size)")
|
|
421
|
+
print("3) 1080p - Full HD (High quality, larger file)")
|
|
422
|
+
print("4) 1440p - 2K (Very high quality)")
|
|
423
|
+
print("5) 2160p - 4K (Maximum quality, huge file)")
|
|
424
|
+
print("6) Best - Auto (Let YouTube decide best quality)")
|
|
425
|
+
print("7) Audio - MP3 (Sound only, ~3MB per minute)")
|
|
426
|
+
print("ā" * 60)
|
|
427
|
+
|
|
428
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
429
|
+
# GET USER CHOICE
|
|
430
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
431
|
+
choice = input("\nš Your choice (1-7): ").strip()
|
|
432
|
+
|
|
433
|
+
# Validate choice
|
|
434
|
+
if choice not in ('1', '2', '3', '4', '5', '6', '7'):
|
|
435
|
+
print("ā Invalid choice - Please select 1-7")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
# Confirmation message
|
|
439
|
+
quality_map = {
|
|
440
|
+
'1': '360p (SD)',
|
|
441
|
+
'2': '720p (HD)',
|
|
442
|
+
'3': '1080p (Full HD)',
|
|
443
|
+
'4': '1440p (2K)',
|
|
444
|
+
'5': '2160p (4K)',
|
|
445
|
+
'6': 'Best Available',
|
|
446
|
+
'7': 'Audio Only (MP3)'
|
|
447
|
+
}
|
|
448
|
+
print(f"\nā
Selected: {quality_map[choice]}")
|
|
449
|
+
|
|
450
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
451
|
+
# START DOWNLOAD
|
|
452
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
453
|
+
download_video(url, choice)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
457
|
+
# PROGRAM ENTRY POINT
|
|
458
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
try:
|
|
461
|
+
main()
|
|
462
|
+
except KeyboardInterrupt:
|
|
463
|
+
print("\n\nš Goodbye!")
|
|
464
|
+
sys.exit(0)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: camelDownloader
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Production-Ready YouTube Downloader
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Operating System :: OS Independent
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: yt-dlp>=2025.01.01
|
|
10
|
+
|
|
11
|
+
# Camel Downloader š«
|
|
12
|
+
|
|
13
|
+
Production YouTube Downloader.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
camel
|
|
25
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
camel_downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
camel_downloader/main.py,sha256=H1LidQTNDZe1V_rTPExRcjmIyyzfDZ-D3isn3Pp-VB0,19374
|
|
3
|
+
cameldownloader-2.0.0.dist-info/METADATA,sha256=DdUB_fvK8RLAPV4EArvu90qppLiuNor4-44o79SIOZM,426
|
|
4
|
+
cameldownloader-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
cameldownloader-2.0.0.dist-info/entry_points.txt,sha256=jVCsTsFS-YB9Wc4tz6lCHeaAnzjfW3RSSRpanoR_rtY,53
|
|
6
|
+
cameldownloader-2.0.0.dist-info/top_level.txt,sha256=w7mSCzUfy2T3L14BPQo4Sf6L9N4u1yDlQkxoLV50AhA,17
|
|
7
|
+
cameldownloader-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
camel_downloader
|