3208 字
16 分钟
使用yt-dlp下载互联网上的任何音视频 | FFmpeg | Python
基于pip中的yt-dlp工具包打造的快速获取音频(AAC编码m4a)、视频(H.264编码mp4)脚本。
正在计算文章时效性...
查看修订历史
相关链接
yt-dlp 实用工具集
基于 yt-dlp 封装的 Python 脚本工具集,提供一键下载、自动转码功能。
脚本说明
依赖
- Python 3.10+
- FFmpeg
安装 yt-dlp 完整版:
pip install "yt-dlp[default]"quick_grab.py — 快速获取视频
下载任意网站视频,自动确保输出为 H.264 + AAC 的 MP4 文件。
特性:
- 自动检测并使用 GPU 硬件加速(NVIDIA NVENC / AMD AMF / Intel QSV),无 GPU 回退 CPU
- 探测源视频码率,按源码率匹配目标画质,避免转码后文件暴涨
- 支持断点续传,网络中断后重新运行可从上次位置继续
- 内置失败重试(10 次重试 + 指数退避)
基本用法:
# 下载最高画质(自动转码为 H.264 MP4)python quick_grab.py "https://www.youtube.com/watch?v=VIDEO_ID"
# 指定输出目录python quick_grab.py "URL" -d D:/Downloads
# 下载 720Ppython quick_grab.py "URL" -q 720
# 仅下载音频python quick_grab.py "URL" --audio
# 下载中英字幕python quick_grab.py "URL" --subtitle
# 下载整个播放列表python quick_grab.py "URL" --playlist
# 自定义编码质量(CRF 越小质量越高,默认 23)python quick_grab.py "URL" --crf 18 --preset slow
# 强制使用 CPU 编码(禁用 GPU 加速)python quick_grab.py "URL" --no-gpu
# 指定输出文件名python quick_grab.py "URL" -o "my_video.mp4"转码流程:
- yt-dlp 下载视频(自动合并视频+音频流)
- ffprobe 检测编码格式
- 如果已经是 H.264 + AAC + MP4 → 跳过(无需转码)
- 如果编码正确但容器非 MP4 → 仅 remux(无损,极快)
- 如果需要转码 → 自动探测源码率,按源码率匹配目标画质:
- NVENC:
-rc vbr -cq -b:v {源码率}k -maxrate {源码率}k - AMF/QSV:类似 VBR 码率控制
- CPU:高码率用
-b:v码率模式,低/未知码率用-crf质量模式 - 低码率(< 2000k)自动降低 CRF 至 20 以保画质
- NVENC:
quick_audio.py — 快速获取音频
下载任意网站音频,转为 AAC 编码的 M4A 文件,兼容 iPhone / Android / Windows / Mac / 车载等所有主流播放器。
基本用法:
# 下载音频,输出 AAC/M4A (192kbps)python quick_audio.py "https://www.youtube.com/watch?v=VIDEO_ID"
# 指定输出目录python quick_audio.py "URL" -d D:/Downloads
# 320kbps 高音质python quick_audio.py "URL" -b 320
# 输出 MP3 格式(兼容极老旧设备)python quick_audio.py "URL" --mp3
# 下载整个播放列表的音频python quick_audio.py "URL" --playlist脚本内容
quick_grab.py
"""快速获取 - 使用 yt-dlp 下载任意网站视频,确保输出 H.264 + AAC MP4
ffmpeg 调用链: 1. yt-dlp 内部调用 ffmpeg 合并分离的视频/音频流 (自动, -c copy) 2. 脚本调用 ffprobe 检测编码格式 3. 自动检测 GPU (NVIDIA NVENC), 优先使用硬件加速转码 4. 如需转码, 有 GPU 时用 h264_nvenc, 无 GPU 时用 libx264 + aac 5. 如编码已正确但容器非 mp4, 仅做 remux (-c copy 到 mp4 容器)
用法: python quick_grab.py <URL> python quick_grab.py <URL> -d D:/Downloads python quick_grab.py <URL> --audio python quick_grab.py <URL> -q 720 python quick_grab.py <URL> --crf 18 --preset slow # 高质量转码"""
import argparseimport ioimport osimport subprocessimport sys
# Fix Windows console encodingif sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
# ---------------------------------------------------------------------------# ffmpeg / ffprobe helpers# ---------------------------------------------------------------------------
def probe_codecs(filepath: str) -> tuple[str, str]: """用 ffprobe 检测视频和音频编码格式""" result = subprocess.run( ["ffprobe", "-v", "error", "-show_entries", "stream=codec_type,codec_name", "-of", "csv=p=0", filepath], capture_output=True, text=True, timeout=10, encoding="utf-8", errors="replace", ) vcodec = acodec = "none" for line in result.stdout.strip().split("\n"): # ffprobe csv=p=0 输出格式: codec_name,codec_type (如 "h264,video") parts = line.strip().split(",") if len(parts) == 2: cname, ctype = parts if ctype == "video": vcodec = cname elif ctype == "audio": acodec = cname return vcodec, acodec
def remux_to_mp4(input_path: str) -> bool: """仅更换容器为 mp4 (不重新编码, 极快)""" output_path = os.path.splitext(input_path)[0] + ".mp4" if input_path == output_path: return True cmd = [ "ffmpeg", "-i", input_path, "-c", "copy", "-movflags", "+faststart", "-y", output_path, ] result = subprocess.run(cmd) if result.returncode == 0 and os.path.isfile(output_path): os.remove(input_path) return True if os.path.isfile(output_path): os.remove(output_path) return False
def _detect_nvenc() -> bool: """检测 ffmpeg 是否支持 NVIDIA NVENC 硬件编码""" try: result = subprocess.run( ["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=10, encoding="utf-8", errors="replace", ) return "h264_nvenc" in result.stdout except (FileNotFoundError, subprocess.TimeoutExpired): return False
def _detect_hardware_encoder() -> str: """检测可用的硬件编码器, 返回编码器名称 (h264_nvenc / h264_amf / h264_qsv / libx264)""" try: result = subprocess.run( ["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=10, encoding="utf-8", errors="replace", ) for enc in ("h264_nvenc", "h264_amf", "h264_qsv"): if enc in result.stdout: return enc except (FileNotFoundError, subprocess.TimeoutExpired): pass return "libx264"
def _probe_source_bitrate(filepath: str) -> int: """获取源视频流码率 (kbps), 获取失败返回 0""" # 优先从流信息获取 try: result = subprocess.run( ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=bit_rate", "-of", "csv=p=0", filepath], capture_output=True, text=True, timeout=10, encoding="utf-8", errors="replace", ) br = result.stdout.strip() if br and br not in ("N/A", ""): return int(br) // 1000 # bps → kbps except (FileNotFoundError, subprocess.TimeoutExpired, ValueError): pass # 回退: 从文件大小 × 时长估算 try: result = subprocess.run( ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filepath], capture_output=True, text=True, timeout=10, encoding="utf-8", errors="replace", ) duration = float(result.stdout.strip()) file_size = os.path.getsize(filepath) return int(file_size * 8 / duration * 0.85 / 1000) # 视频约占85% except (FileNotFoundError, subprocess.TimeoutExpired, ValueError, OSError): return 0
def transcode(input_path: str, crf: int = 23, preset: str = "medium", use_gpu: bool = True) -> bool: """转码为 H.264 + AAC MP4 (参考源码率匹配画质, 优先硬件加速)""" base, ext = os.path.splitext(input_path) output_path = base + "_h264_tmp.mp4"
# 探测源视频码率 source_kbps = _probe_source_bitrate(input_path) # CRF 根据源码率调整: 低码率用更高质量 crf_value = 20 if 0 < source_kbps < 2000 else crf # 缓冲区 = 码率 × 2 buf_kbps = source_kbps * 2 if source_kbps > 0 else 0
# 选择编码器 encoder = _detect_hardware_encoder() if use_gpu else "libx264"
# --- NVENC --- if encoder == "h264_nvenc": _preset_map = { "ultrafast": "p1", "superfast": "p1", "veryfast": "p2", "faster": "p3", "fast": "p4", "medium": "p4", "slow": "p5", "slower": "p7", "veryslow": "p7", } nvenc_preset = _preset_map.get(preset, "p4") if source_kbps > 0: print(f" [GPU/NVENC] 源码率: {source_kbps}k | 目标: {source_kbps}k | cq={crf_value} | preset={nvenc_preset}") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "h264_nvenc", "-preset", nvenc_preset, "-rc", "vbr", "-cq", str(crf_value), "-b:v", f"{source_kbps}k", "-maxrate", f"{source_kbps}k", "-bufsize", f"{buf_kbps}k", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ] else: print(f" [GPU/NVENC] 未知源码率 | cq={crf_value} | preset={nvenc_preset}") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "h264_nvenc", "-preset", nvenc_preset, "-rc", "vbr", "-cq", str(crf_value), "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ]
# --- AMD AMF --- elif encoder == "h264_amf": if source_kbps > 0: print(f" [GPU/AMF] 源码率: {source_kbps}k | 目标: {source_kbps}k") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "h264_amf", "-quality", "speed", "-rc", "vbr", "-b:v", f"{source_kbps}k", "-maxrate", f"{source_kbps}k", "-bufsize", f"{buf_kbps}k", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ] else: print(f" [GPU/AMF] 未知源码率") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "h264_amf", "-quality", "speed", "-rc", "vbr", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ]
# --- Intel QSV --- elif encoder == "h264_qsv": if source_kbps > 0: print(f" [GPU/QSV] 源码率: {source_kbps}k | 目标: {source_kbps}k | quality={crf_value}") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "h264_qsv", "-preset", "medium", "-b:v", f"{source_kbps}k", "-maxrate", f"{source_kbps}k", "-bufsize", f"{buf_kbps}k", "-global_quality", str(crf_value), "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ] else: print(f" [GPU/QSV] 未知源码率 | quality={crf_value}") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "h264_qsv", "-preset", "medium", "-global_quality", str(crf_value), "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ]
# --- CPU libx264 --- else: if source_kbps > 0 and source_kbps >= 2000: print(f" [CPU/x264] 源码率: {source_kbps}k | 目标: {source_kbps}k | preset={preset}") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "libx264", "-preset", preset, "-b:v", f"{source_kbps}k", "-maxrate", f"{source_kbps}k", "-bufsize", f"{buf_kbps}k", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ] else: print(f" [CPU/x264] 未知/低码率 | crf={crf_value} | preset={preset}") cmd = [ "ffmpeg", "-i", input_path, "-c:v", "libx264", "-preset", preset, "-crf", str(crf_value), "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_path, ]
result = subprocess.run(cmd)
if result.returncode == 0 and os.path.isfile(output_path): os.replace(output_path, base + ".mp4") # 清理原始文件 (如果扩展名不同) original = input_path if os.path.isfile(original) and original != base + ".mp4": os.remove(original) return True
if os.path.isfile(output_path): os.remove(output_path) return False
# ---------------------------------------------------------------------------# yt-dlp helpers# ---------------------------------------------------------------------------
def _progress_hook(d: dict): """yt-dlp 下载进度回调""" if d["status"] == "downloading": pct = d.get("_percent_str", "?") spd = d.get("_speed_str", "?") eta = d.get("_eta_str", "?") print(f"\r 下载中: {pct} | 速度: {spd} | 剩余: {eta}", end="", flush=True) elif d["status"] == "finished": print("\n 下载完成,正在合并...")
def _find_output_file(ydl, info: dict) -> str | None: """根据 yt-dlp info 定位实际输出文件""" expected = ydl.prepare_filename(info) if os.path.isfile(expected): return expected # 合并后扩展名可能变为 mp4 base, _ = os.path.splitext(expected) for ext in (".mp4", ".mkv", ".webm", ".ts"): if os.path.isfile(base + ext): return base + ext return None
# ---------------------------------------------------------------------------# 主流程# ---------------------------------------------------------------------------
def grab(url: str, output_dir: str = ".", output_name: str | None = None, audio_only: bool = False, quality: int | None = None, playlist: bool = False, subtitle: bool = False, crf: int = 23, preset: str = "medium", use_gpu: bool = True) -> int: """下载视频并确保 H.264 + AAC MP4 输出""" try: import yt_dlp except ImportError: print("错误: 未安装 yt-dlp,请先执行 pip install yt-dlp") return 1
os.makedirs(output_dir, exist_ok=True)
outtmpl = (os.path.join(output_dir, output_name) if output_name else os.path.join(output_dir, "%(title)s.%(ext)s"))
ydl_opts: dict = { "outtmpl": outtmpl, "progress_hooks": [_progress_hook], # 断点续传 & 重试 "continuedl": True, "retries": 10, "fragment_retries": 10, "file_access_retries": 10, "retry_sleep_functions": {"http": 5, "fragment": 5, "file_access": 3}, }
# 格式选择 if audio_only: ydl_opts["format"] = "bestaudio/best" ydl_opts["postprocessors"] = [{ "key": "FFmpegExtractAudio", "preferredcodec": "best", }] elif quality: ydl_opts["format"] = ( f"bestvideo[height<={quality}]+bestaudio" f"/best[height<={quality}]/best" ) ydl_opts["merge_output_format"] = "mp4" else: ydl_opts["format"] = "bestvideo+bestaudio/best" ydl_opts["merge_output_format"] = "mp4"
if not playlist: ydl_opts["noplaylist"] = True if subtitle: ydl_opts["writesubs"] = True ydl_opts["writeautomaticsub"] = True ydl_opts["subtitleslangs"] = ["zh", "en"]
print(f"\n正在获取: {url}\n")
with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=True)
if info is None: print("错误: 无法获取视频信息") return 1
# --- 后处理: 确保 H.264 + AAC MP4 --- if audio_only: print("\n音频模式,跳过视频编码检查。")
else: # 收集所有需要检查的文件 if "entries" in info: entries = [e for e in info["entries"] if e] else: entries = [info]
print("\n[ 编码检查 ]") for entry in entries: filepath = _find_output_file(ydl, entry) if not filepath: continue
vcodec, acodec = probe_codecs(filepath) name = os.path.basename(filepath) print(f" {name}") print(f" 视频编码: {vcodec} | 音频编码: {acodec}")
is_h264 = vcodec == "h264" is_aac = acodec in ("aac", "none") is_mp4 = filepath.lower().endswith(".mp4")
if is_h264 and is_aac and is_mp4: print(" ✓ 已是 H.264 + AAC MP4,跳过")
elif is_h264 and is_aac and not is_mp4: print(" → 编码正确,封装为 MP4...") if remux_to_mp4(filepath): print(" ✓ 封装完成 (remux, 无损)") else: print(" ✗ 封装失败,保留原始文件")
else: print(f" → 需要转码 ({vcodec}/{acodec} → h264/aac)...") if transcode(filepath, crf=crf, preset=preset, use_gpu=use_gpu): print(" ✓ 转码完成") else: print(" ✗ 转码失败,保留原始文件")
print("\n全部完成!") return 0
def main(): p = argparse.ArgumentParser( description="快速获取 - 下载任意网站视频 (H.264 + AAC MP4)", ) p.add_argument("url", help="视频 URL") p.add_argument("-o", "--output", help="输出文件名") p.add_argument("-d", "--output-dir", default=".", help="输出目录 (默认: 当前目录)") p.add_argument("-a", "--audio", action="store_true", help="仅下载音频") p.add_argument("-q", "--quality", type=int, help="最高分辨率 (如 720, 1080)") p.add_argument("-p", "--playlist", action="store_true", help="下载整个播放列表") p.add_argument("-s", "--subtitle", action="store_true", help="下载字幕") p.add_argument("--crf", type=int, default=23, help="H.264 CRF 质量值 (默认: 23, 范围 0-51, 越小质量越高)") p.add_argument("--preset", default="medium", choices=["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"], help="编码速度预设 (默认: medium)") p.add_argument("--no-gpu", action="store_true", help="禁用 GPU 加速, 强制使用 CPU 软编码")
args = p.parse_args() sys.exit(grab( url=args.url, output_dir=args.output_dir, output_name=args.output, audio_only=args.audio, quality=args.quality, playlist=args.playlist, subtitle=args.subtitle, crf=args.crf, preset=args.preset, use_gpu=not args.no_gpu, ))
if __name__ == "__main__": main()quick_audio.py
"""快速获取音频 - 使用 yt-dlp 下载任意网站音频,转为 AAC 编码的 M4A 文件
输出 M4A (AAC) — 兼容 iPhone / Android / Windows / Mac / 车载 / 所有主流播放器。可选 --mp3 输出 MP3 格式以兼容极老旧设备。
用法: python quick_audio.py <URL> python quick_audio.py <URL> -d D:/Downloads python quick_audio.py <URL> -b 320 # 320kbps 高音质 python quick_audio.py <URL> --mp3 # 输出 MP3 格式"""
import argparseimport ioimport osimport subprocessimport sys
# Fix Windows console encodingif sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
# ---------------------------------------------------------------------------# helpers# ---------------------------------------------------------------------------
def _progress_hook(d: dict): """yt-dlp 下载进度回调""" if d["status"] == "downloading": pct = d.get("_percent_str", "?") spd = d.get("_speed_str", "?") eta = d.get("_eta_str", "?") print(f"\r 下载中: {pct} | 速度: {spd} | 剩余: {eta}", end="", flush=True) elif d["status"] == "finished": print("\n 下载完成,正在处理音频...")
def _find_file(ydl, info: dict) -> str | None: """定位 yt-dlp 实际输出的文件""" expected = ydl.prepare_filename(info) if os.path.isfile(expected): return expected base, _ = os.path.splitext(expected) for ext in (".m4a", ".mp3", ".ogg", ".opus", ".webm", ".wav", ".flac", ".wma", ".aac"): if os.path.isfile(base + ext): return base + ext return None
def _probe_audio_codec(filepath: str) -> str: """检测音频编码格式""" result = subprocess.run( ["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=codec_name", "-of", "csv=p=0", filepath], capture_output=True, text=True, timeout=10, encoding="utf-8", errors="replace", ) return result.stdout.strip() or "unknown"
def _convert(input_path: str, bitrate: int = 192, output_mp3: bool = False) -> bool: """转码音频为 AAC/M4A 或 MP3""" base, _ = os.path.splitext(input_path) target_ext = ".mp3" if output_mp3 else ".m4a" output_path = base + "_tmp" + target_ext
if output_mp3: codec = "libmp3lame" quality_arg = ["-b:a", f"{bitrate}k"] print(f" → 转码为 MP3 ({bitrate}kbps)...") else: codec = "aac" quality_arg = ["-b:a", f"{bitrate}k"] print(f" → 转码为 AAC/M4A ({bitrate}kbps)...")
cmd = [ "ffmpeg", "-i", input_path, "-c:a", codec, *quality_arg, "-movflags", "+faststart", "-y", output_path, ] result = subprocess.run(cmd)
if result.returncode == 0 and os.path.isfile(output_path): final_path = base + target_ext os.replace(output_path, final_path) # 清理原始文件 if os.path.isfile(input_path) and input_path != final_path: os.remove(input_path) return True
if os.path.isfile(output_path): os.remove(output_path) return False
# ---------------------------------------------------------------------------# 主流程# ---------------------------------------------------------------------------
def grab_audio(url: str, output_dir: str = ".", output_name: str | None = None, bitrate: int = 192, playlist: bool = False, output_mp3: bool = False) -> int: """下载音频并转为 AAC/M4A 或 MP3""" try: import yt_dlp except ImportError: print("错误: 未安装 yt-dlp,请先执行 pip install yt-dlp") return 1
os.makedirs(output_dir, exist_ok=True)
target_ext = ".mp3" if output_mp3 else ".m4a" outtmpl = (os.path.join(output_dir, output_name) if output_name else os.path.join(output_dir, "%(title)s.%(ext)s"))
ydl_opts: dict = { "outtmpl": outtmpl, "format": "bestaudio/best", "progress_hooks": [_progress_hook], # 断点续传 & 重试 "continuedl": True, "retries": 10, "fragment_retries": 10, "file_access_retries": 10, "retry_sleep_functions": {"http": 5, "fragment": 5, "file_access": 3}, }
if not playlist: ydl_opts["noplaylist"] = True
target_format = "MP3" if output_mp3 else "AAC/M4A" print(f"\n正在获取音频 ({target_format}, {bitrate}kbps): {url}\n")
with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=True)
if info is None: print("错误: 无法获取音频信息") return 1
# 收集条目 if "entries" in info: entries = [e for e in info["entries"] if e] else: entries = [info]
# 目标编码 target_codec = "mp3" if output_mp3 else "aac"
print(f"\n[ 音频检查 ]") for entry in entries: filepath = _find_file(ydl, entry) if not filepath: print(f" ⚠ 未找到文件: {entry.get('title', '?')}") continue
name = os.path.basename(filepath) codec = _probe_audio_codec(filepath) _, ext = os.path.splitext(filepath) is_target = (codec == target_codec and ext.lower() == target_ext)
print(f" {name}") print(f" 编码: {codec}")
if is_target: print(f" ✓ 已是目标格式 ({target_codec}{target_ext}),跳过") else: _convert(filepath, bitrate=bitrate, output_mp3=output_mp3) print(f" ✓ 转码完成 → {target_codec}{target_ext}")
print("\n全部完成!") return 0
def main(): p = argparse.ArgumentParser( description="快速获取音频 - 下载任意网站音频 (AAC/M4A 或 MP3)", ) p.add_argument("url", help="音频/视频 URL") p.add_argument("-d", "--output-dir", default=".", help="输出目录 (默认: 当前目录)") p.add_argument("-o", "--output", help="输出文件名") p.add_argument("-b", "--bitrate", type=int, default=192, help="音频码率 kbps (默认: 192, 建议: 128/192/256/320)") p.add_argument("-p", "--playlist", action="store_true", help="下载整个播放列表") p.add_argument("--mp3", action="store_true", help="输出 MP3 格式 (默认: AAC/M4A)") args = p.parse_args()
sys.exit(grab_audio( url=args.url, output_dir=args.output_dir, output_name=args.output, bitrate=args.bitrate, playlist=args.playlist, output_mp3=args.mp3, ))
if __name__ == "__main__": main()个人脚本仓库
Waiting for api.github.com...
如果觉得有用,欢迎给个 Star!如果有问题或建议,欢迎提 Issue 或 PR。
发现错误或想要改进这篇文章?
使用yt-dlp下载互联网上的任何音视频 | FFmpeg | Python
https://blog.srprolin.top/posts/2026-04-16-yt-dlp/