#!/usr/bin/env python3 """ YouTube Video Downloader Requires: yt-dlp → pip install yt-dlp """ import subprocess import sys import os import shutil def check_ytdlp(): try: import yt_dlp # noqa: F401 except ImportError: print("Installing yt-dlp...") subprocess.check_call([sys.executable, "-m", "pip", "install", "yt-dlp"]) def has_ffmpeg() -> bool: """Return True if ffmpeg is available on PATH.""" return shutil.which("ffmpeg") is not None def list_formats(url: str): """Print available formats for a video.""" import yt_dlp with yt_dlp.YoutubeDL({"quiet": True}) as ydl: info = ydl.extract_info(url, download=False) print(f"\nTitle: {info.get('title')}") print(f"Duration: {info.get('duration_string', 'N/A')}\n") print(f"{'ID':<12} {'EXT':<6} {'RESOLUTION':<12} {'FPS':<6} NOTE") print("-" * 60) for f in info.get("formats", []): fid = f.get("format_id", "") ext = f.get("ext", "") res = f.get("resolution") or f.get("format_note", "") fps = str(f.get("fps") or "") note = f.get("format_note", "") print(f"{fid:<12} {ext:<6} {res:<12} {fps:<6} {note}") def build_format_string(quality: str, ffmpeg: bool) -> str: """ Return a yt-dlp format string appropriate for whether ffmpeg is available. With ffmpeg → prefer separate video+audio streams (best quality, then merge). Without ffmpeg → request a single pre-muxed file so no merging is needed. """ if ffmpeg: return { "best": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", "1080p": "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]/best[height<=1080]", "720p": "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720][ext=mp4]/best[height<=720]", "480p": "bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480][ext=mp4]/best[height<=480]", "360p": "bestvideo[height<=360][ext=mp4]+bestaudio[ext=m4a]/best[height<=360][ext=mp4]/best[height<=360]", }.get(quality, "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best") else: # Single-file (pre-muxed) formats — no merging required return { "best": "best[ext=mp4]/best", "1080p": "best[height<=1080][ext=mp4]/best[height<=1080]", "720p": "best[height<=720][ext=mp4]/best[height<=720]", "480p": "best[height<=480][ext=mp4]/best[height<=480]", "360p": "best[height<=360][ext=mp4]/best[height<=360]", }.get(quality, "best[ext=mp4]/best") def download(url: str, quality: str, output_dir: str, audio_only: bool): """Download a video or audio from YouTube.""" import yt_dlp ffmpeg = has_ffmpeg() os.makedirs(output_dir, exist_ok=True) outtmpl = os.path.join(output_dir, "%(title)s.%(ext)s") if audio_only: if ffmpeg: ydl_opts = { "format": "bestaudio/best", "outtmpl": outtmpl, "postprocessors": [{ "key": "FFmpegExtractAudio", "preferredcodec": "mp3", "preferredquality": "192", }], } else: # Without ffmpeg, download best audio as-is (usually .m4a) print(" ⚠️ ffmpeg not found — audio will be saved as .m4a instead of .mp3") ydl_opts = { "format": "bestaudio[ext=m4a]/bestaudio", "outtmpl": outtmpl, } else: fmt = build_format_string(quality, ffmpeg) ydl_opts = { "format": fmt, "outtmpl": outtmpl, } if ffmpeg: ydl_opts["merge_output_format"] = "mp4" with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([url]) def prompt_quality() -> str: options = ["best", "1080p", "720p", "480p", "360p"] print("\nSelect quality:") for i, opt in enumerate(options, 1): print(f" {i}. {opt}") while True: choice = input("Enter number (default 1 = best): ").strip() or "1" if choice.isdigit() and 1 <= int(choice) <= len(options): return options[int(choice) - 1] print("Invalid choice, try again.") def main(): check_ytdlp() print("=" * 50) print(" YouTube Downloader (powered by yt-dlp)") print("=" * 50) # Show ffmpeg status upfront if has_ffmpeg(): print(" ✅ ffmpeg detected — full quality available") else: print(" ⚠️ ffmpeg NOT found — quality may be limited (max ~720p)") print(" Run install_dependencies.py to fix this\n") url = input("\nEnter YouTube URL: ").strip() if not url: print("No URL provided. Exiting.") sys.exit(1) print("\nWhat would you like to do?") print(" 1. Download video") print(" 2. Download audio only (MP3)") print(" 3. List available formats") mode = input("Enter choice (1/2/3): ").strip() if mode == "3": list_formats(url) sys.exit(0) audio_only = mode == "2" quality = "best" if not audio_only: quality = prompt_quality() output_dir = input("\nSave to folder (default: ./downloads): ").strip() or "./downloads" print(f"\nDownloading {'audio' if audio_only else f'{quality} video'}...") print(f"Saving to: {os.path.abspath(output_dir)}\n") try: download(url, quality, output_dir, audio_only) print("\n✅ Download complete!") except Exception as e: print(f"\n❌ Error: {e}") sys.exit(1) if __name__ == "__main__": main()