Fork to yt-mp3-ui
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
84a8e0faa7
commit
9c6360e9c4
10 changed files with 243 additions and 288 deletions
|
|
@ -2,6 +2,7 @@ import sys
|
|||
import json
|
||||
import uuid
|
||||
import asyncio
|
||||
import logging
|
||||
import httpx
|
||||
import urllib.parse
|
||||
import mimetypes
|
||||
|
|
@ -17,6 +18,11 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|||
import yt_dlp
|
||||
|
||||
|
||||
logger = logging.getLogger("yt_dlp_web")
|
||||
|
||||
GENERIC_ERROR = "Une erreur est survenue. Veuillez réessayer."
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
|
|
@ -29,7 +35,8 @@ app.add_middleware(
|
|||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
return JSONResponse(status_code=500, content={"detail": str(exc)})
|
||||
logger.exception("Unhandled error on %s %s", request.method, request.url.path)
|
||||
return JSONResponse(status_code=500, content={"detail": GENERIC_ERROR})
|
||||
|
||||
|
||||
DOWNLOAD_DIR = Path(__file__).parent / "downloads"
|
||||
|
|
@ -38,14 +45,6 @@ DOWNLOAD_DIR.mkdir(exist_ok=True)
|
|||
FRONTEND_DIR = Path(__file__).parent.parent / "frontend"
|
||||
|
||||
|
||||
FORMAT_PRESETS = {
|
||||
"best": "bestvideo+bestaudio/bestvideo/best",
|
||||
"1080p": "bestvideo[height<=1080]+bestaudio/bestvideo[height<=1080]/best",
|
||||
"720p": "bestvideo[height<=720]+bestaudio/bestvideo[height<=720]/best",
|
||||
"480p": "bestvideo[height<=480]+bestaudio/bestvideo[height<=480]/best",
|
||||
"audio": "bestaudio/best",
|
||||
}
|
||||
|
||||
# file_id -> progress/meta
|
||||
_progress: dict[str, dict] = {}
|
||||
|
||||
|
|
@ -59,8 +58,6 @@ class InfoRequest(BaseModel):
|
|||
|
||||
class DownloadRequest(BaseModel):
|
||||
url: str
|
||||
preset: str = "best"
|
||||
format_id: str | None = None
|
||||
file_id: str | None = None
|
||||
|
||||
|
||||
|
|
@ -80,11 +77,12 @@ async def get_info(req: InfoRequest):
|
|||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = await asyncio.to_thread(ydl.extract_info, req.url, download=False)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception:
|
||||
logger.exception("Info extraction failed for url=%s", req.url)
|
||||
raise HTTPException(status_code=400, detail="Impossible d'extraire les infos de la vidéo.")
|
||||
|
||||
if not info:
|
||||
raise HTTPException(status_code=400, detail="Could not extract video info.")
|
||||
raise HTTPException(status_code=400, detail="Impossible d'extraire les infos de la vidéo.")
|
||||
|
||||
thumbnails = info.get("thumbnails") or []
|
||||
thumbnail_url = None
|
||||
|
|
@ -99,28 +97,6 @@ async def get_info(req: InfoRequest):
|
|||
|
||||
thumbnail_url = thumbnail_url or info.get("thumbnail")
|
||||
|
||||
formats = [
|
||||
{
|
||||
"format_id": f.get("format_id"),
|
||||
"ext": f.get("ext"),
|
||||
"resolution": f.get("resolution") or f.get("format_note") or "unknown",
|
||||
"filesize": f.get("filesize") or f.get("filesize_approx"),
|
||||
"vcodec": f.get("vcodec"),
|
||||
"acodec": f.get("acodec"),
|
||||
"fps": f.get("fps"),
|
||||
"height": f.get("height") or 0,
|
||||
"tbr": f.get("tbr") or 0,
|
||||
}
|
||||
for f in (info.get("formats") or [])
|
||||
if f.get("ext") != "mhtml"
|
||||
and f.get("protocol") != "mhtml"
|
||||
and (f.get("vcodec", "none") != "none" or f.get("acodec", "none") != "none")
|
||||
]
|
||||
formats.sort(key=lambda f: (f["height"], f["tbr"]), reverse=True)
|
||||
|
||||
if not formats:
|
||||
raise HTTPException(status_code=400, detail="No downloadable formats found.")
|
||||
|
||||
return {
|
||||
"title": info.get("title"),
|
||||
"thumbnail": f"/thumbnail?url={thumbnail_url}" if thumbnail_url else None,
|
||||
|
|
@ -129,7 +105,6 @@ async def get_info(req: InfoRequest):
|
|||
"view_count": info.get("view_count"),
|
||||
"like_count": info.get("like_count"),
|
||||
"upload_date": info.get("upload_date"),
|
||||
"formats": formats,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -143,10 +118,11 @@ async def proxy_thumbnail(url: str):
|
|||
media_type=r.headers.get("content-type", "image/jpeg"),
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=502, detail="Could not fetch thumbnail")
|
||||
logger.exception("Thumbnail fetch failed for url=%s", url)
|
||||
raise HTTPException(status_code=502, detail="Impossible de récupérer la miniature")
|
||||
|
||||
|
||||
async def _run_download(file_id: str, url: str, fmt: str, output_template: str):
|
||||
async def _run_download(file_id: str, url: str, output_template: str):
|
||||
cancelled = asyncio.Event()
|
||||
|
||||
def progress_hook(d):
|
||||
|
|
@ -185,14 +161,18 @@ async def _run_download(file_id: str, url: str, fmt: str, output_template: str):
|
|||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"format": fmt,
|
||||
"format": "bestaudio/best",
|
||||
"outtmpl": output_template,
|
||||
"merge_output_format": "mp4",
|
||||
"restrictfilenames": True,
|
||||
"writethumbnail": True,
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegVideoConvertor",
|
||||
"preferedformat": "mp4",
|
||||
}
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "320",
|
||||
},
|
||||
{"key": "FFmpegMetadata"},
|
||||
{"key": "EmbedThumbnail"},
|
||||
],
|
||||
"progress_hooks": [progress_hook],
|
||||
}
|
||||
|
|
@ -208,10 +188,11 @@ async def _run_download(file_id: str, url: str, fmt: str, output_template: str):
|
|||
_progress[file_id]["status"] = "cancelled"
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Download failed for file_id=%s url=%s", file_id, url)
|
||||
if file_id in _progress:
|
||||
_progress[file_id]["status"] = "error"
|
||||
_progress[file_id]["error"] = str(e)
|
||||
_progress[file_id]["error"] = GENERIC_ERROR
|
||||
raise
|
||||
|
||||
|
||||
|
|
@ -219,10 +200,10 @@ def _finalize_download(file_id: str, info: dict):
|
|||
matches = list(DOWNLOAD_DIR.glob(f"{file_id}.*"))
|
||||
if not matches:
|
||||
_progress[file_id]["status"] = "error"
|
||||
_progress[file_id]["error"] = "Download failed: file not found"
|
||||
_progress[file_id]["error"] = "Échec du téléchargement : fichier introuvable"
|
||||
return
|
||||
|
||||
filepath = matches[0]
|
||||
filepath = next((m for m in matches if m.suffix == ".mp3"), matches[0])
|
||||
title = info.get("title") or file_id
|
||||
|
||||
filename_ascii = "".join(
|
||||
|
|
@ -249,7 +230,6 @@ def _finalize_download(file_id: str, info: dict):
|
|||
async def download_video(req: DownloadRequest):
|
||||
file_id = req.file_id or str(uuid.uuid4())
|
||||
output_template = str(DOWNLOAD_DIR / f"{file_id}.%(ext)s")
|
||||
fmt = req.format_id if req.format_id else FORMAT_PRESETS.get(req.preset, FORMAT_PRESETS["best"])
|
||||
|
||||
_progress[file_id] = {
|
||||
"status": "starting",
|
||||
|
|
@ -257,11 +237,10 @@ async def download_video(req: DownloadRequest):
|
|||
"eta": None,
|
||||
"speed": None,
|
||||
"url": req.url,
|
||||
"fmt": fmt,
|
||||
"output_template": output_template,
|
||||
}
|
||||
|
||||
task = asyncio.create_task(_run_download(file_id, req.url, fmt, output_template))
|
||||
task = asyncio.create_task(_run_download(file_id, req.url, output_template))
|
||||
_tasks[file_id] = task
|
||||
|
||||
def _done_callback(t: asyncio.Task):
|
||||
|
|
@ -271,10 +250,10 @@ async def download_video(req: DownloadRequest):
|
|||
except asyncio.CancelledError:
|
||||
if file_id in _progress:
|
||||
_progress[file_id]["status"] = "cancelled"
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
if file_id in _progress:
|
||||
_progress[file_id]["status"] = "error"
|
||||
_progress[file_id]["error"] = str(e)
|
||||
_progress[file_id]["error"] = GENERIC_ERROR
|
||||
finally:
|
||||
_tasks.pop(file_id, None)
|
||||
|
||||
|
|
@ -303,11 +282,11 @@ async def cancel_download(file_id: str):
|
|||
async def serve_file(file_id: str):
|
||||
meta = _progress.get(file_id)
|
||||
if not meta or meta.get("status") != "ready":
|
||||
raise HTTPException(status_code=404, detail="File not ready")
|
||||
raise HTTPException(status_code=404, detail="Fichier pas encore prêt")
|
||||
|
||||
filepath = Path(meta["filepath"])
|
||||
if not filepath.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
raise HTTPException(status_code=404, detail="Fichier introuvable")
|
||||
|
||||
filename_ascii = meta["filename_ascii"]
|
||||
filename_encoded = meta["filename_encoded"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue