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