v2
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user