FastAPI, yt-dlp 및 ffmpeg를 사용한 오디오 추출기 (YouTube MP3) 만들기
Source: Dev.to

소개
비디오에서 오디오를 추출해 팟캐스트처럼 듣거나 수업을 복습하는 것은 매우 일반적인 사용 사례입니다. 하지만 다음과 같이 하고 싶을 때 문제가 발생합니다:
- 컴퓨터에 별다른 프로그램을 설치하지 않고
- 모바일에서
이 포스트에서는 우리가 사용하고 있는 오디오 추출기가 내부적으로 어떻게 구성되어 있는지 이야기합니다
👉
그리고 도구
👉
전체 코드를 설명하지는 않지만 핵심 아이디어는 FastAPI, yt‑dlp, ffmpeg, reCAPTCHA와 YouTube(Shorts 포함)에서 비교적 잘 동작하도록 하는 몇 가지 트릭입니다.
일반 흐름: URL에서 다운로드 가능한 MP3로
Endpoint “URL → MP3” 흐름은 다음과 같습니다:
- 사용자가 POST 요청을 보냅니다:
source_url: YouTube(또는 기타 호환 플랫폼) 링크calidad_mp3:192k,128k또는64krecaptcha_token: 남용/봇 방지를 위해
- reCAPTCHA와 URL을 검증합니다.
- yt‑dlp를 사용해 사용 가능한 포맷을 검사하고 가장 좋은 오디오 스트림을 선택합니다.
- 오디오만 다운로드합니다(다른 방법이 없을 경우 비디오 + 오디오 프로그레시브 스트림을 다운로드).
- 해당 파일을 ffmpeg에 전달해 요청된 비트레이트로 MP3로 변환합니다.
- 다음과 같은 JSON을 반환합니다:
download_url:/descargar/{filename}형태의 다운로드 엔드포인트용
- 임시 파일을 정리합니다.
사용자 입장에서는 이 모든 것이 매우 간단한 폼으로 제공됩니다.
👉
HTTP 레이어: FastAPI + reCAPTCHA
웹 부분은 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에서 불필요한 파라미터를 제거합니다
- request id (rid): 각 요청은 로그에 UUID를 포함하며, 특정 비디오와 관련된 문제를 디버깅하는 데 큰 도움이 됩니다.
Source: …
yt‑dlp: 오디오 스트림을 올바르게 선택하기
yt‑dlp는 다양한 포맷 정보를 많이 반환합니다. 단순히 "format": "bestaudio/best"를 사용하는 대신 다음을 고려했습니다:
- 직접 URL이 없는 포맷(스토리보드, 썸네일 등)을 피하기
- 오디오 전용(예:
m4a/140)이 있으면 우선 선택하기 - 빈값에 가까운 포맷은 무시하기
- 드물게 발생하는 경우를 대비해 fallback 로 진행형(비디오 + 오디오) 스트림을 사용하기
포맷 선택 함수의 아이디어는 다음과 같은 (단순화된) 의사코드와 같습니다:
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)]
# 매우 일반적인 YouTube의 m4a / id 140이 있으면 우선 선택
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)
# Fallback: 진행형 비디오+오디오 스트림 (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):url,manifest_url또는fragment_base_url을 확인합니다.is_storyboard(f):format_id가sb*형태이거나 확장자가mhtml, 이미지 등인 포맷을 제외합니다.
이러한 방법을 통해 “유효한 포맷이 없음” 상황을 크게 줄이고, 대역폭 사용을 절감할 수 있습니다.
player_client 사용하기: Android, iOS 및 웹
yt‑dlp의 흥미로운 점은 다음 인수입니다:
"extractor_args": {"youtube": {"player_client": [player_client]}}
YouTube는 당신이 다음 중 하나인 척 할 때 동일하게 동작하지 않습니다:
- Android 클라이언트
- iOS 클라이언트
- 웹 플레이어
실제로, 특정 콘텐츠(특히 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‑Agent 및 Accept‑Language
- 더 탄력적으로 만들기 위한 Timeouts 및 retries
결론
FastAPI, yt‑dlp, ffmpeg와 최적 스트림을 선택하는 약간의 로직을 사용하면, 데스크톱과 모바일 모두에서 잘 작동하고 로컬 설치가 필요 없는 비디오를 MP3로 변환하는 서비스를 제공할 수 있습니다.
시도해보고 싶다면, 방문하고 직접 링크를 실험해 보세요. Happy hacking!
Accept-Language
force_ipv4retries,fragment_retries,socket_timeoutcookiefile(존재하는 경우, 일부 제한을 우회하기 위해)
Source:
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 Hz와 2채널을 사용하지만, 순수 음성의 경우 모노로 최적화할 수 있습니다.
- 최종 파일 크기를 MB 단위와 비트레이트로 저장하여 (개인 정보 없이) 집계 통계를 생성합니다.
스트리밍 다운로드 및 정리
다운로드 엔드포인트는 GET /descargar/{filename}이며 파일을 StreamingResponse 로 청크 단위로 전송합니다:
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, bitrate, 크기; 개인 콘텐츠는 포함되지 않습니다. - 프라이버시: 임시 및 최종 파일을 주기적으로 삭제합니다.
- 저작권: 양식과 텍스트에서 링크 옵션(YouTube 등)은 본인 소유이거나 허가받은 콘텐츠에만 사용하도록 명시합니다.
기술 흐름을 실험하고 싶다면 YouTube 부분을 무시하고 동일한 도구에서 로컬 비디오를 사용해 볼 수 있습니다:
👉
