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.
File without changes
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ camel = camel_downloader.main:main
@@ -0,0 +1 @@
1
+ camel_downloader