172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
#!/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()
|