fix(downloads): R5 S1 — downloads robustness
- pick the produced file safely: Path("") is Path("."), which passes .exists(),
so a missing yt-dlp filepath skipped the staging fallback and crashed in
with_name ("PosixPath('.') has an empty name") on non-YouTube sources
- unwrap a playlist/multi_video extraction to the entry that carries the media
- run_ffmpeg: drain stderr while it runs (a full 64KB pipe blocked ffmpeg
forever and hung the worker thread), 1s cancel polling, stall watchdog and a
bounded wait on exit
- escape apostrophes/backslashes in the ffmpeg concat list (pre-0.51.0 names)
- sweep orphaned HLS segment dirs at startup (nothing else ever reaped them)
This commit is contained in:
+41
-11
@@ -341,13 +341,51 @@ def _staging_dir(asset_id: int) -> Path:
|
||||
return Path(settings.download_root) / ".staging" / f"asset-{asset_id}"
|
||||
|
||||
|
||||
_THUMB_SUFFIXES = (".jpg", ".jpeg", ".png", ".webp")
|
||||
|
||||
|
||||
def _find_thumbnail(staging: Path, media: Path) -> Path | None:
|
||||
for p in staging.iterdir():
|
||||
if p != media and p.suffix.lower() in (".jpg", ".jpeg", ".png", ".webp"):
|
||||
if p != media and p.suffix.lower() in _THUMB_SUFFIXES:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _produced_file(info: dict, staging: Path) -> Path:
|
||||
"""The media file yt-dlp actually wrote.
|
||||
|
||||
`requested_downloads[0].filepath` is the authoritative answer on YouTube, but an arbitrary-URL
|
||||
extractor may not fill it in at all — and `Path("")` is `Path(".")`, which *passes* `.exists()`,
|
||||
so a missing value used to sail past the fallback and blow up two steps later (`with_name` on an
|
||||
empty name: the BBC-news failure). Anything that isn't a real file counts as absent; then fall
|
||||
back to the largest non-thumbnail file left in staging."""
|
||||
req = (info.get("requested_downloads") or [{}])[0]
|
||||
fp = req.get("filepath") or req.get("_filename") or info.get("filepath") or info.get("_filename")
|
||||
if fp:
|
||||
produced = Path(str(fp))
|
||||
if produced.is_file():
|
||||
return produced
|
||||
cands = [
|
||||
p
|
||||
for p in staging.iterdir()
|
||||
if p.is_file() and p.suffix.lower() not in _THUMB_SUFFIXES and not p.name.endswith(".part")
|
||||
]
|
||||
if not cands:
|
||||
raise RuntimeError("yt-dlp produced no output file")
|
||||
return max(cands, key=lambda p: p.stat().st_size)
|
||||
|
||||
|
||||
def _media_info(info: dict) -> dict:
|
||||
"""Unwrap a playlist/multi_video extraction to the entry that carries the download.
|
||||
|
||||
A single non-YouTube page (a news article's video, a multi-rendition embed) can extract as a
|
||||
wrapper whose own dict has no `requested_downloads` — the media lives on the first entry."""
|
||||
entries = [e for e in (info.get("entries") or []) if e]
|
||||
if entries and not info.get("requested_downloads"):
|
||||
return {**info, **entries[0]}
|
||||
return info
|
||||
|
||||
|
||||
# Errors worth retrying with a different player-client set (YouTube per-client flakiness).
|
||||
_RETRIABLE_MARKERS = (
|
||||
"not a bot",
|
||||
@@ -447,17 +485,9 @@ def _download(job_id: int, asset_id: int, source_kind: str, source_ref: str, spe
|
||||
|
||||
_set_job(job_id, phase="downloading", progress=0)
|
||||
url = service.source_url(source_kind, source_ref)
|
||||
info = _extract_with_fallback(job_id, asset_id, url, spec, staging)
|
||||
|
||||
req = (info.get("requested_downloads") or [{}])[0]
|
||||
produced = Path(req.get("filepath") or "")
|
||||
if not produced.exists():
|
||||
# Fallback: the single media file left in staging (ignore the thumbnail).
|
||||
cands = [p for p in staging.iterdir() if p.suffix.lower() not in (".jpg", ".jpeg", ".png", ".webp")]
|
||||
if not cands:
|
||||
raise RuntimeError("yt-dlp produced no output file")
|
||||
produced = cands[0]
|
||||
info = _media_info(_extract_with_fallback(job_id, asset_id, url, spec, staging))
|
||||
|
||||
produced = _produced_file(info, staging)
|
||||
produced = _ensure_browser_playable(job_id, produced, spec, info)
|
||||
ext = produced.suffix.lstrip(".").lower()
|
||||
meta = storage.MediaMeta(
|
||||
|
||||
Reference in New Issue
Block a user