From 75f23641b3eab40c869156be406286cddefdd27c Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 26 Jun 2026 15:37:08 -0700 Subject: [PATCH] youtube downloads: show 'Importing' during the ffmpeg merge, not a stuck 100% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/video/youtube_download.py | 26 ++++++++++++++++++++------ tests/test_youtube_download.py | 9 +++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/core/video/youtube_download.py b/core/video/youtube_download.py index d68efe9e..aa4def1c 100644 --- a/core/video/youtube_download.py +++ b/core/video/youtube_download.py @@ -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 diff --git a/tests/test_youtube_download.py b/tests/test_youtube_download.py index d4d4399d..b6a9689d 100644 --- a/tests/test_youtube_download.py +++ b/tests/test_youtube_download.py @@ -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):