Add a method for extracting info from a list of urls
[youtube-dl] / youtube_dl / FileDownloader.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import absolute_import
5
6 import math
7 import io
8 import os
9 import re
10 import socket
11 import subprocess
12 import sys
13 import time
14 import traceback
15
16 if os.name == 'nt':
17     import ctypes
18
19 from .utils import *
20
21
22 class FileDownloader(object):
23     """File Downloader class.
24
25     File downloader objects are the ones responsible of downloading the
26     actual video file and writing it to disk if the user has requested
27     it, among some other tasks. In most cases there should be one per
28     program. As, given a video URL, the downloader doesn't know how to
29     extract all the needed information, task that InfoExtractors do, it
30     has to pass the URL to one of them.
31
32     For this, file downloader objects have a method that allows
33     InfoExtractors to be registered in a given order. When it is passed
34     a URL, the file downloader handles it to the first InfoExtractor it
35     finds that reports being able to handle it. The InfoExtractor extracts
36     all the information about the video or videos the URL refers to, and
37     asks the FileDownloader to process the video information, possibly
38     downloading the video.
39
40     File downloaders accept a lot of parameters. In order not to saturate
41     the object constructor with arguments, it receives a dictionary of
42     options instead. These options are available through the params
43     attribute for the InfoExtractors to use. The FileDownloader also
44     registers itself as the downloader in charge for the InfoExtractors
45     that are added to it, so this is a "mutual registration".
46
47     Available options:
48
49     username:          Username for authentication purposes.
50     password:          Password for authentication purposes.
51     usenetrc:          Use netrc for authentication instead.
52     quiet:             Do not print messages to stdout.
53     forceurl:          Force printing final URL.
54     forcetitle:        Force printing title.
55     forcethumbnail:    Force printing thumbnail URL.
56     forcedescription:  Force printing description.
57     forcefilename:     Force printing final filename.
58     simulate:          Do not download the video files.
59     format:            Video format code.
60     format_limit:      Highest quality format to try.
61     outtmpl:           Template for output names.
62     restrictfilenames: Do not allow "&" and spaces in file names
63     ignoreerrors:      Do not stop on download errors.
64     ratelimit:         Download speed limit, in bytes/sec.
65     nooverwrites:      Prevent overwriting files.
66     retries:           Number of times to retry for HTTP error 5xx
67     buffersize:        Size of download buffer in bytes.
68     noresizebuffer:    Do not automatically resize the download buffer.
69     continuedl:        Try to continue downloads if possible.
70     noprogress:        Do not print the progress bar.
71     playliststart:     Playlist item to start at.
72     playlistend:       Playlist item to end at.
73     matchtitle:        Download only matching titles.
74     rejecttitle:       Reject downloads for matching titles.
75     logtostderr:       Log messages to stderr instead of stdout.
76     consoletitle:      Display progress in console window's titlebar.
77     nopart:            Do not use temporary .part files.
78     updatetime:        Use the Last-modified header to set output file timestamps.
79     writedescription:  Write the video description to a .description file
80     writeinfojson:     Write the video description to a .info.json file
81     writesubtitles:    Write the video subtitles to a .srt file
82     subtitleslang:     Language of the subtitles to download
83     test:              Download only first bytes to test the downloader.
84     keepvideo:         Keep the video file after post-processing
85     min_filesize:      Skip files smaller than this size
86     max_filesize:      Skip files larger than this size
87     """
88
89     params = None
90     _ies = []
91     _pps = []
92     _download_retcode = None
93     _num_downloads = None
94     _screen_file = None
95
96     def __init__(self, params):
97         """Create a FileDownloader object with the given options."""
98         self._ies = []
99         self._pps = []
100         self._progress_hooks = []
101         self._download_retcode = 0
102         self._num_downloads = 0
103         self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
104         self.params = params
105
106         if '%(stitle)s' in self.params['outtmpl']:
107             self.to_stderr(u'WARNING: %(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
108
109     @staticmethod
110     def format_bytes(bytes):
111         if bytes is None:
112             return 'N/A'
113         if type(bytes) is str:
114             bytes = float(bytes)
115         if bytes == 0.0:
116             exponent = 0
117         else:
118             exponent = int(math.log(bytes, 1024.0))
119         suffix = 'bkMGTPEZY'[exponent]
120         converted = float(bytes) / float(1024 ** exponent)
121         return '%.2f%s' % (converted, suffix)
122
123     @staticmethod
124     def calc_percent(byte_counter, data_len):
125         if data_len is None:
126             return '---.-%'
127         return '%6s' % ('%3.1f%%' % (float(byte_counter) / float(data_len) * 100.0))
128
129     @staticmethod
130     def calc_eta(start, now, total, current):
131         if total is None:
132             return '--:--'
133         dif = now - start
134         if current == 0 or dif < 0.001: # One millisecond
135             return '--:--'
136         rate = float(current) / dif
137         eta = int((float(total) - float(current)) / rate)
138         (eta_mins, eta_secs) = divmod(eta, 60)
139         if eta_mins > 99:
140             return '--:--'
141         return '%02d:%02d' % (eta_mins, eta_secs)
142
143     @staticmethod
144     def calc_speed(start, now, bytes):
145         dif = now - start
146         if bytes == 0 or dif < 0.001: # One millisecond
147             return '%10s' % '---b/s'
148         return '%10s' % ('%s/s' % FileDownloader.format_bytes(float(bytes) / dif))
149
150     @staticmethod
151     def best_block_size(elapsed_time, bytes):
152         new_min = max(bytes / 2.0, 1.0)
153         new_max = min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB
154         if elapsed_time < 0.001:
155             return int(new_max)
156         rate = bytes / elapsed_time
157         if rate > new_max:
158             return int(new_max)
159         if rate < new_min:
160             return int(new_min)
161         return int(rate)
162
163     @staticmethod
164     def parse_bytes(bytestr):
165         """Parse a string indicating a byte quantity into an integer."""
166         matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
167         if matchobj is None:
168             return None
169         number = float(matchobj.group(1))
170         multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
171         return int(round(number * multiplier))
172
173     def add_info_extractor(self, ie):
174         """Add an InfoExtractor object to the end of the list."""
175         self._ies.append(ie)
176         ie.set_downloader(self)
177
178     def add_post_processor(self, pp):
179         """Add a PostProcessor object to the end of the chain."""
180         self._pps.append(pp)
181         pp.set_downloader(self)
182
183     def to_screen(self, message, skip_eol=False):
184         """Print message to stdout if not in quiet mode."""
185         assert type(message) == type(u'')
186         if not self.params.get('quiet', False):
187             terminator = [u'\n', u''][skip_eol]
188             output = message + terminator
189             if 'b' in getattr(self._screen_file, 'mode', '') or sys.version_info[0] < 3: # Python 2 lies about the mode of sys.stdout/sys.stderr
190                 output = output.encode(preferredencoding(), 'ignore')
191             self._screen_file.write(output)
192             self._screen_file.flush()
193
194     def to_stderr(self, message):
195         """Print message to stderr."""
196         assert type(message) == type(u'')
197         output = message + u'\n'
198         if 'b' in getattr(self._screen_file, 'mode', '') or sys.version_info[0] < 3: # Python 2 lies about the mode of sys.stdout/sys.stderr
199             output = output.encode(preferredencoding())
200         sys.stderr.write(output)
201
202     def to_cons_title(self, message):
203         """Set console/terminal window title to message."""
204         if not self.params.get('consoletitle', False):
205             return
206         if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
207             # c_wchar_p() might not be necessary if `message` is
208             # already of type unicode()
209             ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
210         elif 'TERM' in os.environ:
211             self.to_screen('\033]0;%s\007' % message, skip_eol=True)
212
213     def fixed_template(self):
214         """Checks if the output template is fixed."""
215         return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None)
216
217     def trouble(self, message=None, tb=None):
218         """Determine action to take when a download problem appears.
219
220         Depending on if the downloader has been configured to ignore
221         download errors or not, this method may throw an exception or
222         not when errors are found, after printing the message.
223
224         tb, if given, is additional traceback information.
225         """
226         if message is not None:
227             self.to_stderr(message)
228         if self.params.get('verbose'):
229             if tb is None:
230                 tb_data = traceback.format_list(traceback.extract_stack())
231                 tb = u''.join(tb_data)
232             self.to_stderr(tb)
233         if not self.params.get('ignoreerrors', False):
234             raise DownloadError(message)
235         self._download_retcode = 1
236
237     def slow_down(self, start_time, byte_counter):
238         """Sleep if the download speed is over the rate limit."""
239         rate_limit = self.params.get('ratelimit', None)
240         if rate_limit is None or byte_counter == 0:
241             return
242         now = time.time()
243         elapsed = now - start_time
244         if elapsed <= 0.0:
245             return
246         speed = float(byte_counter) / elapsed
247         if speed > rate_limit:
248             time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
249
250     def temp_name(self, filename):
251         """Returns a temporary filename for the given filename."""
252         if self.params.get('nopart', False) or filename == u'-' or \
253                 (os.path.exists(encodeFilename(filename)) and not os.path.isfile(encodeFilename(filename))):
254             return filename
255         return filename + u'.part'
256
257     def undo_temp_name(self, filename):
258         if filename.endswith(u'.part'):
259             return filename[:-len(u'.part')]
260         return filename
261
262     def try_rename(self, old_filename, new_filename):
263         try:
264             if old_filename == new_filename:
265                 return
266             os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
267         except (IOError, OSError) as err:
268             self.trouble(u'ERROR: unable to rename file')
269
270     def try_utime(self, filename, last_modified_hdr):
271         """Try to set the last-modified time of the given file."""
272         if last_modified_hdr is None:
273             return
274         if not os.path.isfile(encodeFilename(filename)):
275             return
276         timestr = last_modified_hdr
277         if timestr is None:
278             return
279         filetime = timeconvert(timestr)
280         if filetime is None:
281             return filetime
282         try:
283             os.utime(filename, (time.time(), filetime))
284         except:
285             pass
286         return filetime
287
288     def report_writedescription(self, descfn):
289         """ Report that the description file is being written """
290         self.to_screen(u'[info] Writing video description to: ' + descfn)
291
292     def report_writesubtitles(self, srtfn):
293         """ Report that the subtitles file is being written """
294         self.to_screen(u'[info] Writing video subtitles to: ' + srtfn)
295
296     def report_writeinfojson(self, infofn):
297         """ Report that the metadata file has been written """
298         self.to_screen(u'[info] Video description metadata as JSON to: ' + infofn)
299
300     def report_destination(self, filename):
301         """Report destination filename."""
302         self.to_screen(u'[download] Destination: ' + filename)
303
304     def report_progress(self, percent_str, data_len_str, speed_str, eta_str):
305         """Report download progress."""
306         if self.params.get('noprogress', False):
307             return
308         if self.params.get('progress_with_newline', False):
309             self.to_screen(u'[download] %s of %s at %s ETA %s' %
310                 (percent_str, data_len_str, speed_str, eta_str))
311         else:
312             self.to_screen(u'\r[download] %s of %s at %s ETA %s' %
313                 (percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
314         self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
315                 (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
316
317     def report_resuming_byte(self, resume_len):
318         """Report attempt to resume at given byte."""
319         self.to_screen(u'[download] Resuming download at byte %s' % resume_len)
320
321     def report_retry(self, count, retries):
322         """Report retry in case of HTTP error 5xx"""
323         self.to_screen(u'[download] Got server HTTP error. Retrying (attempt %d of %d)...' % (count, retries))
324
325     def report_file_already_downloaded(self, file_name):
326         """Report file has already been fully downloaded."""
327         try:
328             self.to_screen(u'[download] %s has already been downloaded' % file_name)
329         except (UnicodeEncodeError) as err:
330             self.to_screen(u'[download] The file has already been downloaded')
331
332     def report_unable_to_resume(self):
333         """Report it was impossible to resume download."""
334         self.to_screen(u'[download] Unable to resume')
335
336     def report_finish(self):
337         """Report download finished."""
338         if self.params.get('noprogress', False):
339             self.to_screen(u'[download] Download completed')
340         else:
341             self.to_screen(u'')
342
343     def increment_downloads(self):
344         """Increment the ordinal that assigns a number to each file."""
345         self._num_downloads += 1
346
347     def prepare_filename(self, info_dict):
348         """Generate the output filename."""
349         try:
350             template_dict = dict(info_dict)
351
352             template_dict['epoch'] = int(time.time())
353             template_dict['autonumber'] = u'%05d' % self._num_downloads
354
355             sanitize = lambda k,v: sanitize_filename(
356                 u'NA' if v is None else compat_str(v),
357                 restricted=self.params.get('restrictfilenames'),
358                 is_id=(k==u'id'))
359             template_dict = dict((k, sanitize(k, v)) for k,v in template_dict.items())
360
361             filename = self.params['outtmpl'] % template_dict
362             return filename
363         except (ValueError, KeyError) as err:
364             self.trouble(u'ERROR: invalid system charset or erroneous output template')
365             return None
366
367     def _match_entry(self, info_dict):
368         """ Returns None iff the file should be downloaded """
369
370         title = info_dict['title']
371         matchtitle = self.params.get('matchtitle', False)
372         if matchtitle:
373             if not re.search(matchtitle, title, re.IGNORECASE):
374                 return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
375         rejecttitle = self.params.get('rejecttitle', False)
376         if rejecttitle:
377             if re.search(rejecttitle, title, re.IGNORECASE):
378                 return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
379         return None
380         
381     def extract_info(self, url):
382         '''
383         Returns a list with a dictionary for each video we find.
384          '''
385         suitable_found = False
386         for ie in self._ies:
387             # Go to next InfoExtractor if not suitable
388             if not ie.suitable(url):
389                 continue
390
391             # Warn if the _WORKING attribute is False
392             if not ie.working():
393                 self.to_stderr(u'WARNING: the program functionality for this site has been marked as broken, '
394                                u'and will probably not work. If you want to go on, use the -i option.')
395
396             # Suitable InfoExtractor found
397             suitable_found = True
398
399             # Extract information from URL and process it
400             try:
401                 videos = ie.extract(url)
402                 for video in videos or []:
403                     if not 'extractor' in video:
404                         #The extractor has already been set somewher else
405                         video['extractor'] = ie.IE_NAME
406                 return videos
407             except ExtractorError as de: # An error we somewhat expected
408                 self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback())
409                 break
410             except Exception as e:
411                 if self.params.get('ignoreerrors', False):
412                     self.trouble(u'ERROR: ' + compat_str(e), tb=compat_str(traceback.format_exc()))
413                     break
414                 else:
415                     raise
416         if not suitable_found:
417                 self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url)
418     def extract_info_iterable(self, urls):
419         '''
420             Return the videos founded for the urls
421         '''
422         results = []
423         for url in urls:
424             results.extend(self.extract_info(url))
425         return results
426
427     def process_info(self, info_dict):
428         """Process a single dictionary returned by an InfoExtractor."""
429
430         # Keep for backwards compatibility
431         info_dict['stitle'] = info_dict['title']
432
433         if not 'format' in info_dict:
434             info_dict['format'] = info_dict['ext']
435
436         reason = self._match_entry(info_dict)
437         if reason is not None:
438             self.to_screen(u'[download] ' + reason)
439             return
440
441         max_downloads = self.params.get('max_downloads')
442         if max_downloads is not None:
443             if self._num_downloads > int(max_downloads):
444                 raise MaxDownloadsReached()
445
446         filename = self.prepare_filename(info_dict)
447
448         # Forced printings
449         if self.params.get('forcetitle', False):
450             compat_print(info_dict['title'])
451         if self.params.get('forceurl', False):
452             compat_print(info_dict['url'])
453         if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
454             compat_print(info_dict['thumbnail'])
455         if self.params.get('forcedescription', False) and 'description' in info_dict:
456             compat_print(info_dict['description'])
457         if self.params.get('forcefilename', False) and filename is not None:
458             compat_print(filename)
459         if self.params.get('forceformat', False):
460             compat_print(info_dict['format'])
461
462         # Do nothing else if in simulate mode
463         if self.params.get('simulate', False):
464             return
465
466         if filename is None:
467             return
468
469         try:
470             dn = os.path.dirname(encodeFilename(filename))
471             if dn != '' and not os.path.exists(dn): # dn is already encoded
472                 os.makedirs(dn)
473         except (OSError, IOError) as err:
474             self.trouble(u'ERROR: unable to create directory ' + compat_str(err))
475             return
476
477         if self.params.get('writedescription', False):
478             try:
479                 descfn = filename + u'.description'
480                 self.report_writedescription(descfn)
481                 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
482                     descfile.write(info_dict['description'])
483             except (OSError, IOError):
484                 self.trouble(u'ERROR: Cannot write description file ' + descfn)
485                 return
486
487         if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
488             # subtitles download errors are already managed as troubles in relevant IE
489             # that way it will silently go on when used with unsupporting IE
490             try:
491                 srtfn = filename.rsplit('.', 1)[0] + u'.srt'
492                 self.report_writesubtitles(srtfn)
493                 with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile:
494                     srtfile.write(info_dict['subtitles'])
495             except (OSError, IOError):
496                 self.trouble(u'ERROR: Cannot write subtitles file ' + descfn)
497                 return
498
499         if self.params.get('writeinfojson', False):
500             infofn = filename + u'.info.json'
501             self.report_writeinfojson(infofn)
502             try:
503                 json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle'])
504                 write_json_file(json_info_dict, encodeFilename(infofn))
505             except (OSError, IOError):
506                 self.trouble(u'ERROR: Cannot write metadata to JSON file ' + infofn)
507                 return
508
509         if not self.params.get('skip_download', False):
510             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
511                 success = True
512             else:
513                 try:
514                     success = self._do_download(filename, info_dict)
515                 except (OSError, IOError) as err:
516                     raise UnavailableVideoError()
517                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
518                     self.trouble(u'ERROR: unable to download video data: %s' % str(err))
519                     return
520                 except (ContentTooShortError, ) as err:
521                     self.trouble(u'ERROR: content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
522                     return
523
524             if success:
525                 try:
526                     self.post_process(filename, info_dict)
527                 except (PostProcessingError) as err:
528                     self.trouble(u'ERROR: postprocessing: %s' % str(err))
529                     return
530
531     def download(self, url_list):
532         """Download a given list of URLs."""
533         if len(url_list) > 1 and self.fixed_template():
534             raise SameFileError(self.params['outtmpl'])
535
536         for url in url_list:
537             videos = self.extract_info(url)
538
539             for video in videos or []:
540                 try:
541                     self.increment_downloads()
542                     self.process_info(video)
543                 except UnavailableVideoError:
544                     self.trouble(u'\nERROR: unable to download video')
545
546         return self._download_retcode
547
548     def post_process(self, filename, ie_info):
549         """Run all the postprocessors on the given file."""
550         info = dict(ie_info)
551         info['filepath'] = filename
552         keep_video = None
553         for pp in self._pps:
554             try:
555                 keep_video_wish,new_info = pp.run(info)
556                 if keep_video_wish is not None:
557                     if keep_video_wish:
558                         keep_video = keep_video_wish
559                     elif keep_video is None:
560                         # No clear decision yet, let IE decide
561                         keep_video = keep_video_wish
562             except PostProcessingError as e:
563                 self.to_stderr(u'ERROR: ' + e.msg)
564         if keep_video is False and not self.params.get('keepvideo', False):
565             try:
566                 self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
567                 os.remove(encodeFilename(filename))
568             except (IOError, OSError):
569                 self.to_stderr(u'WARNING: Unable to remove downloaded video file')
570
571     def _download_with_rtmpdump(self, filename, url, player_url, page_url):
572         self.report_destination(filename)
573         tmpfilename = self.temp_name(filename)
574
575         # Check for rtmpdump first
576         try:
577             subprocess.call(['rtmpdump', '-h'], stdout=(file(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
578         except (OSError, IOError):
579             self.trouble(u'ERROR: RTMP download detected but "rtmpdump" could not be run')
580             return False
581
582         # Download using rtmpdump. rtmpdump returns exit code 2 when
583         # the connection was interrumpted and resuming appears to be
584         # possible. This is part of rtmpdump's normal usage, AFAIK.
585         basic_args = ['rtmpdump', '-q', '-r', url, '-o', tmpfilename]
586         if player_url is not None:
587             basic_args += ['-W', player_url]
588         if page_url is not None:
589             basic_args += ['--pageUrl', page_url]
590         args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)]
591         if self.params.get('verbose', False):
592             try:
593                 import pipes
594                 shell_quote = lambda args: ' '.join(map(pipes.quote, args))
595             except ImportError:
596                 shell_quote = repr
597             self.to_screen(u'[debug] rtmpdump command line: ' + shell_quote(args))
598         retval = subprocess.call(args)
599         while retval == 2 or retval == 1:
600             prevsize = os.path.getsize(encodeFilename(tmpfilename))
601             self.to_screen(u'\r[rtmpdump] %s bytes' % prevsize, skip_eol=True)
602             time.sleep(5.0) # This seems to be needed
603             retval = subprocess.call(basic_args + ['-e'] + [[], ['-k', '1']][retval == 1])
604             cursize = os.path.getsize(encodeFilename(tmpfilename))
605             if prevsize == cursize and retval == 1:
606                 break
607              # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those
608             if prevsize == cursize and retval == 2 and cursize > 1024:
609                 self.to_screen(u'\r[rtmpdump] Could not download the whole video. This can happen for some advertisements.')
610                 retval = 0
611                 break
612         if retval == 0:
613             fsize = os.path.getsize(encodeFilename(tmpfilename))
614             self.to_screen(u'\r[rtmpdump] %s bytes' % fsize)
615             self.try_rename(tmpfilename, filename)
616             self._hook_progress({
617                 'downloaded_bytes': fsize,
618                 'total_bytes': fsize,
619                 'filename': filename,
620                 'status': 'finished',
621             })
622             return True
623         else:
624             self.trouble(u'\nERROR: rtmpdump exited with code %d' % retval)
625             return False
626
627     def _do_download(self, filename, info_dict):
628         url = info_dict['url']
629
630         # Check file already present
631         if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False):
632             self.report_file_already_downloaded(filename)
633             self._hook_progress({
634                 'filename': filename,
635                 'status': 'finished',
636             })
637             return True
638
639         # Attempt to download using rtmpdump
640         if url.startswith('rtmp'):
641             return self._download_with_rtmpdump(filename, url,
642                                                 info_dict.get('player_url', None),
643                                                 info_dict.get('page_url', None))
644
645         tmpfilename = self.temp_name(filename)
646         stream = None
647
648         # Do not include the Accept-Encoding header
649         headers = {'Youtubedl-no-compression': 'True'}
650         if 'user_agent' in info_dict:
651             headers['Youtubedl-user-agent'] = info_dict['user_agent']
652         basic_request = compat_urllib_request.Request(url, None, headers)
653         request = compat_urllib_request.Request(url, None, headers)
654
655         if self.params.get('test', False):
656             request.add_header('Range','bytes=0-10240')
657
658         # Establish possible resume length
659         if os.path.isfile(encodeFilename(tmpfilename)):
660             resume_len = os.path.getsize(encodeFilename(tmpfilename))
661         else:
662             resume_len = 0
663
664         open_mode = 'wb'
665         if resume_len != 0:
666             if self.params.get('continuedl', False):
667                 self.report_resuming_byte(resume_len)
668                 request.add_header('Range','bytes=%d-' % resume_len)
669                 open_mode = 'ab'
670             else:
671                 resume_len = 0
672
673         count = 0
674         retries = self.params.get('retries', 0)
675         while count <= retries:
676             # Establish connection
677             try:
678                 if count == 0 and 'urlhandle' in info_dict:
679                     data = info_dict['urlhandle']
680                 data = compat_urllib_request.urlopen(request)
681                 break
682             except (compat_urllib_error.HTTPError, ) as err:
683                 if (err.code < 500 or err.code >= 600) and err.code != 416:
684                     # Unexpected HTTP error
685                     raise
686                 elif err.code == 416:
687                     # Unable to resume (requested range not satisfiable)
688                     try:
689                         # Open the connection again without the range header
690                         data = compat_urllib_request.urlopen(basic_request)
691                         content_length = data.info()['Content-Length']
692                     except (compat_urllib_error.HTTPError, ) as err:
693                         if err.code < 500 or err.code >= 600:
694                             raise
695                     else:
696                         # Examine the reported length
697                         if (content_length is not None and
698                                 (resume_len - 100 < int(content_length) < resume_len + 100)):
699                             # The file had already been fully downloaded.
700                             # Explanation to the above condition: in issue #175 it was revealed that
701                             # YouTube sometimes adds or removes a few bytes from the end of the file,
702                             # changing the file size slightly and causing problems for some users. So
703                             # I decided to implement a suggested change and consider the file
704                             # completely downloaded if the file size differs less than 100 bytes from
705                             # the one in the hard drive.
706                             self.report_file_already_downloaded(filename)
707                             self.try_rename(tmpfilename, filename)
708                             self._hook_progress({
709                                 'filename': filename,
710                                 'status': 'finished',
711                             })
712                             return True
713                         else:
714                             # The length does not match, we start the download over
715                             self.report_unable_to_resume()
716                             open_mode = 'wb'
717                             break
718             # Retry
719             count += 1
720             if count <= retries:
721                 self.report_retry(count, retries)
722
723         if count > retries:
724             self.trouble(u'ERROR: giving up after %s retries' % retries)
725             return False
726
727         data_len = data.info().get('Content-length', None)
728         if data_len is not None:
729             data_len = int(data_len) + resume_len
730             min_data_len = self.params.get("min_filesize", None)
731             max_data_len =  self.params.get("max_filesize", None)
732             if min_data_len is not None and data_len < min_data_len:
733                 self.to_screen(u'\r[download] File is smaller than min-filesize (%s bytes < %s bytes). Aborting.' % (data_len, min_data_len))
734                 return False
735             if max_data_len is not None and data_len > max_data_len:
736                 self.to_screen(u'\r[download] File is larger than max-filesize (%s bytes > %s bytes). Aborting.' % (data_len, max_data_len))
737                 return False
738
739         data_len_str = self.format_bytes(data_len)
740         byte_counter = 0 + resume_len
741         block_size = self.params.get('buffersize', 1024)
742         start = time.time()
743         while True:
744             # Download and write
745             before = time.time()
746             data_block = data.read(block_size)
747             after = time.time()
748             if len(data_block) == 0:
749                 break
750             byte_counter += len(data_block)
751
752             # Open file just in time
753             if stream is None:
754                 try:
755                     (stream, tmpfilename) = sanitize_open(tmpfilename, open_mode)
756                     assert stream is not None
757                     filename = self.undo_temp_name(tmpfilename)
758                     self.report_destination(filename)
759                 except (OSError, IOError) as err:
760                     self.trouble(u'ERROR: unable to open for writing: %s' % str(err))
761                     return False
762             try:
763                 stream.write(data_block)
764             except (IOError, OSError) as err:
765                 self.trouble(u'\nERROR: unable to write data: %s' % str(err))
766                 return False
767             if not self.params.get('noresizebuffer', False):
768                 block_size = self.best_block_size(after - before, len(data_block))
769
770             # Progress message
771             speed_str = self.calc_speed(start, time.time(), byte_counter - resume_len)
772             if data_len is None:
773                 self.report_progress('Unknown %', data_len_str, speed_str, 'Unknown ETA')
774             else:
775                 percent_str = self.calc_percent(byte_counter, data_len)
776                 eta_str = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
777                 self.report_progress(percent_str, data_len_str, speed_str, eta_str)
778
779             self._hook_progress({
780                 'downloaded_bytes': byte_counter,
781                 'total_bytes': data_len,
782                 'tmpfilename': tmpfilename,
783                 'filename': filename,
784                 'status': 'downloading',
785             })
786
787             # Apply rate limit
788             self.slow_down(start, byte_counter - resume_len)
789
790         if stream is None:
791             self.trouble(u'\nERROR: Did not get any data blocks')
792             return False
793         stream.close()
794         self.report_finish()
795         if data_len is not None and byte_counter != data_len:
796             raise ContentTooShortError(byte_counter, int(data_len))
797         self.try_rename(tmpfilename, filename)
798
799         # Update file modification time
800         if self.params.get('updatetime', True):
801             info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None))
802
803         self._hook_progress({
804             'downloaded_bytes': byte_counter,
805             'total_bytes': byte_counter,
806             'filename': filename,
807             'status': 'finished',
808         })
809
810         return True
811
812     def _hook_progress(self, status):
813         for ph in self._progress_hooks:
814             ph(status)
815
816     def add_progress_hook(self, ph):
817         """ ph gets called on download progress, with a dictionary with the entries
818         * filename: The final filename
819         * status: One of "downloading" and "finished"
820
821         It can also have some of the following entries:
822
823         * downloaded_bytes: Bytes on disks
824         * total_bytes: Total bytes, None if unknown
825         * tmpfilename: The filename we're currently writing to
826
827         Hooks are guaranteed to be called at least once (with status "finished")
828         if the download is successful.
829         """
830         self._progress_hooks.append(ph)