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:
parent
f6360e90da
commit
75f23641b3
2 changed files with 29 additions and 6 deletions
|
|
@ -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,
|
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,
|
"""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."""
|
a fixed output path (dir + stem + yt-dlp's own ext), polite defaults. Pure."""
|
||||||
sel = format_selection(profile)
|
sel = format_selection(profile)
|
||||||
|
|
@ -109,6 +110,8 @@ def ydl_download_opts(profile: Any, dest_dir: str, dest_stem: str,
|
||||||
opts.update(cookie_opts)
|
opts.update(cookie_opts)
|
||||||
if progress_hook:
|
if progress_hook:
|
||||||
opts["progress_hooks"] = [progress_hook]
|
opts["progress_hooks"] = [progress_hook]
|
||||||
|
if postprocess_hook:
|
||||||
|
opts["postprocessor_hooks"] = [postprocess_hook] # fires while ffmpeg merges/converts
|
||||||
return opts
|
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,
|
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
|
"""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."""
|
``{ok, dest_path|None, error|None}``. The yt-dlp class is injectable for tests."""
|
||||||
vid = str(video_id or "").strip()
|
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)
|
factory = ydl_factory or (yt_dlp.YoutubeDL if yt_dlp else None)
|
||||||
if factory is None:
|
if factory is None:
|
||||||
return {"ok": False, "dest_path": None, "error": "yt-dlp unavailable"}
|
return {"ok": False, "dest_path": None, "error": "yt-dlp unavailable"}
|
||||||
opts = ydl_download_opts(profile, dest_dir, dest_stem,
|
opts = ydl_download_opts(profile, dest_dir, dest_stem, progress_hook=progress_hook,
|
||||||
progress_hook=progress_hook, cookie_opts=cookie_opts)
|
postprocess_hook=postprocess_hook, cookie_opts=cookie_opts)
|
||||||
url = vid if vid.startswith("http") else "https://www.youtube.com/watch?v=" + vid
|
url = vid if vid.startswith("http") else "https://www.youtube.com/watch?v=" + vid
|
||||||
try:
|
try:
|
||||||
with factory(opts) as ydl:
|
with factory(opts) as ydl:
|
||||||
|
|
@ -241,6 +244,7 @@ def process_youtube_download(
|
||||||
move: Callable[[str, str], Any] = _default_move,
|
move: Callable[[str, str], Any] = _default_move,
|
||||||
sidecars: Callable[[str, str, Dict[str, Any], Dict[str, Any]], Any] = _default_sidecars,
|
sidecars: Callable[[str, str, Dict[str, Any], Dict[str, Any]], Any] = _default_sidecars,
|
||||||
progress_hook: Optional[Callable] = None,
|
progress_hook: Optional[Callable] = None,
|
||||||
|
postprocess_hook: Optional[Callable] = None,
|
||||||
cookie_opts: Optional[dict] = None,
|
cookie_opts: Optional[dict] = None,
|
||||||
now: Optional[Callable[[], str]] = None,
|
now: Optional[Callable[[], str]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
|
@ -268,7 +272,7 @@ def process_youtube_download(
|
||||||
update_row(dl.get("id"), status="downloading", progress=0, filename=dest.get("filename"))
|
update_row(dl.get("id"), status="downloading", progress=0, filename=dest.get("filename"))
|
||||||
|
|
||||||
res = download(dl.get("media_id"), dl_dir, stem, profile, cont,
|
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"):
|
if not res.get("ok"):
|
||||||
err = res.get("error") or "Download failed"
|
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
|
except Exception: # noqa: BLE001, S110 - a progress glitch must not abort the download
|
||||||
pass
|
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):
|
def _archive(row, upd):
|
||||||
try:
|
try:
|
||||||
db.record_download_history({**row, **upd})
|
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,
|
update_row=db.update_video_download, archive=_archive,
|
||||||
clear_wishlist=lambda vid: db.remove_youtube_from_wishlist("video", vid),
|
clear_wishlist=lambda vid: db.remove_youtube_from_wishlist("video", vid),
|
||||||
stage_dir=stage_dir,
|
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:
|
finally:
|
||||||
_active_worker_ids.discard(dl_id) # worker done — no longer protects this row
|
_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
|
start_next_queued(db_provider) # one out, one in — drain the queue
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,15 @@ def test_ydl_opts_carry_format_selection_and_fixed_output():
|
||||||
assert opts["noplaylist"] is True
|
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 ───────────────────────────────────────
|
# ── download_one with an injected yt-dlp ───────────────────────────────────────
|
||||||
class _FakeYDL:
|
class _FakeYDL:
|
||||||
def __init__(self, opts):
|
def __init__(self, opts):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue