291 lines
8.9 KiB
Python
291 lines
8.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
YouTube Audio Downloader
|
|
|
|
This module provides functionality to download audio from YouTube videos using yt-dlp.
|
|
It supports cross-platform operation (Windows, macOS, Linux) and includes automatic
|
|
yt-dlp binary management with built-in protection against overwriting existing files.
|
|
|
|
Key Features:
|
|
- Download audio from YouTube videos as MP3 files
|
|
- Automatic yt-dlp binary download and updates
|
|
- Cross-platform support (Windows, macOS, Linux)
|
|
- Customizable audio quality and bitrate
|
|
- Default output location with overwrite capability
|
|
- Custom output locations with overwrite protection
|
|
- Automatic directory creation for custom paths
|
|
|
|
Default Behavior:
|
|
- Output: ~/tmp/download_audio_result.mp3 (allows overwrite)
|
|
- Quality: bestaudio[abr<=64]/worstaudio
|
|
- Format: MP3
|
|
- Bitrate: 64K
|
|
|
|
Custom Output:
|
|
- Cannot overwrite existing files (raises FileExistsError)
|
|
- Creates parent directories automatically
|
|
|
|
Example Usage:
|
|
# Download with defaults (allows overwrite)
|
|
$ python download_audio.py --video-url "https://www.youtube.com/watch?v=d6rZtgHcbWA"
|
|
|
|
# Custom quality and bitrate
|
|
$ python download_audio.py --video-url "https://youtu.be/xyz" --quality "bestaudio" --bitrate "128K"
|
|
|
|
# Custom output location (cannot overwrite existing)
|
|
$ python download_audio.py --video-url "https://youtu.be/xyz" --output "/path/to/audio.mp3"
|
|
|
|
Requirements:
|
|
- ffmpeg installed at ~/ffmpeg
|
|
- Internet connection for downloading yt-dlp and videos
|
|
- yt-dlp will be automatically downloaded if not present
|
|
|
|
Platform Support:
|
|
- Windows: Downloads yt-dlp.exe
|
|
- macOS: Downloads yt-dlp_macos
|
|
- Linux: Downloads yt-dlp_linux
|
|
|
|
Author: sanhe
|
|
Plugin: youtube@sanhe-claude-code-plugins
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import subprocess
|
|
import urllib.request
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
# Detect OS
|
|
IS_WINDOWS = False
|
|
IS_MACOS = False
|
|
IS_LINUX = False
|
|
|
|
_platform = sys.platform
|
|
if _platform in ["win32", "cygwin"]:
|
|
IS_WINDOWS = True
|
|
elif _platform == "darwin":
|
|
IS_MACOS = True
|
|
elif _platform == "linux":
|
|
IS_LINUX = True
|
|
else:
|
|
raise RuntimeError(f"Unsupported platform: {_platform}")
|
|
|
|
dir_home = Path.home()
|
|
dir_tmp = dir_home / "tmp"
|
|
try:
|
|
dir_tmp.mkdir(exist_ok=True)
|
|
except Exception: # pragma: no cover
|
|
pass
|
|
path_audio = dir_tmp / "download_audio_result.mp3"
|
|
|
|
if IS_WINDOWS:
|
|
filename = "yt-dlp.exe"
|
|
path_yt_dlp = dir_home / "yt-dlp.exe"
|
|
elif IS_MACOS:
|
|
filename = "yt-dlp_macos"
|
|
path_yt_dlp = dir_home / "yt-dlp_macos"
|
|
elif IS_LINUX:
|
|
filename = "yt-dlp_linux"
|
|
path_yt_dlp = dir_home / "yt-dlp_linux"
|
|
else:
|
|
raise RuntimeError("Unsupported operating system")
|
|
|
|
path_ffmpeg = dir_home / "ffmpeg"
|
|
|
|
|
|
def get_latest_yt_dlp_release() -> str:
|
|
"""Get the latest yt-dlp release version from GitHub API."""
|
|
api_url = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest"
|
|
|
|
with urllib.request.urlopen(api_url) as response:
|
|
data = json.loads(response.read().decode())
|
|
tag_name = data["tag_name"]
|
|
return tag_name
|
|
|
|
|
|
def download_yt_dlp():
|
|
"""Download the appropriate yt-dlp binary based on the OS."""
|
|
# Get latest release version
|
|
latest_version = get_latest_yt_dlp_release()
|
|
print(f"Latest yt-dlp version: {latest_version}")
|
|
|
|
# Determine download URL and local path based on OS
|
|
download_url = f"https://github.com/yt-dlp/yt-dlp/releases/download/{latest_version}/{filename}"
|
|
|
|
print(f"Downloading {filename} from {download_url}...")
|
|
print(f"Saving to {path_yt_dlp}...")
|
|
|
|
# Download the file
|
|
urllib.request.urlretrieve(download_url, path_yt_dlp)
|
|
|
|
# Set executable permissions for macOS and Linux
|
|
if IS_MACOS or IS_LINUX:
|
|
os.chmod(path_yt_dlp, 0o755)
|
|
|
|
print(f"Successfully downloaded yt-dlp to {path_yt_dlp}")
|
|
|
|
|
|
def download_audio(
|
|
video_url: str,
|
|
quality: str = "bestaudio[abr<=64]/worstaudio",
|
|
audio_format: str = "mp3",
|
|
bitrate: str = "64K",
|
|
output_path: str = None,
|
|
):
|
|
"""
|
|
Download audio from a YouTube video using yt-dlp.
|
|
|
|
Args:
|
|
video_url: YouTube video URL to download
|
|
quality: yt-dlp quality selector (default: "bestaudio[abr<=64]/worstaudio")
|
|
audio_format: Output audio format (default: "mp3")
|
|
bitrate: Audio bitrate (default: "64K")
|
|
output_path: Custom output path (default: ~/tmp/download_audio_result.mp3)
|
|
|
|
Returns:
|
|
Path to the downloaded audio file
|
|
"""
|
|
output = output_path if output_path else str(path_audio)
|
|
|
|
args = [
|
|
f"{path_yt_dlp}",
|
|
"-f",
|
|
quality,
|
|
"--extract-audio",
|
|
"--audio-format",
|
|
audio_format,
|
|
"--audio-quality",
|
|
bitrate,
|
|
"-o",
|
|
output,
|
|
"--restrict-filenames",
|
|
"--ffmpeg-location",
|
|
f"{str(path_ffmpeg)}",
|
|
video_url,
|
|
]
|
|
result = subprocess.run(args, check=True, capture_output=True)
|
|
return output
|
|
|
|
|
|
def main():
|
|
"""
|
|
Main CLI entry point for downloading YouTube audio.
|
|
|
|
This script downloads audio from YouTube videos using yt-dlp and ffmpeg.
|
|
It automatically downloads yt-dlp if not present in the home directory.
|
|
|
|
Example usage:
|
|
# Use default output location (allows overwrite)
|
|
python download_audio.py --video-url "https://www.youtube.com/watch?v=d6rZtgHcbWA"
|
|
|
|
# Specify custom quality and bitrate
|
|
python download_audio.py --video-url "https://youtu.be/xyz" --quality "bestaudio" --bitrate "128K"
|
|
|
|
# Specify custom output location (cannot overwrite existing file)
|
|
python download_audio.py --video-url "https://youtu.be/xyz" --output "/path/to/output.mp3"
|
|
|
|
Requirements:
|
|
- ffmpeg must be installed at ~/ffmpeg
|
|
- yt-dlp will be automatically downloaded to home directory if not present
|
|
|
|
Note:
|
|
- Default location (~/tmp/download_audio_result.mp3) allows overwrite
|
|
- Custom output locations cannot overwrite existing files
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Download audio from YouTube videos as MP3 files using yt-dlp",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
%(prog)s --video-url "https://www.youtube.com/watch?v=d6rZtgHcbWA"
|
|
%(prog)s --video-url "https://youtu.be/xyz" --quality "bestaudio" --bitrate "128K"
|
|
%(prog)s --video-url "https://youtu.be/xyz" --output "/custom/path/audio.mp3"
|
|
|
|
Quality options:
|
|
bestaudio - Best quality audio available
|
|
bestaudio[abr<=64] - Best audio with max 64kbps bitrate
|
|
worstaudio - Lowest quality audio (smallest file)
|
|
""",
|
|
)
|
|
|
|
# Required arguments
|
|
parser.add_argument(
|
|
"--video-url",
|
|
type=str,
|
|
required=True,
|
|
help="YouTube video URL to download (required)",
|
|
)
|
|
|
|
# Optional arguments for yt-dlp customization
|
|
parser.add_argument(
|
|
"--quality",
|
|
type=str,
|
|
default="bestaudio[abr<=64]/worstaudio",
|
|
help='Quality selector for yt-dlp (default: "bestaudio[abr<=64]/worstaudio")',
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--audio-format",
|
|
type=str,
|
|
default="mp3",
|
|
help="Output audio format: mp3, aac, wav, etc. (default: mp3)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--bitrate",
|
|
type=str,
|
|
default="64K",
|
|
help="Audio bitrate: 64K, 128K, 192K, 320K, etc. (default: 64K)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--output",
|
|
type=str,
|
|
default=None,
|
|
help=f"Custom output file path (default: {path_audio}). Note: Default location allows overwrite, custom locations cannot overwrite existing files.",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Ensure yt-dlp is downloaded
|
|
if not path_yt_dlp.exists():
|
|
print("yt-dlp not found, downloading...")
|
|
download_yt_dlp()
|
|
|
|
# Determine output path and check for existing files
|
|
if args.output:
|
|
path_output = Path(args.output).expanduser()
|
|
# For custom locations, check if file already exists
|
|
if (path_output != path_audio) and path_output.exists():
|
|
raise FileExistsError(
|
|
f"Output file already exists at {path_output}. "
|
|
f"Please choose a different location or remove the existing file. "
|
|
f"(Default location {path_audio} allows overwrite)"
|
|
)
|
|
# Ensure output directory exists
|
|
path_output.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path_str = str(path_output)
|
|
else:
|
|
# Use default location (allows overwrite)
|
|
path_audio.unlink(missing_ok=True)
|
|
output_path_str = None
|
|
|
|
# Download the audio
|
|
output_file = download_audio(
|
|
video_url=args.video_url,
|
|
quality=args.quality,
|
|
audio_format=args.audio_format,
|
|
bitrate=args.bitrate,
|
|
output_path=output_path_str,
|
|
)
|
|
|
|
print(f"✓ Audio successfully downloaded from {args.video_url}")
|
|
print(f"✓ Saved to: {output_file}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|