Volta versao do download-video

This commit is contained in:
LeoMortari
2025-09-29 00:57:16 -03:00
parent f2b2f551f1
commit ebba8ef8bd
2 changed files with 49 additions and 83 deletions

116
main.py
View File

@@ -1,6 +1,5 @@
import os import os
import uuid import uuid
import re
from typing import Optional from typing import Optional
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query
@@ -8,20 +7,13 @@ from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api.formatters import SRTFormatter from youtube_transcript_api.formatters import SRTFormatter
from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from unidecode import unidecode from utils import extract_video_id, sanitize_title
app = FastAPI( app = FastAPI(
title="YouTube Transcript, Download and Metadata API", title="YouTube Transcript, Download and Metadata API",
version="1.0.0" version="1.0.0"
) )
def extract_video_id(url: str) -> str:
import re
match = re.search(r"(?:v=|youtu\.be/)([A-Za-z0-9_-]{11})", url)
if not match:
raise ValueError("URL inválida do YouTube")
return match.group(1)
@app.get("/get-transcript") @app.get("/get-transcript")
def get_transcript( def get_transcript(
url: Optional[str] = Query(None, description="URL completa do vídeo"), url: Optional[str] = Query(None, description="URL completa do vídeo"),
@@ -146,7 +138,6 @@ def download_video(
): ):
if not url and not videoId: if not url and not videoId:
raise HTTPException(status_code=400, detail="Informe 'url' ou 'videoId'") raise HTTPException(status_code=400, detail="Informe 'url' ou 'videoId'")
if url: if url:
target = url target = url
try: try:
@@ -157,106 +148,65 @@ def download_video(
target = f"https://www.youtube.com/watch?v={videoId}" target = f"https://www.youtube.com/watch?v={videoId}"
video_id = videoId video_id = videoId
quality_targets = {"low": 480, "medium": 720, "high": 1080} quality_map = {
"low": "bestvideo[height<=480]+bestaudio/best[height<=480]",
"medium": "bestvideo[height<=720]+bestaudio/best[height<=720]",
"high": "bestvideo+bestaudio/best"
}
qualidade = qualidade.lower() qualidade = qualidade.lower()
if qualidade not in quality_map:
if qualidade not in quality_targets:
raise HTTPException(status_code=400, detail="Qualidade deve ser: low, medium ou high") raise HTTPException(status_code=400, detail="Qualidade deve ser: low, medium ou high")
target_height = quality_targets[qualidade]
videos_dir = "/app/videos" videos_dir = "/app/videos"
os.makedirs(videos_dir, exist_ok=True) os.makedirs(videos_dir, exist_ok=True)
unique_id = str(uuid.uuid4()) unique_id = str(uuid.uuid4())
outtmpl = os.path.join(videos_dir, f"{unique_id}.%(ext)s") output_template = os.path.join(videos_dir, f"{unique_id}.%(ext)s")
fmt_expr = (
# tenta vídeo <= alvo + melhor áudio
f"bv*[height<={target_height}]+ba/"
# cai pra qualquer melhor vídeo <= alvo (progressivo se houver)
f"b[height<={target_height}]/"
# último recurso: qualquer best
f"b"
)
ydl_opts = { ydl_opts = {
"outtmpl": outtmpl, "format": quality_map[qualidade],
"outtmpl": output_template,
"quiet": True, "quiet": True,
"no_warnings": True,
"ignoreerrors": False,
"noplaylist": True, "noplaylist": True,
"merge_output_format": "mp4", "merge_output_format": "mp4",
"force_ipv4": True,
"geo_bypass": True,
"extractor_args": {"youtube": {"player_client": ["android"], "player_skip": ["webpage"]}},
"http_headers": {
"Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"User-Agent": "com.google.android.youtube/19.17.36 (Linux; U; Android 13) gzip",
},
"hls_prefer_native": True,
"concurrent_fragment_downloads": 1,
} }
try: try:
with YoutubeDL(ydl_opts) as ydl: with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(target, download=False) base = ydl.extract_info(target, download=False)
if not info: title = base.get("title", unique_id)
raise HTTPException(status_code=404, detail="Não foi possível obter informações do vídeo.") clean_title = sanitize_title(title)
if info.get("_type") == "playlist":
entries = info.get("entries") or []
if not entries:
raise HTTPException(status_code=404, detail="Nenhum vídeo encontrado na playlist.")
info = entries[0]
title = info.get("title") or unique_id
clean_title = unidecode(title)
clean_title = re.sub(r"[^\w\s-]", "", clean_title).strip()
clean_title = re.sub(r"\s+", "_", clean_title)
filename = f"{clean_title}_{qualidade}.mp4" filename = f"{clean_title}_{qualidade}.mp4"
final_path = os.path.join(videos_dir, filename) final_path = os.path.join(videos_dir, filename)
print('Info ok')
if os.path.exists(final_path): if os.path.exists(final_path):
return {"videoId": video_id, "filename": filename} return {
"videoId": video_id,
# Baixa de fato "filename": filename
}
print('Lets download')
result = ydl.extract_info(target, download=True) result = ydl.extract_info(target, download=True)
# Descobre o arquivo gerado if "requested_downloads" in result and len(result["requested_downloads"]) > 0:
real_file_path = None real_file_path = result["requested_downloads"][0]["filepath"]
if isinstance(result, dict): elif "filepath" in result:
# yt-dlp costuma preencher requested_downloads real_file_path = result["filepath"]
reqs = result.get("requested_downloads") or [] else:
if reqs: real_file_path = output_template.replace("%(ext)s", "mp4")
real_file_path = reqs[0].get("filepath")
if not real_file_path:
real_file_path = result.get("filepath")
if not real_file_path:
# fallback bruto para o template com mp4
real_file_path = outtmpl.replace("%(ext)s", "mp4")
if not os.path.exists(real_file_path):
# Ajuda a diagnosticar quando o formato pedido não existe
# (por segurança não expomos toda a lista ao cliente)
raise HTTPException(
status_code=500,
detail="Falha ao localizar o arquivo baixado. O formato selecionado pode não estar disponível para este vídeo."
)
os.rename(real_file_path, final_path) os.rename(real_file_path, final_path)
except HTTPException:
raise
except Exception as e: except Exception as e:
# Erros comuns: falta do ffmpeg no container raise HTTPException(status_code=500, detail=f"Erro ao baixar vídeo: {e}")
msg = str(e)
if "ffmpeg" in msg.lower():
msg += " (verifique se o ffmpeg está instalado no container)"
raise HTTPException(status_code=500, detail=f"Erro ao baixar vídeo: {msg}")
return {"videoId": video_id, "filename": filename} return {
"videoId": video_id,
"filename": filename
}
@app.get("/search") @app.get("/search")
def search_youtube_yt_dlp( def search_youtube_yt_dlp(

16
utils.py Normal file
View File

@@ -0,0 +1,16 @@
import re
from unidecode import unidecode
def extract_video_id(url: str) -> str:
match = re.search(r"(?:v=|youtu\.be/)([A-Za-z0-9_-]{11})", url)
if not match:
raise ValueError("URL inválida do YouTube")
return match.group(1)
def sanitize_title(s: str) -> str:
s = unidecode(s or "video")
s = re.sub(r"[^\w\s-]", "", s).strip()
return re.sub(r"_+", "_", re.sub(r"\s+", "_", s)) or "video"