mirror of
https://github.com/alexta69/metube.git
synced 2026-09-21 13:35:01 +00:00
fix: show the post-download processing phase in the UI (closes #424)
yt-dlp reports a download 'finished' as soon as the media bytes have
landed, but the ffmpeg work that follows -- merging, re-encoding, chapter
splitting, sponsor removal -- routinely takes longer than the download
itself. The postprocessor hook only ever looked at MoveFiles and
SplitChapters finishing, so that whole phase said nothing: the row sat in
the Downloading table on a full, frozen progress bar, and the item counted
as neither active nor queued in the header stats.
Report a 'postprocessing' status when a postprocessor starts, and reuse
the indeterminate bar the UI already runs for 'preparing', labelled so a
long re-encode is distinguishable from a stall.
Measured on a real download re-encoded with the reporter's FFmpegCopyStream
config, the UI now receives:
[ 0.27s] downloading moving bar
[ 0.28s] finished <- yt-dlp, bytes are down
[ 0.28s] postprocessing animated "Post-processing"
[13.18s] finished done
i.e. 12.9s that used to render as a static 100% bar.
The hook is latched off once MoveFiles reports finished, since that branch
announces the finished file: a 'postprocessing' arriving afterwards would
flip the row back out of its completed state for no reason. It cannot fail
the download -- _download puts an unconditional 'finished' once download()
returns, so the terminal status is settled either way -- but the flicker
and the extra broadcast are both pointless. yt-dlp does run an 'after_move'
stage after MoveFiles, though every postprocessor MeTube configures is
'after_filter' or 'post_process', both of which precede it.
update_status drops a repeated 'postprocessing': yt-dlp's metaclass wraps
run() once per class in a postprocessor's MRO, so one whose subclass
overrides run reports started twice -- FFmpegCopyStream did exactly that in
the live run, three queued statuses collapsing to a single broadcast.
Extracted to _make_postprocessor_hook, mirroring _make_progress_hook, so
the ordering above is testable; every new regression test was confirmed to
fail with the fix backed out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+65
-29
@@ -771,6 +771,59 @@ class Download:
|
||||
|
||||
return put_status
|
||||
|
||||
def _make_postprocessor_hook(self):
|
||||
# yt-dlp reports the download 'finished' as soon as the media bytes
|
||||
# have landed, but the ffmpeg work that follows -- merging,
|
||||
# re-encoding, chapter splitting, sponsor removal -- routinely takes
|
||||
# longer than the download itself and said nothing until now, leaving
|
||||
# the row on a full, frozen progress bar for minutes. Report the phase
|
||||
# so the UI can run its indeterminate bar instead.
|
||||
#
|
||||
# Latched off once MoveFiles reports finished: that branch emits the
|
||||
# terminal 'finished', and _post_download_cleanup turns any other
|
||||
# final status into an error, so nothing may overwrite it. yt-dlp does
|
||||
# run an 'after_move' stage after MoveFiles, but every postprocessor
|
||||
# MeTube configures is 'after_filter' or 'post_process', both of which
|
||||
# precede it. Each postprocessor emits exactly one started/finished
|
||||
# pair, so this needs no throttling.
|
||||
postprocessing_done = False
|
||||
|
||||
def put_status_postprocessor(d):
|
||||
nonlocal postprocessing_done
|
||||
if d['postprocessor'] == 'MoveFiles' and d['status'] == 'finished':
|
||||
postprocessing_done = True
|
||||
filepath = d['info_dict']['filepath']
|
||||
if '__finaldir' in d['info_dict']:
|
||||
finaldir = d['info_dict']['__finaldir']
|
||||
filename = os.path.join(finaldir, os.path.basename(filepath))
|
||||
else:
|
||||
filename = filepath
|
||||
self.status_queue.put({'status': 'finished', 'filename': filename})
|
||||
# For captions-only downloads, yt-dlp may still report a media-like
|
||||
# filepath in MoveFiles. Capture subtitle outputs explicitly so the
|
||||
# UI can link to real caption files.
|
||||
if getattr(self.info, 'download_type', '') == 'captions':
|
||||
requested_subtitles = d.get('info_dict', {}).get('requested_subtitles', {}) or {}
|
||||
for subtitle in requested_subtitles.values():
|
||||
if isinstance(subtitle, dict) and subtitle.get('filepath'):
|
||||
self.status_queue.put({'subtitle_file': subtitle['filepath']})
|
||||
|
||||
# Capture all chapter files when SplitChapters finishes
|
||||
elif d.get('postprocessor') == 'SplitChapters' and d.get('status') == 'finished':
|
||||
chapters = d.get('info_dict', {}).get('chapters', [])
|
||||
if chapters:
|
||||
for chapter in chapters:
|
||||
if isinstance(chapter, dict) and 'filepath' in chapter:
|
||||
log.info(f"Captured chapter file: {chapter['filepath']}")
|
||||
self.status_queue.put({'chapter_file': chapter['filepath']})
|
||||
else:
|
||||
log.warning("SplitChapters finished but no chapter files found in info_dict")
|
||||
|
||||
elif d.get('status') == 'started' and not postprocessing_done:
|
||||
self.status_queue.put({'status': 'postprocessing'})
|
||||
|
||||
return put_status_postprocessor
|
||||
|
||||
def _make_youtube_dl(self, params):
|
||||
ydl = _ConfinedYoutubeDL(
|
||||
params=params,
|
||||
@@ -815,35 +868,7 @@ class Download:
|
||||
try:
|
||||
debug_logging = logging.getLogger().isEnabledFor(logging.DEBUG)
|
||||
put_status = self._make_progress_hook()
|
||||
|
||||
def put_status_postprocessor(d):
|
||||
if d['postprocessor'] == 'MoveFiles' and d['status'] == 'finished':
|
||||
filepath = d['info_dict']['filepath']
|
||||
if '__finaldir' in d['info_dict']:
|
||||
finaldir = d['info_dict']['__finaldir']
|
||||
filename = os.path.join(finaldir, os.path.basename(filepath))
|
||||
else:
|
||||
filename = filepath
|
||||
self.status_queue.put({'status': 'finished', 'filename': filename})
|
||||
# For captions-only downloads, yt-dlp may still report a media-like
|
||||
# filepath in MoveFiles. Capture subtitle outputs explicitly so the
|
||||
# UI can link to real caption files.
|
||||
if getattr(self.info, 'download_type', '') == 'captions':
|
||||
requested_subtitles = d.get('info_dict', {}).get('requested_subtitles', {}) or {}
|
||||
for subtitle in requested_subtitles.values():
|
||||
if isinstance(subtitle, dict) and subtitle.get('filepath'):
|
||||
self.status_queue.put({'subtitle_file': subtitle['filepath']})
|
||||
|
||||
# Capture all chapter files when SplitChapters finishes
|
||||
elif d.get('postprocessor') == 'SplitChapters' and d.get('status') == 'finished':
|
||||
chapters = d.get('info_dict', {}).get('chapters', [])
|
||||
if chapters:
|
||||
for chapter in chapters:
|
||||
if isinstance(chapter, dict) and 'filepath' in chapter:
|
||||
log.info(f"Captured chapter file: {chapter['filepath']}")
|
||||
self.status_queue.put({'chapter_file': chapter['filepath']})
|
||||
else:
|
||||
log.warning("SplitChapters finished but no chapter files found in info_dict")
|
||||
put_status_postprocessor = self._make_postprocessor_hook()
|
||||
|
||||
ytdl_params = {
|
||||
'quiet': not debug_logging,
|
||||
@@ -1090,6 +1115,17 @@ class Download:
|
||||
self.info.size = file_size
|
||||
continue
|
||||
|
||||
# yt-dlp's postprocessor metaclass wraps run() once per class in the
|
||||
# MRO, so a postprocessor whose subclass overrides run reports
|
||||
# started twice (FFmpegCopyStream, among others). Nothing has changed
|
||||
# between the two, so drop the repeat rather than re-encode and
|
||||
# rebroadcast the same state to every connected client. Compared
|
||||
# against the live status, not a flag in the hook, so the phase is
|
||||
# still announced when a pre_process postprocessor ran before the
|
||||
# download and 'downloading' came in between.
|
||||
if status['status'] == 'postprocessing' and self.info.status == 'postprocessing':
|
||||
continue
|
||||
|
||||
self.info.status = status['status']
|
||||
self.info.msg = status.get('msg')
|
||||
if 'downloaded_bytes' in status:
|
||||
|
||||
Reference in New Issue
Block a user