From 0b378f32f2d41634a8ed3aa64c016171b5a51dc1 Mon Sep 17 00:00:00 2001 From: jpmvaz Date: Sun, 13 Sep 2026 19:56:37 +0100 Subject: [PATCH] v2 --- README.md | 172 ++++++++++++++++++++++++++ install_dependencies.py | 260 ++++++++++++++++++++++++++++++++++++++++ yt_downloader.py | 171 ++++++++++++++++++++++++++ 3 files changed, 603 insertions(+) create mode 100644 README.md create mode 100644 install_dependencies.py create mode 100644 yt_downloader.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..2789c3a --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# YouTube Downloader + +> Powered by yt-dlp • Cross-platform • Python 3.8+ + +**Version 1.0** + +--- + +## Overview + +This package provides two Python scripts that together let you download YouTube videos and audio to your computer with a simple interactive menu. No programming knowledge is required to use them. + +| File | Purpose | +|------|---------| +| `yt_downloader.py` | Main script — interactive downloader with quality selection | +| `install_dependencies.py` | One-time setup — installs yt-dlp and ffmpeg automatically | +| `README.md` | This document | + +--- + +## System Requirements + +| Requirement | Details | +|-------------|---------| +| Python | Version 3.8 or newer (`python --version` to check) | +| pip | Included with Python 3.4+; upgraded automatically by installer | +| yt-dlp | Installed automatically by `install_dependencies.py` | +| ffmpeg | Installed automatically by `install_dependencies.py` (optional but strongly recommended) | +| Internet | Required for downloading videos | +| Disk space | Varies by video length and quality — allow at least 500 MB free | + +> **Note:** ffmpeg is optional but required for 1080p/4K quality and MP3 audio conversion. Without it the downloader falls back to pre-muxed formats (typically 720p max). + +--- + +## Quick Start + +### Step 1 — Run the installer (once only) + +Open a terminal or command prompt in the folder containing the scripts, then run: + +```bash +python install_dependencies.py +``` + +The installer will automatically detect your operating system and install everything needed. You should see green checkmarks confirming success. + +### Step 2 — Download a video + +```bash +python yt_downloader.py +``` + +Follow the on-screen prompts: + +- Paste the YouTube URL and press Enter +- Choose an action: download video, download audio, or list formats +- Select a quality (if downloading video) +- Choose a save folder (default is `./downloads` in the same directory) + +--- + +## Installer — `install_dependencies.py` + +Run this script once before using the downloader. It is safe to run again at any time; it will upgrade existing installations. + +### What it installs + +- **pip** — upgraded to the latest version to avoid dependency resolver issues +- **yt-dlp** — the core download engine (kept up to date) +- **ffmpeg** — the media processing library used for merging video/audio streams and converting to MP3 + +### Platform support + +| Platform | ffmpeg install method | +|----------|-----------------------| +| Windows | winget → Chocolatey → Scoop (uses whichever is found first) | +| macOS | Homebrew (installs Homebrew automatically if not present) | +| Linux — Debian / Ubuntu | apt-get | +| Linux — Fedora / RHEL | dnf + RPM Fusion repository | +| Linux — Arch Linux | pacman | + +### After the installer runs + +A verification step checks both tools and reports their detected versions. If ffmpeg shows a warning instead of a checkmark, try opening a new terminal window and running the installer again — PATH changes sometimes require a terminal restart. + +--- + +## Downloader — `yt_downloader.py` + +### Startup check + +When launched the script immediately checks for ffmpeg and shows its status: + +- ✅ **Green checkmark** — full quality available including 1080p, 4K, and MP3 audio +- ⚠️ **Warning** — ffmpeg not found; quality limited to ~720p and audio saves as `.m4a` + +### Menu options + +| Option | Description | +|--------|-------------| +| 1 — Download video | Downloads as MP4. Prompts for quality selection (see below). | +| 2 — Download audio only | Extracts audio. Saves as MP3 (with ffmpeg) or `.m4a` (without). | +| 3 — List available formats | Prints every available format ID, extension, resolution, and FPS for the given URL. Useful for troubleshooting. | + +### Quality options + +| Quality | Behaviour | +|---------|-----------| +| best (default) | Highest available quality for the video | +| 1080p | Up to 1920 × 1080 — requires ffmpeg | +| 720p | Up to 1280 × 720 | +| 480p | Up to 854 × 480 | +| 360p | Up to 640 × 360 — smallest file size | + +### Output folder + +By default files are saved to a `downloads/` subfolder next to the script. You can type any absolute or relative path at the prompt, for example: + +``` +C:\Users\Alice\Videos (Windows) +/home/alice/Videos (macOS / Linux) +./my-downloads (relative path, any OS) +``` + +### How quality selection works internally + +YouTube streams high-quality video and audio as separate files. ffmpeg is used to merge them after download. When ffmpeg is not available the script automatically switches to pre-muxed single-file formats to avoid errors — this is why quality is capped at ~720p without ffmpeg. + +--- + +## Troubleshooting + +| Error / Symptom | Solution | +|-----------------|----------| +| `ffmpeg is not installed. Aborting due to --abort-on-error` | Run `install_dependencies.py`, then open a new terminal and try again. | +| `python: command not found` | Try `python3` instead. On Windows, ensure Python is added to PATH during installation. | +| `pip is not recognised` | Run: `python -m ensurepip --upgrade` | +| Download stops or gives HTTP 403 | Update yt-dlp: `pip install --upgrade yt-dlp` (YouTube changes its API regularly) | +| Age-restricted or private video fails | These videos require authentication. yt-dlp supports cookie-based login — see the [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp). | +| 1080p not available after installing ffmpeg | Restart your terminal so the updated PATH takes effect, then try again. | +| Very slow download speed | Normal for large files. Progress is shown during download. Check your internet connection. | + +--- + +## Keeping Things Up to Date + +YouTube frequently changes its internal API. If downloads suddenly stop working, update yt-dlp: + +```bash +pip install --upgrade yt-dlp +``` + +You can also re-run the installer at any time — it upgrades all components safely. + +--- + +## Legal Notice + +> This tool is intended for personal use only. Downloading copyrighted content without the rights holder's permission may violate YouTube's Terms of Service and applicable copyright law. Only download content you have the right to download — such as your own uploads, Creative Commons-licensed videos, or content explicitly made available for offline use. + +--- + +## Quick Reference Card + +| Task | Command | +|------|---------| +| First-time setup | `python install_dependencies.py` | +| Download a video | `python yt_downloader.py` | +| Update yt-dlp only | `pip install --upgrade yt-dlp` | +| Check ffmpeg version | `ffmpeg -version` | +| Check yt-dlp version | `yt-dlp --version` | diff --git a/install_dependencies.py b/install_dependencies.py new file mode 100644 index 0000000..f0b5ef8 --- /dev/null +++ b/install_dependencies.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +""" +Installer for YouTube Downloader dependencies. +Installs: yt-dlp, ffmpeg +Supports: Windows, macOS, Linux (Debian/Ubuntu, Fedora/RHEL, Arch) +""" + +import os +import sys +import shutil +import subprocess +import platform + + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def run(cmd: list[str], check=True) -> subprocess.CompletedProcess: + print(f" → {' '.join(cmd)}") + return subprocess.run(cmd, check=check) + + +def ok(msg: str): + print(f" ✅ {msg}") + + +def info(msg: str): + print(f" ℹ️ {msg}") + + +def warn(msg: str): + print(f" ⚠️ {msg}") + + +def fail(msg: str): + print(f" ❌ {msg}") + sys.exit(1) + + +def section(title: str): + print(f"\n{'─' * 50}") + print(f" {title}") + print(f"{'─' * 50}") + + +# ── Platform detection ───────────────────────────────────────────────────────── + +OS = platform.system() # 'Windows', 'Darwin', 'Linux' +IS_WIN = OS == "Windows" +IS_MAC = OS == "Darwin" +IS_LINUX = OS == "Linux" + + +def linux_distro() -> str: + """Return 'debian', 'fedora', 'arch', or 'unknown'.""" + if shutil.which("apt-get"): + return "debian" + if shutil.which("dnf") or shutil.which("yum"): + return "fedora" + if shutil.which("pacman"): + return "arch" + return "unknown" + + +# ── Python / pip check ───────────────────────────────────────────────────────── + +def check_python(): + section("Checking Python") + major, minor = sys.version_info[:2] + print(f" Python {major}.{minor} detected") + if major < 3 or (major == 3 and minor < 8): + fail("Python 3.8+ is required. Please upgrade and re-run this script.") + ok("Python version is compatible") + + +def ensure_pip(): + section("Checking pip") + try: + import pip # noqa: F401 + ok("pip is available") + except ImportError: + info("pip not found — installing via ensurepip...") + run([sys.executable, "-m", "ensurepip", "--upgrade"]) + ok("pip installed") + + # Always upgrade pip to avoid resolver issues + run([sys.executable, "-m", "pip", "install", "--upgrade", "pip"]) + ok("pip is up to date") + + +# ── yt-dlp ───────────────────────────────────────────────────────────────────── + +def install_ytdlp(): + section("Installing yt-dlp") + run([sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"]) + ok("yt-dlp installed/updated") + + +# ── ffmpeg ───────────────────────────────────────────────────────────────────── + +def ffmpeg_already_installed() -> bool: + if shutil.which("ffmpeg"): + result = subprocess.run( + ["ffmpeg", "-version"], capture_output=True, text=True, check=False + ) + version_line = result.stdout.splitlines()[0] if result.stdout else "unknown" + info(f"ffmpeg already installed: {version_line}") + return True + return False + + +def install_ffmpeg_windows(): + """ + On Windows, download the pre-built ffmpeg binary via winget or chocolatey. + Falls back to manual instructions if neither is available. + """ + if shutil.which("winget"): + info("Using winget to install ffmpeg...") + run(["winget", "install", "--id", "Gyan.FFmpeg", "-e", "--silent"]) + elif shutil.which("choco"): + info("Using Chocolatey to install ffmpeg...") + run(["choco", "install", "ffmpeg", "-y"]) + elif shutil.which("scoop"): + info("Using Scoop to install ffmpeg...") + run(["scoop", "install", "ffmpeg"]) + else: + warn("No package manager found (winget / choco / scoop).") + print(""" + Please install ffmpeg manually: + 1. Go to https://www.gyan.dev/ffmpeg/builds/ + 2. Download the latest 'release essentials' zip + 3. Extract it and add the 'bin' folder to your PATH + """) + return + ok("ffmpeg installed") + + +def install_ffmpeg_mac(): + if shutil.which("brew"): + info("Using Homebrew to install ffmpeg...") + run(["brew", "install", "ffmpeg"]) + elif shutil.which("port"): + info("Using MacPorts to install ffmpeg...") + run(["sudo", "port", "install", "ffmpeg"]) + else: + warn("Homebrew not found. Installing Homebrew first...") + install_cmd = ( + '/bin/bash -c "$(curl -fsSL ' + 'https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + ) + subprocess.run(install_cmd, shell=True, check=True) + run(["brew", "install", "ffmpeg"]) + ok("ffmpeg installed") + + +def install_ffmpeg_linux(): + distro = linux_distro() + + if distro == "debian": + info("Using apt-get (Debian/Ubuntu)...") + run(["sudo", "apt-get", "update", "-qq"]) + run(["sudo", "apt-get", "install", "-y", "ffmpeg"]) + + elif distro == "fedora": + info("Using dnf (Fedora/RHEL)...") + # ffmpeg lives in RPM Fusion on Fedora + run(["sudo", "dnf", "install", "-y", + "https://download1.rpmfusion.org/free/fedora/" + "rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm"], check=False) + run(["sudo", "dnf", "install", "-y", "ffmpeg"]) + + elif distro == "arch": + info("Using pacman (Arch Linux)...") + run(["sudo", "pacman", "-Sy", "--noconfirm", "ffmpeg"]) + + else: + warn("Unrecognised Linux distro — cannot auto-install ffmpeg.") + print(""" + Please install ffmpeg with your distro's package manager, e.g.: + sudo apt-get install ffmpeg # Debian / Ubuntu + sudo dnf install ffmpeg # Fedora + sudo pacman -S ffmpeg # Arch + """) + return + + ok("ffmpeg installed") + + +def install_ffmpeg(): + section("Installing ffmpeg") + + if ffmpeg_already_installed(): + ok("Skipping — ffmpeg is already installed") + return + + if IS_WIN: + install_ffmpeg_windows() + elif IS_MAC: + install_ffmpeg_mac() + elif IS_LINUX: + install_ffmpeg_linux() + else: + warn(f"Unsupported OS: {OS}") + + +# ── Final verification ───────────────────────────────────────────────────────── + +def verify(): + section("Verification") + all_good = True + + # yt-dlp + try: + import yt_dlp + ok(f"yt-dlp {yt_dlp.version.__version__}") + except ImportError: + fail("yt-dlp import failed after installation!") + all_good = False + + # ffmpeg + if shutil.which("ffmpeg"): + result = subprocess.run( + ["ffmpeg", "-version"], capture_output=True, text=True, check=False + ) + version_line = result.stdout.splitlines()[0] if result.stdout else "unknown version" + ok(f"ffmpeg — {version_line}") + else: + warn("ffmpeg not found in PATH. High-quality merging may not work.") + warn("You can still download videos but 1080p/4K may be unavailable.") + all_good = False + + return all_good + + +# ── Main ─────────────────────────────────────────────────────────────────────── + +def main(): + print("\n" + "=" * 50) + print(" YouTube Downloader — Dependency Installer") + print("=" * 50) + print(f" OS : {OS} ({platform.machine()})") + print(f" Python : {sys.version.split()[0]}") + + check_python() + ensure_pip() + install_ytdlp() + install_ffmpeg() + + all_good = verify() + + print("\n" + "=" * 50) + if all_good: + print(" 🎉 All dependencies installed successfully!") + print(" Run python yt_downloader.py to start downloading.") + else: + print(" ⚠️ Setup finished with warnings (see above).") + print("=" * 50 + "\n") + + +if __name__ == "__main__": + main() diff --git a/yt_downloader.py b/yt_downloader.py new file mode 100644 index 0000000..5eb67b3 --- /dev/null +++ b/yt_downloader.py @@ -0,0 +1,171 @@ +#!/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()