使用 FastAPI、yt-dlp 和 ffmpeg 构建音频提取器(YouTube MP3)

发布: (2026年1月9日 GMT+8 07:05)
8 min read
原文: Dev.to

Source: Dev.to

构建一个使用 FastAPI、yt‑dlp 和 ffmpeg 的 YouTube MP3 音频提取器的封面图片

Mario

介绍

从视频中提取音频以便像播客一样收听或复习课堂内容是一个非常典型的使用场景。当你想要做到以下几点时,问题就出现了:

  • 不在电脑上安装任何奇怪的软件
  • 在移动设备上操作

在这篇文章中,我会讲述我们在

👉

以及在以下工具中

👉

使用的音频提取器内部是如何构建的。

我不会解释完整的代码,但会说明关键思路:FastAPIyt‑dlpffmpegreCAPTCHA以及一些技巧,使其在 YouTube(包括 Shorts)上能够相对良好地运行。

一般流程:从 URL 到可下载的 MP3

Endpoint “URL → MP3” 的流程大致如下:

  1. 用户发送一个 POST,内容包括:
    • source_url:YouTube 链接(或其他兼容平台)
    • calidad_mp3192k128k64k
    • recaptcha_token:用于防止滥用/机器人
  2. 验证 reCAPTCHA 和 URL。
  3. 使用 yt‑dlp 检查可用的格式并选择最佳音频流。
  4. 只下载音频(如果没有其他选项,则下载视频 + 音频的渐进流)。
  5. 将该文件交给 ffmpeg,按请求的比特率转换为 MP3。
  6. 返回一个 JSON,内容包括:
    • download_url:指向类似 /descargar/{filename} 的下载端点
  7. 清理临时文件。

对用户而言,这一切只是一 个非常简洁的表单

👉

HTTP层:FastAPI + reCAPTCHA

Web 部分是一个使用表单的经典 FastAPI 端点:

@router.post("/convertir_video_a_mp3")
async def convertir_enlace_a_mp3(
    request: Request,
    source_url: str = Form(...),
    calidad_mp3: str = Form("128k"),
):
    # 1) Log de request (request_id, IP, path, etc.)
    # 2) Verificar reCAPTCHA con el token del formulario
    # 3) Validar/normalizar la URL
    # 4) Lanzar la lógica de descarga + conversión
    ...

需要注意的事项

  • reCAPTCHA:在使用 yt‑dlp 消耗 CPU/网络之前进行验证。
  • URL 正规化:我们使用函数 normalize_youtube_url(...),它:
    • 强制使用 https://
    • 清除 YouTube URL 中多余的参数
  • 请求 ID(rid):每个请求在日志中带有一个 UUID,这在调试具体视频的问题时非常有帮助。

Source:

yt‑dlp:正确选择音频流

yt‑dlp 会返回大量的格式信息。与其使用简单的 "format": "bestaudio/best",我们更关注以下几点:

  • 避免没有直接 URL 的格式(如 storyboards、缩略图等)
  • 优先选择 仅音频(例如 m4a/140),如果可用的话
  • 忽略伪空的格式
  • 为罕见情况提供 渐进式(视频 + 音频)回退

格式选择函数的思路大致如下(简化的伪代码):

def pick_best_audio_format(info_dict):
    fmts = info_dict.get("formats") or []

    def is_valid_audio(f):
        return (
            f.get("vcodec") == "none"
            and (f.get("acodec") or "").lower() not in {"", "none"}
            and has_url(f)
            and not is_storyboard(f)
        )

    audio_only = [f for f in fmts if is_valid_audio(f)]

    # 如果存在 m4a / id 140(在 YouTube 上非常常见),优先使用
    m4a = [f for f in audio_only if f.get("ext") == "m4a" or f.get("format_id") == "140"]
    if m4a:
        return best_by_abr(m4a)

    # 其余合理的仅音频格式
    if audio_only:
        return best_by_abr(audio_only)

    # 回退:渐进式视频+音频(我们会用 ffmpeg 提取音频)
    progressive = [f for f in fmts if is_progressive_with_audio(f)]
    if progressive:
        return best_by_abr_or_tbr(progressive)

    # 最后手段
    return "bestaudio/best"

额外过滤(实际实现)

  • has_url(f): 检查 urlmanifest_urlfragment_base_url
  • is_storyboard(f): 排除 format_id 形如 sb* 或扩展名为 mhtml、图片等的格式。

这些措施显著减少了“没有有效格式”的情况,并节省了带宽。

使用 player_client:Android、iOS 与 Web

一个有趣的 yt‑dlp 细节是参数:

"extractor_args": {"youtube": {"player_client": [player_client]}}

如果你伪装成以下客户端,YouTube 的表现会不同:

  • Android 客户端
  • iOS 客户端
  • Web 播放器

实际上,对于某些内容(尤其是 Shorts),某些客户端的效果比其他的好。端点的实现大致如下:

clients = ["android", "ios", "web"]
downloaded = False

for client in clients:
    try:
        opts = ytdlp_opts_base(client)
        info = extract_info_only(opts, source_url)
        fmt = pick_best_audio_format(info)
        download_with_format(opts, source_url, fmt)
        downloaded = True
        break
    except Exception as e:
        log_error(e, client)
        continue

# Fallback sin player_client (a veces da formatos que los otros bloquean)
if not downloaded:
    opts = ytdlp_opts_base("web")
    opts.pop("extractor_args", None)  # sin client
    # ... intentar de nuevo

ytdlp_opts_base(...) 中还会调整以下内容:

  • 为更好地模拟所选客户端,设置 User‑AgentAccept‑Language
  • 为提高鲁棒性,设置 Timeouts(超时)和 retries(重试)。

结论

使用 FastAPIyt‑dlpffmpeg 以及一些用于挑选最佳流的逻辑,就可以提供一个在桌面和移动端都能良好运行、无需本地安装的在线视频转 MP3 服务。

如果你感兴趣,访问 并使用你自己的链接进行实验。Happy hacking!

Accept-Language

  • force_ipv4
  • retries, fragment_retries, socket_timeout
  • cookiefile(如果存在,用于绕过某些限制)

使用 ffmpeg 转换为 MP3

下载文件(音频或视频 + 音频)后,我们使用 ffmpeg 进行处理:

cmd = [
    FFMPEG_BIN, "-y",
    "-i", str(downloaded_path),
    "-vn",                    # sin vídeo
    "-c:a", "libmp3lame",
    "-b:a", calidad_mp3,      # "192k", "128k" o "64k"
    "-ar", "44100",
    "-ac", "2",
    str(final_mp3),
]
safe_run_ffmpeg(cmd)

一些细节

  • safe_run_ffmpeg 是一个 wrapper,用于限制运行时间并捕获错误。
  • 我们始终使用 44100 Hz2 声道 以保证兼容性,尽管对于纯语音可以优化为单声道。
  • 我们记录最终大小(MB)和比特率,以便生成汇总统计(不包含个人数据)。

流式下载与清理

El endpoint de descarga es GET /descargar/{filename} y envía el archivo como StreamingResponse en chunks:

def iterfile(path: Path, chunk_size: int = 1024 * 1024):
    """Yield the file in chunks."""
    with path.open("rb") as f:
        while chunk := f.read(chunk_size):
            yield chunk

return StreamingResponse(
    iterfile(file_path),
    media_type="audio/mpeg",
    headers={
        "Content‑Disposition": f'attachment; filename="{filename}"',
        "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0, private",
    },
)

重要细节

  • 下载的原始文件会在 finally 块中被删除。
  • 生成的 MP3 会保留在磁盘上,但清理进程会每 24 h 自动删除一次。
  • 相同的模式同时用于 YouTube 部分以及网页的其他工具,包括通用音频转换器:

👉

安全性、限制和法律方面

作为公共服务,需要注意以下几点:

  • reCAPTCHA 在 URL → MP3 路径上,用于阻止自动化滥用。
  • 严格的 URL 验证(仅 http(s)://,规范化),以防止服务充当开放代理。
  • 最小但有用的日志request_id、IP、user‑agent、比特率、大小;不记录私人内容。
  • 隐私:临时文件和最终文件会定期删除。
  • 版权:表单和文字明确说明链接选项(YouTube 或其他)仅用于自己的内容或已获授权的内容。

如果你只想尝试技术流程,可以忽略 YouTube 部分,直接在同一工具中使用本地视频进行测试:
👉

Back to Blog

相关文章

阅读更多 »