youtube downloads: show 'Importing' during the ffmpeg merge, not a stuck 100%

the '100% but still Downloading for a while' phase was yt-dlp merging audio+video
(ffmpeg) INSIDE the download call — status stayed 'downloading' the whole time. now a
postprocessor_hook flips the row to 'importing' the moment post-processing starts, so
the card shows 'Importing' through the merge AND the library move (which already set
it), then completes. matches the movies/TV treatment. hook wiring tested.
This commit is contained in:
BoulderBadgeDad 2026-06-26 15:37:08 -07:00
parent f6360e90da
commit 75f23641b3
2 changed files with 29 additions and 6 deletions

View file

@ -84,7 +84,8 @@ def plan_destination(dl: Dict[str, Any], settings: Dict[str, Any], container: st
def ydl_download_opts(profile: Any, dest_dir: str, dest_stem: str,
*, progress_hook: Optional[Callable] = None, cookie_opts: Optional[dict] = None) -> dict:
*, progress_hook: Optional[Callable] = None, postprocess_hook: Optional[Callable] = None,
cookie_opts: Optional[dict] = None) -> dict:
"""The yt-dlp options dict for one download: format selection from the quality profile,
a fixed output path (dir + stem + yt-dlp's own ext), polite defaults. Pure."""
sel = format_selection(profile)
@ -109,6 +110,8 @@ def ydl_download_opts(profile: Any, dest_dir: str, dest_stem: str,
opts.update(cookie_opts)
if progress_hook:
opts["progress_hooks"] = [progress_hook]
if postprocess_hook:
opts["postprocessor_hooks"] = [postprocess_hook] # fires while ffmpeg merges/converts
return opts
@ -122,7 +125,7 @@ def _stem_and_container(dest: Dict[str, str], container: str) -> tuple:
def download_one(video_id: Any, dest_dir: str, dest_stem: str, profile: Any, container: str,
*, ydl_factory=None, progress_hook=None, cookie_opts=None) -> Dict[str, Any]:
*, ydl_factory=None, progress_hook=None, postprocess_hook=None, cookie_opts=None) -> Dict[str, Any]:
"""Run yt-dlp for ONE video into ``dest_dir/dest_stem.ext``. Returns
``{ok, dest_path|None, error|None}``. The yt-dlp class is injectable for tests."""
vid = str(video_id or "").strip()
@ -131,8 +134,8 @@ def download_one(video_id: Any, dest_dir: str, dest_stem: str, profile: Any, con
factory = ydl_factory or (yt_dlp.YoutubeDL if yt_dlp else None)
if factory is None:
return {"ok": False, "dest_path": None, "error": "yt-dlp unavailable"}
opts = ydl_download_opts(profile, dest_dir, dest_stem,
progress_hook=progress_hook, cookie_opts=cookie_opts)
opts = ydl_download_opts(profile, dest_dir, dest_stem, progress_hook=progress_hook,
postprocess_hook=postprocess_hook, cookie_opts=cookie_opts)
url = vid if vid.startswith("http") else "https://www.youtube.com/watch?v=" + vid
try:
with factory(opts) as ydl:
@ -241,6 +244,7 @@ def process_youtube_download(
move: Callable[[str, str], Any] = _default_move,
sidecars: Callable[[str, str, Dict[str, Any], Dict[str, Any]], Any] = _default_sidecars,
progress_hook: Optional[Callable] = None,
postprocess_hook: Optional[Callable] = None,
cookie_opts: Optional[dict] = None,
now: Optional[Callable[[], str]] = None,
) -> Dict[str, Any]:
@ -268,7 +272,7 @@ def process_youtube_download(
update_row(dl.get("id"), status="downloading", progress=0, filename=dest.get("filename"))
res = download(dl.get("media_id"), dl_dir, stem, profile, cont,
progress_hook=progress_hook, cookie_opts=cookie_opts)
progress_hook=progress_hook, postprocess_hook=postprocess_hook, cookie_opts=cookie_opts)
if not res.get("ok"):
err = res.get("error") or "Download failed"
@ -399,6 +403,16 @@ def run_youtube_download(dl_id: Any, db_provider: Callable) -> None:
except Exception: # noqa: BLE001, S110 - a progress glitch must not abort the download
pass
def _postprocess(d):
# yt-dlp finished the bytes and is now merging audio+video / converting (ffmpeg) — flip
# to 'importing' so the card stops sitting on a stuck-looking 100% 'Downloading' until
# it (and the library move that follows) are done. Best-effort.
try:
if d.get("status") in ("started", "processing"):
db.update_video_download(dl_id, status="importing", progress=100)
except Exception: # noqa: BLE001, S110 - a hook glitch must not abort the download
pass
def _archive(row, upd):
try:
db.record_download_history({**row, **upd})
@ -426,7 +440,7 @@ def run_youtube_download(dl_id: Any, db_provider: Callable) -> None:
update_row=db.update_video_download, archive=_archive,
clear_wishlist=lambda vid: db.remove_youtube_from_wishlist("video", vid),
stage_dir=stage_dir,
progress_hook=_progress, cookie_opts=cookie_opts, now=_now)
progress_hook=_progress, postprocess_hook=_postprocess, cookie_opts=cookie_opts, now=_now)
finally:
_active_worker_ids.discard(dl_id) # worker done — no longer protects this row
start_next_queued(db_provider) # one out, one in — drain the queue

View file

@ -49,6 +49,15 @@ def test_ydl_opts_carry_format_selection_and_fixed_output():
assert opts["noplaylist"] is True
def test_ydl_opts_wire_the_postprocess_hook_for_the_merge_phase():
# the hook flips the row to 'importing' while ffmpeg merges — so it doesn't sit on 100%
def hook(_d):
return None
opts = ytd.ydl_download_opts(default_profile(), "/d", "stem", postprocess_hook=hook)
assert opts["postprocessor_hooks"] == [hook]
assert "postprocessor_hooks" not in ytd.ydl_download_opts(default_profile(), "/d", "stem")
# ── download_one with an injected yt-dlp ───────────────────────────────────────
class _FakeYDL:
def __init__(self, opts):