瀏覽代碼

[YoutubeDL] Use a progress hook for progress reporting

Instead of every downloader calling two helper functions, let our progress report be an ordinary progress hook like everyone else's.
Closes #4875.
Philipp Hagemeister 10 年之前
父節點
當前提交
5cda4eda72
共有 5 個文件被更改,包括 128 次插入73 次删除
  1. 10 3
      youtube_dl/YoutubeDL.py
  2. 55 32
      youtube_dl/downloader/common.py
  3. 44 26
      youtube_dl/downloader/f4m.py
  4. 10 3
      youtube_dl/downloader/http.py
  5. 9 9
      youtube_dl/downloader/rtmp.py

+ 10 - 3
youtube_dl/YoutubeDL.py

@@ -199,18 +199,25 @@ class YoutubeDL(object):
                        postprocessor.
                        postprocessor.
     progress_hooks:    A list of functions that get called on download
     progress_hooks:    A list of functions that get called on download
                        progress, with a dictionary with the entries
                        progress, with a dictionary with the entries
-                       * status: One of "downloading" and "finished".
+                       * status: One of "downloading", "error", or "finished".
                                  Check this first and ignore unknown values.
                                  Check this first and ignore unknown values.
 
 
-                       If status is one of "downloading" or "finished", the
+                       If status is one of "downloading", or "finished", the
                        following properties may also be present:
                        following properties may also be present:
                        * filename: The final filename (always present)
                        * filename: The final filename (always present)
+                       * tmpfilename: The filename we're currently writing to
                        * downloaded_bytes: Bytes on disk
                        * downloaded_bytes: Bytes on disk
                        * total_bytes: Size of the whole file, None if unknown
                        * total_bytes: Size of the whole file, None if unknown
-                       * tmpfilename: The filename we're currently writing to
+                       * total_bytes_estimate: Guess of the eventual file size,
+                                               None if unavailable.
+                       * elapsed: The number of seconds since download started.
                        * eta: The estimated time in seconds, None if unknown
                        * eta: The estimated time in seconds, None if unknown
                        * speed: The download speed in bytes/second, None if
                        * speed: The download speed in bytes/second, None if
                                 unknown
                                 unknown
+                       * fragment_index: The counter of the currently
+                                         downloaded video fragment.
+                       * fragment_count: The number of fragments (= individual
+                                         files that will be merged)
 
 
                        Progress hooks are guaranteed to be called at least once
                        Progress hooks are guaranteed to be called at least once
                        (with status "finished") if the download is successful.
                        (with status "finished") if the download is successful.

+ 55 - 32
youtube_dl/downloader/common.py

@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+from __future__ import division, unicode_literals
 
 
 import os
 import os
 import re
 import re
@@ -54,6 +54,7 @@ class FileDownloader(object):
         self.ydl = ydl
         self.ydl = ydl
         self._progress_hooks = []
         self._progress_hooks = []
         self.params = params
         self.params = params
+        self.add_progress_hook(self.report_progress)
 
 
     @staticmethod
     @staticmethod
     def format_seconds(seconds):
     def format_seconds(seconds):
@@ -226,42 +227,64 @@ class FileDownloader(object):
             self.to_screen(clear_line + fullmsg, skip_eol=not is_last_line)
             self.to_screen(clear_line + fullmsg, skip_eol=not is_last_line)
         self.to_console_title('youtube-dl ' + msg)
         self.to_console_title('youtube-dl ' + msg)
 
 
-    def report_progress(self, percent, data_len_str, speed, eta):
-        """Report download progress."""
-        if self.params.get('noprogress', False):
+    def report_progress(self, s):
+        if s['status'] == 'finished':
+            if self.params.get('noprogress', False):
+                self.to_screen('[download] Download completed')
+            else:
+                s['_total_bytes_str'] = format_bytes(s['total_bytes'])
+                if s.get('elapsed') is not None:
+                    s['_elapsed_str'] = self.format_seconds(s['elapsed'])
+                    msg_template = '100%% of %(_total_bytes_str)s in %(_elapsed_str)s'
+                else:
+                    msg_template = '100%% of %(_total_bytes_str)s'
+                self._report_progress_status(
+                    msg_template % s, is_last_line=True)
+
+        if self.params.get('noprogress'):
             return
             return
-        if eta is not None:
-            eta_str = self.format_eta(eta)
-        else:
-            eta_str = 'Unknown ETA'
-        if percent is not None:
-            percent_str = self.format_percent(percent)
+
+        if s['status'] != 'downloading':
+            return
+
+        if s.get('eta') is not None:
+            s['_eta_str'] = self.format_eta(s['eta'])
         else:
         else:
-            percent_str = 'Unknown %'
-        speed_str = self.format_speed(speed)
+            s['_eta_str'] = 'Unknown ETA'
 
 
-        msg = ('%s of %s at %s ETA %s' %
-               (percent_str, data_len_str, speed_str, eta_str))
-        self._report_progress_status(msg)
+        if s.get('total_bytes') and s.get('downloaded_bytes') is not None:
+            s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes'])
+        elif s.get('total_bytes_estimate') and s.get('downloaded_bytes') is not None:
+            s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes_estimate'])
+        else:
+            if s.get('downloaded_bytes') == 0:
+                s['_percent_str'] = self.format_percent(0)
+            else:
+                s['_percent_str'] = 'Unknown %'
 
 
-    def report_progress_live_stream(self, downloaded_data_len, speed, elapsed):
-        if self.params.get('noprogress', False):
-            return
-        downloaded_str = format_bytes(downloaded_data_len)
-        speed_str = self.format_speed(speed)
-        elapsed_str = FileDownloader.format_seconds(elapsed)
-        msg = '%s at %s (%s)' % (downloaded_str, speed_str, elapsed_str)
-        self._report_progress_status(msg)
-
-    def report_finish(self, data_len_str, tot_time):
-        """Report download finished."""
-        if self.params.get('noprogress', False):
-            self.to_screen('[download] Download completed')
+        if s.get('speed') is not None:
+            s['_speed_str'] = self.format_speed(s['speed'])
+        else:
+            s['_speed_str'] = 'Unknown speed'
+
+        if s.get('total_bytes') is not None:
+            s['_total_bytes_str'] = format_bytes(s['total_bytes'])
+            msg_template = '%(_percent_str)s of %(_total_bytes_str)s at %(_speed_str)s ETA %(_eta_str)s'
+        elif s.get('total_bytes_estimate') is not None:
+            s['_total_bytes_estimate_str'] = format_bytes(s['total_bytes_estimate'])
+            msg_template = '%(_percent_str)s of ~%(_total_bytes_estimate_str)s at %(_speed_str)s ETA %(_eta_str)s'
         else:
         else:
-            self._report_progress_status(
-                ('100%% of %s in %s' %
-                 (data_len_str, self.format_seconds(tot_time))),
-                is_last_line=True)
+            if s.get('downloaded_bytes') is not None:
+                s['_downloaded_bytes_str'] = format_bytes(s['downloaded_bytes'])
+                if s.get('elapsed'):
+                    s['_elapsed_str'] = self.format_seconds(s['elapsed'])
+                    msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s (%(_elapsed_str)s)'
+                else:
+                    msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
+            else:
+                msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s'
+
+        self._report_progress_status(msg_template % s)
 
 
     def report_resuming_byte(self, resume_len):
     def report_resuming_byte(self, resume_len):
         """Report attempt to resume at given byte."""
         """Report attempt to resume at given byte."""

+ 44 - 26
youtube_dl/downloader/f4m.py

@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+from __future__ import division, unicode_literals
 
 
 import base64
 import base64
 import io
 import io
@@ -252,17 +252,6 @@ class F4mFD(FileDownloader):
         requested_bitrate = info_dict.get('tbr')
         requested_bitrate = info_dict.get('tbr')
         self.to_screen('[download] Downloading f4m manifest')
         self.to_screen('[download] Downloading f4m manifest')
         manifest = self.ydl.urlopen(man_url).read()
         manifest = self.ydl.urlopen(man_url).read()
-        self.report_destination(filename)
-        http_dl = HttpQuietDownloader(
-            self.ydl,
-            {
-                'continuedl': True,
-                'quiet': True,
-                'noprogress': True,
-                'ratelimit': self.params.get('ratelimit', None),
-                'test': self.params.get('test', False),
-            }
-        )
 
 
         doc = etree.fromstring(manifest)
         doc = etree.fromstring(manifest)
         formats = [(int(f.attrib.get('bitrate', -1)), f)
         formats = [(int(f.attrib.get('bitrate', -1)), f)
@@ -298,39 +287,67 @@ class F4mFD(FileDownloader):
         # For some akamai manifests we'll need to add a query to the fragment url
         # For some akamai manifests we'll need to add a query to the fragment url
         akamai_pv = xpath_text(doc, _add_ns('pv-2.0'))
         akamai_pv = xpath_text(doc, _add_ns('pv-2.0'))
 
 
+        self.report_destination(filename)
+        http_dl = HttpQuietDownloader(
+            self.ydl,
+            {
+                'continuedl': True,
+                'quiet': True,
+                'noprogress': True,
+                'ratelimit': self.params.get('ratelimit', None),
+                'test': self.params.get('test', False),
+            }
+        )
         tmpfilename = self.temp_name(filename)
         tmpfilename = self.temp_name(filename)
         (dest_stream, tmpfilename) = sanitize_open(tmpfilename, 'wb')
         (dest_stream, tmpfilename) = sanitize_open(tmpfilename, 'wb')
+
         write_flv_header(dest_stream)
         write_flv_header(dest_stream)
         write_metadata_tag(dest_stream, metadata)
         write_metadata_tag(dest_stream, metadata)
 
 
         # This dict stores the download progress, it's updated by the progress
         # This dict stores the download progress, it's updated by the progress
         # hook
         # hook
         state = {
         state = {
+            'status': 'downloading',
             'downloaded_bytes': 0,
             'downloaded_bytes': 0,
-            'frag_counter': 0,
+            'frag_index': 0,
+            'frag_count': total_frags,
+            'filename': filename,
+            'tmpfilename': tmpfilename,
         }
         }
         start = time.time()
         start = time.time()
 
 
-        def frag_progress_hook(status):
-            frag_total_bytes = status.get('total_bytes', 0)
-            estimated_size = (state['downloaded_bytes'] +
-                              (total_frags - state['frag_counter']) * frag_total_bytes)
-            if status['status'] == 'finished':
+        def frag_progress_hook(s):
+            if s['status'] not in ('downloading', 'finished'):
+                return
+
+            frag_total_bytes = s.get('total_bytes', 0)
+            if s['status'] == 'finished':
                 state['downloaded_bytes'] += frag_total_bytes
                 state['downloaded_bytes'] += frag_total_bytes
-                state['frag_counter'] += 1
-                progress = self.calc_percent(state['frag_counter'], total_frags)
+                state['frag_index'] += 1
+
+            estimated_size = (
+                (state['downloaded_bytes'] + frag_total_bytes)
+                / (state['frag_index'] + 1) * total_frags)
+            time_now = time.time()
+            state['total_bytes_estimate'] = estimated_size
+            state['elapsed'] = time_now - start
+
+            if s['status'] == 'finished':
+                progress = self.calc_percent(state['frag_index'], total_frags)
                 byte_counter = state['downloaded_bytes']
                 byte_counter = state['downloaded_bytes']
             else:
             else:
-                frag_downloaded_bytes = status['downloaded_bytes']
+                frag_downloaded_bytes = s['downloaded_bytes']
                 byte_counter = state['downloaded_bytes'] + frag_downloaded_bytes
                 byte_counter = state['downloaded_bytes'] + frag_downloaded_bytes
                 frag_progress = self.calc_percent(frag_downloaded_bytes,
                 frag_progress = self.calc_percent(frag_downloaded_bytes,
                                                   frag_total_bytes)
                                                   frag_total_bytes)
-                progress = self.calc_percent(state['frag_counter'], total_frags)
+                progress = self.calc_percent(state['frag_index'], total_frags)
                 progress += frag_progress / float(total_frags)
                 progress += frag_progress / float(total_frags)
 
 
-            eta = self.calc_eta(start, time.time(), estimated_size, byte_counter)
-            self.report_progress(progress, format_bytes(estimated_size),
-                                 status.get('speed'), eta)
+                state['eta'] = self.calc_eta(
+                    start, time_now, estimated_size, state['downloaded_bytes'] + frag_downloaded_bytes)
+                state['speed'] = s.get('speed')
+            self._hook_progress(state)
+
         http_dl.add_progress_hook(frag_progress_hook)
         http_dl.add_progress_hook(frag_progress_hook)
 
 
         frags_filenames = []
         frags_filenames = []
@@ -354,8 +371,8 @@ class F4mFD(FileDownloader):
             frags_filenames.append(frag_filename)
             frags_filenames.append(frag_filename)
 
 
         dest_stream.close()
         dest_stream.close()
-        self.report_finish(format_bytes(state['downloaded_bytes']), time.time() - start)
 
 
+        elapsed = time.time() - start
         self.try_rename(tmpfilename, filename)
         self.try_rename(tmpfilename, filename)
         for frag_file in frags_filenames:
         for frag_file in frags_filenames:
             os.remove(frag_file)
             os.remove(frag_file)
@@ -366,6 +383,7 @@ class F4mFD(FileDownloader):
             'total_bytes': fsize,
             'total_bytes': fsize,
             'filename': filename,
             'filename': filename,
             'status': 'finished',
             'status': 'finished',
+            'elapsed': elapsed,
         })
         })
 
 
         return True
         return True

+ 10 - 3
youtube_dl/downloader/http.py

@@ -200,16 +200,16 @@ class HttpFD(FileDownloader):
             else:
             else:
                 percent = self.calc_percent(byte_counter, data_len)
                 percent = self.calc_percent(byte_counter, data_len)
                 eta = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
                 eta = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
-            self.report_progress(percent, data_len_str, speed, eta)
 
 
             self._hook_progress({
             self._hook_progress({
+                'status': 'downloading',
                 'downloaded_bytes': byte_counter,
                 'downloaded_bytes': byte_counter,
                 'total_bytes': data_len,
                 'total_bytes': data_len,
                 'tmpfilename': tmpfilename,
                 'tmpfilename': tmpfilename,
                 'filename': filename,
                 'filename': filename,
-                'status': 'downloading',
                 'eta': eta,
                 'eta': eta,
                 'speed': speed,
                 'speed': speed,
+                'elapsed': now - start,
             })
             })
 
 
             if is_test and byte_counter == data_len:
             if is_test and byte_counter == data_len:
@@ -221,7 +221,13 @@ class HttpFD(FileDownloader):
             return False
             return False
         if tmpfilename != '-':
         if tmpfilename != '-':
             stream.close()
             stream.close()
-        self.report_finish(data_len_str, (time.time() - start))
+
+        self._hook_progress({
+            'downloaded_bytes': byte_counter,
+            'total_bytes': data_len,
+            'tmpfilename': tmpfilename,
+            'status': 'error',
+        })
         if data_len is not None and byte_counter != data_len:
         if data_len is not None and byte_counter != data_len:
             raise ContentTooShortError(byte_counter, int(data_len))
             raise ContentTooShortError(byte_counter, int(data_len))
         self.try_rename(tmpfilename, filename)
         self.try_rename(tmpfilename, filename)
@@ -235,6 +241,7 @@ class HttpFD(FileDownloader):
             'total_bytes': byte_counter,
             'total_bytes': byte_counter,
             'filename': filename,
             'filename': filename,
             'status': 'finished',
             'status': 'finished',
+            'elapsed': time.time() - start,
         })
         })
 
 
         return True
         return True

+ 9 - 9
youtube_dl/downloader/rtmp.py

@@ -51,23 +51,23 @@ class RtmpFD(FileDownloader):
                     if not resume_percent:
                     if not resume_percent:
                         resume_percent = percent
                         resume_percent = percent
                         resume_downloaded_data_len = downloaded_data_len
                         resume_downloaded_data_len = downloaded_data_len
-                    eta = self.calc_eta(start, time.time(), 100 - resume_percent, percent - resume_percent)
-                    speed = self.calc_speed(start, time.time(), downloaded_data_len - resume_downloaded_data_len)
+                    time_now = time.time()
+                    eta = self.calc_eta(start, time_now, 100 - resume_percent, percent - resume_percent)
+                    speed = self.calc_speed(start, time_now, downloaded_data_len - resume_downloaded_data_len)
                     data_len = None
                     data_len = None
                     if percent > 0:
                     if percent > 0:
                         data_len = int(downloaded_data_len * 100 / percent)
                         data_len = int(downloaded_data_len * 100 / percent)
-                    data_len_str = '~' + format_bytes(data_len)
-                    self.report_progress(percent, data_len_str, speed, eta)
-                    cursor_in_new_line = False
                     self._hook_progress({
                     self._hook_progress({
+                        'status': 'downloading',
                         'downloaded_bytes': downloaded_data_len,
                         'downloaded_bytes': downloaded_data_len,
-                        'total_bytes': data_len,
+                        'total_bytes_estimate': data_len,
                         'tmpfilename': tmpfilename,
                         'tmpfilename': tmpfilename,
                         'filename': filename,
                         'filename': filename,
-                        'status': 'downloading',
                         'eta': eta,
                         'eta': eta,
+                        'elapsed': time_now - start,
                         'speed': speed,
                         'speed': speed,
                     })
                     })
+                    cursor_in_new_line = False
                 else:
                 else:
                     # no percent for live streams
                     # no percent for live streams
                     mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec', line)
                     mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec', line)
@@ -75,15 +75,15 @@ class RtmpFD(FileDownloader):
                         downloaded_data_len = int(float(mobj.group(1)) * 1024)
                         downloaded_data_len = int(float(mobj.group(1)) * 1024)
                         time_now = time.time()
                         time_now = time.time()
                         speed = self.calc_speed(start, time_now, downloaded_data_len)
                         speed = self.calc_speed(start, time_now, downloaded_data_len)
-                        self.report_progress_live_stream(downloaded_data_len, speed, time_now - start)
-                        cursor_in_new_line = False
                         self._hook_progress({
                         self._hook_progress({
                             'downloaded_bytes': downloaded_data_len,
                             'downloaded_bytes': downloaded_data_len,
                             'tmpfilename': tmpfilename,
                             'tmpfilename': tmpfilename,
                             'filename': filename,
                             'filename': filename,
                             'status': 'downloading',
                             'status': 'downloading',
+                            'elapsed': time_now - start,
                             'speed': speed,
                             'speed': speed,
                         })
                         })
+                        cursor_in_new_line = False
                     elif self.params.get('verbose', False):
                     elif self.params.get('verbose', False):
                         if not cursor_in_new_line:
                         if not cursor_in_new_line:
                             self.to_screen('')
                             self.to_screen('')