]>
Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/FileDownloader.py
2 # -*- coding: utf-8 -*-
4 from __future__
import absolute_import
21 class FileDownloader ( object ):
22 """File Downloader class.
24 File downloader objects are the ones responsible of downloading the
25 actual video file and writing it to disk if the user has requested
26 it, among some other tasks. In most cases there should be one per
27 program. As, given a video URL, the downloader doesn't know how to
28 extract all the needed information, task that InfoExtractors do, it
29 has to pass the URL to one of them.
31 For this, file downloader objects have a method that allows
32 InfoExtractors to be registered in a given order. When it is passed
33 a URL, the file downloader handles it to the first InfoExtractor it
34 finds that reports being able to handle it. The InfoExtractor extracts
35 all the information about the video or videos the URL refers to, and
36 asks the FileDownloader to process the video information, possibly
37 downloading the video.
39 File downloaders accept a lot of parameters. In order not to saturate
40 the object constructor with arguments, it receives a dictionary of
41 options instead. These options are available through the params
42 attribute for the InfoExtractors to use. The FileDownloader also
43 registers itself as the downloader in charge for the InfoExtractors
44 that are added to it, so this is a "mutual registration".
48 username: Username for authentication purposes.
49 password: Password for authentication purposes.
50 usenetrc: Use netrc for authentication instead.
51 quiet: Do not print messages to stdout.
52 forceurl: Force printing final URL.
53 forcetitle: Force printing title.
54 forcethumbnail: Force printing thumbnail URL.
55 forcedescription: Force printing description.
56 forcefilename: Force printing final filename.
57 simulate: Do not download the video files.
58 format: Video format code.
59 format_limit: Highest quality format to try.
60 outtmpl: Template for output names.
61 restrictfilenames: Do not allow "&" and spaces in file names
62 ignoreerrors: Do not stop on download errors.
63 ratelimit: Download speed limit, in bytes/sec.
64 nooverwrites: Prevent overwriting files.
65 retries: Number of times to retry for HTTP error 5xx
66 buffersize: Size of download buffer in bytes.
67 noresizebuffer: Do not automatically resize the download buffer.
68 continuedl: Try to continue downloads if possible.
69 noprogress: Do not print the progress bar.
70 playliststart: Playlist item to start at.
71 playlistend: Playlist item to end at.
72 matchtitle: Download only matching titles.
73 rejecttitle: Reject downloads for matching titles.
74 logtostderr: Log messages to stderr instead of stdout.
75 consoletitle: Display progress in console window's titlebar.
76 nopart: Do not use temporary .part files.
77 updatetime: Use the Last-modified header to set output file timestamps.
78 writedescription: Write the video description to a .description file
79 writeinfojson: Write the video description to a .info.json file
80 writesubtitles: Write the video subtitles to a .srt file
81 subtitleslang: Language of the subtitles to download
82 test: Download only first bytes to test the downloader.
88 _download_retcode
= None
92 def __init__ ( self
, params
):
93 """Create a FileDownloader object with the given options."""
96 self
._ download
_ retcode
= 0
97 self
._ num
_ downloads
= 0
98 self
._ screen
_ file
= [ sys
. stdout
, sys
. stderr
][ params
. get ( 'logtostderr' , False )]
101 if ' %(stitle)s ' in self
. params
[ 'outtmpl' ]:
102 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.' )
105 def format_bytes ( bytes ):
108 if type ( bytes ) is str :
113 exponent
= int ( math
. log ( bytes , 1024.0 ))
114 suffix
= 'bkMGTPEZY' [ exponent
]
115 converted
= float ( bytes ) / float ( 1024 ** exponent
)
116 return '%.2f %s ' % ( converted
, suffix
)
119 def calc_percent ( byte_counter
, data_len
):
122 return '%6s' % ( '%3.1f %% ' % ( float ( byte_counter
) / float ( data_len
) * 100.0 ))
125 def calc_eta ( start
, now
, total
, current
):
129 if current
== 0 or dif
< 0.001 : # One millisecond
131 rate
= float ( current
) / dif
132 eta
= int (( float ( total
) - float ( current
)) / rate
)
133 ( eta_mins
, eta_secs
) = divmod ( eta
, 60 )
136 return ' %0 2d: %0 2d' % ( eta_mins
, eta_secs
)
139 def calc_speed ( start
, now
, bytes ):
141 if bytes == 0 or dif
< 0.001 : # One millisecond
142 return '%10s' % '---b/s'
143 return '%10s' % ( ' %s /s' % FileDownloader
. format_bytes ( float ( bytes ) / dif
))
146 def best_block_size ( elapsed_time
, bytes ):
147 new_min
= max ( bytes / 2.0 , 1.0 )
148 new_max
= min ( max ( bytes * 2.0 , 1.0 ), 4194304 ) # Do not surpass 4 MB
149 if elapsed_time
< 0.001 :
151 rate
= bytes / elapsed_time
159 def parse_bytes ( bytestr
):
160 """Parse a string indicating a byte quantity into an integer."""
161 matchobj
= re
. match ( r
'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$' , bytestr
)
164 number
= float ( matchobj
. group ( 1 ))
165 multiplier
= 1024.0 ** 'bkmgtpezy' . index ( matchobj
. group ( 2 ). lower ())
166 return int ( round ( number
* multiplier
))
168 def add_info_extractor ( self
, ie
):
169 """Add an InfoExtractor object to the end of the list."""
171 ie
. set_downloader ( self
)
173 def add_post_processor ( self
, pp
):
174 """Add a PostProcessor object to the end of the chain."""
176 pp
. set_downloader ( self
)
178 def to_screen ( self
, message
, skip_eol
= False ):
179 """Print message to stdout if not in quiet mode."""
180 assert type ( message
) == type ( u
'' )
181 if not self
. params
. get ( 'quiet' , False ):
182 terminator
= [ u
' \n ' , u
'' ][ skip_eol
]
183 output
= message
+ terminator
184 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
185 output
= output
. encode ( preferredencoding (), 'ignore' )
186 self
._ screen
_ file
. write ( output
)
187 self
._ screen
_ file
. flush ()
189 def to_stderr ( self
, message
):
190 """Print message to stderr."""
191 assert type ( message
) == type ( u
'' )
192 output
= message
+ u
' \n '
193 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
194 output
= output
. encode ( preferredencoding ())
195 sys
. stderr
. write ( output
)
197 def to_cons_title ( self
, message
):
198 """Set console/terminal window title to message."""
199 if not self
. params
. get ( 'consoletitle' , False ):
201 if os
. name
== 'nt' and ctypes
. windll
. kernel32
. GetConsoleWindow ():
202 # c_wchar_p() might not be necessary if `message` is
203 # already of type unicode()
204 ctypes
. windll
. kernel32
. SetConsoleTitleW ( ctypes
. c_wchar_p ( message
))
205 elif 'TERM' in os
. environ
:
206 sys
. stderr
. write ( ' \033 ]0; %s \007 ' % message
. encode ( preferredencoding ()))
208 def fixed_template ( self
):
209 """Checks if the output template is fixed."""
210 return ( re
. search ( u
'(?u)% \\ (.+? \\ )s' , self
. params
[ 'outtmpl' ]) is None )
212 def trouble ( self
, message
= None ):
213 """Determine action to take when a download problem appears.
215 Depending on if the downloader has been configured to ignore
216 download errors or not, this method may throw an exception or
217 not when errors are found, after printing the message.
219 if message
is not None :
220 self
. to_stderr ( message
)
221 if self
. params
. get ( 'verbose' ):
222 self
. to_stderr ( u
'' . join ( traceback
. format_list ( traceback
. extract_stack ())))
223 if not self
. params
. get ( 'ignoreerrors' , False ):
224 raise DownloadError ( message
)
225 self
._ download
_ retcode
= 1
227 def slow_down ( self
, start_time
, byte_counter
):
228 """Sleep if the download speed is over the rate limit."""
229 rate_limit
= self
. params
. get ( 'ratelimit' , None )
230 if rate_limit
is None or byte_counter
== 0 :
233 elapsed
= now
- start_time
236 speed
= float ( byte_counter
) / elapsed
237 if speed
> rate_limit
:
238 time
. sleep (( byte_counter
- rate_limit
* ( now
- start_time
)) / rate_limit
)
240 def temp_name ( self
, filename
):
241 """Returns a temporary filename for the given filename."""
242 if self
. params
. get ( 'nopart' , False ) or filename
== u
'-' or \
243 ( os
. path
. exists ( encodeFilename ( filename
)) and not os
. path
. isfile ( encodeFilename ( filename
))):
245 return filename
+ u
'.part'
247 def undo_temp_name ( self
, filename
):
248 if filename
. endswith ( u
'.part' ):
249 return filename
[:- len ( u
'.part' )]
252 def try_rename ( self
, old_filename
, new_filename
):
254 if old_filename
== new_filename
:
256 os
. rename ( encodeFilename ( old_filename
), encodeFilename ( new_filename
))
257 except ( IOError , OSError ) as err
:
258 self
. trouble ( u
'ERROR: unable to rename file' )
260 def try_utime ( self
, filename
, last_modified_hdr
):
261 """Try to set the last-modified time of the given file."""
262 if last_modified_hdr
is None :
264 if not os
. path
. isfile ( encodeFilename ( filename
)):
266 timestr
= last_modified_hdr
269 filetime
= timeconvert ( timestr
)
273 os
. utime ( filename
, ( time
. time (), filetime
))
278 def report_writedescription ( self
, descfn
):
279 """ Report that the description file is being written """
280 self
. to_screen ( u
'[info] Writing video description to: ' + descfn
)
282 def report_writesubtitles ( self
, srtfn
):
283 """ Report that the subtitles file is being written """
284 self
. to_screen ( u
'[info] Writing video subtitles to: ' + srtfn
)
286 def report_writeinfojson ( self
, infofn
):
287 """ Report that the metadata file has been written """
288 self
. to_screen ( u
'[info] Video description metadata as JSON to: ' + infofn
)
290 def report_destination ( self
, filename
):
291 """Report destination filename."""
292 self
. to_screen ( u
'[download] Destination: ' + filename
)
294 def report_progress ( self
, percent_str
, data_len_str
, speed_str
, eta_str
):
295 """Report download progress."""
296 if self
. params
. get ( 'noprogress' , False ):
298 self
. to_screen ( u
' \r [download] %s of %s at %s ETA %s ' %
299 ( percent_str
, data_len_str
, speed_str
, eta_str
), skip_eol
= True )
300 self
. to_cons_title ( u
'youtube-dl - %s of %s at %s ETA %s ' %
301 ( percent_str
. strip (), data_len_str
. strip (), speed_str
. strip (), eta_str
. strip ()))
303 def report_resuming_byte ( self
, resume_len
):
304 """Report attempt to resume at given byte."""
305 self
. to_screen ( u
'[download] Resuming download at byte %s ' % resume_len
)
307 def report_retry ( self
, count
, retries
):
308 """Report retry in case of HTTP error 5xx"""
309 self
. to_screen ( u
'[download] Got server HTTP error. Retrying (attempt %d of %d )...' % ( count
, retries
))
311 def report_file_already_downloaded ( self
, file_name
):
312 """Report file has already been fully downloaded."""
314 self
. to_screen ( u
'[download] %s has already been downloaded' % file_name
)
315 except ( UnicodeEncodeError ) as err
:
316 self
. to_screen ( u
'[download] The file has already been downloaded' )
318 def report_unable_to_resume ( self
):
319 """Report it was impossible to resume download."""
320 self
. to_screen ( u
'[download] Unable to resume' )
322 def report_finish ( self
):
323 """Report download finished."""
324 if self
. params
. get ( 'noprogress' , False ):
325 self
. to_screen ( u
'[download] Download completed' )
329 def increment_downloads ( self
):
330 """Increment the ordinal that assigns a number to each file."""
331 self
._ num
_ downloads
+= 1
333 def prepare_filename ( self
, info_dict
):
334 """Generate the output filename."""
336 template_dict
= dict ( info_dict
)
338 template_dict
[ 'epoch' ] = int ( time
. time ())
339 template_dict
[ 'autonumber' ] = u
' %0 5d' % self
._ num
_ downloads
341 sanitize
= lambda k
, v
: sanitize_filename (
342 u
'NA' if v
is None else compat_str ( v
),
343 restricted
= self
. params
. get ( 'restrictfilenames' ),
345 template_dict
= dict (( k
, sanitize ( k
, v
)) for k
, v
in template_dict
. items ())
347 filename
= self
. params
[ 'outtmpl' ] % template_dict
349 except ( ValueError , KeyError ) as err
:
350 self
. trouble ( u
'ERROR: invalid system charset or erroneous output template' )
353 def _match_entry ( self
, info_dict
):
354 """ Returns None iff the file should be downloaded """
356 title
= info_dict
[ 'title' ]
357 matchtitle
= self
. params
. get ( 'matchtitle' , False )
359 matchtitle
= matchtitle
. decode ( 'utf8' )
360 if not re
. search ( matchtitle
, title
, re
. IGNORECASE
):
361 return u
'[download] "' + title
+ '" title did not match pattern "' + matchtitle
+ '"'
362 rejecttitle
= self
. params
. get ( 'rejecttitle' , False )
364 rejecttitle
= rejecttitle
. decode ( 'utf8' )
365 if re
. search ( rejecttitle
, title
, re
. IGNORECASE
):
366 return u
'"' + title
+ '" title matched reject pattern "' + rejecttitle
+ '"'
369 def process_info ( self
, info_dict
):
370 """Process a single dictionary returned by an InfoExtractor."""
372 # Keep for backwards compatibility
373 info_dict
[ 'stitle' ] = info_dict
[ 'title' ]
375 if not 'format' in info_dict
:
376 info_dict
[ 'format' ] = info_dict
[ 'ext' ]
378 reason
= self
._ match
_ entry
( info_dict
)
379 if reason
is not None :
380 self
. to_screen ( u
'[download] ' + reason
)
383 max_downloads
= self
. params
. get ( 'max_downloads' )
384 if max_downloads
is not None :
385 if self
._ num
_ downloads
> int ( max_downloads
):
386 raise MaxDownloadsReached ()
388 filename
= self
. prepare_filename ( info_dict
)
391 if self
. params
. get ( 'forcetitle' , False ):
392 compat_print ( info_dict
[ 'title' ])
393 if self
. params
. get ( 'forceurl' , False ):
394 compat_print ( info_dict
[ 'url' ])
395 if self
. params
. get ( 'forcethumbnail' , False ) and 'thumbnail' in info_dict
:
396 compat_print ( info_dict
[ 'thumbnail' ])
397 if self
. params
. get ( 'forcedescription' , False ) and 'description' in info_dict
:
398 compat_print ( info_dict
[ 'description' ])
399 if self
. params
. get ( 'forcefilename' , False ) and filename
is not None :
400 compat_print ( filename
)
401 if self
. params
. get ( 'forceformat' , False ):
402 compat_print ( info_dict
[ 'format' ])
404 # Do nothing else if in simulate mode
405 if self
. params
. get ( 'simulate' , False ):
412 dn
= os
. path
. dirname ( encodeFilename ( filename
))
413 if dn
!= '' and not os
. path
. exists ( dn
): # dn is already encoded
415 except ( OSError , IOError ) as err
:
416 self
. trouble ( u
'ERROR: unable to create directory ' + compat_str ( err
))
419 if self
. params
. get ( 'writedescription' , False ):
421 descfn
= filename
+ u
'.description'
422 self
. report_writedescription ( descfn
)
423 descfile
= open ( encodeFilename ( descfn
), 'wb' )
425 descfile
. write ( info_dict
[ 'description' ]. encode ( 'utf-8' ))
428 except ( OSError , IOError ):
429 self
. trouble ( u
'ERROR: Cannot write description file ' + descfn
)
432 if self
. params
. get ( 'writesubtitles' , False ) and 'subtitles' in info_dict
and info_dict
[ 'subtitles' ]:
433 # subtitles download errors are already managed as troubles in relevant IE
434 # that way it will silently go on when used with unsupporting IE
436 srtfn
= filename
. rsplit ( '.' , 1 )[ 0 ] + u
'.srt'
437 self
. report_writesubtitles ( srtfn
)
438 srtfile
= open ( encodeFilename ( srtfn
), 'wb' )
440 srtfile
. write ( info_dict
[ 'subtitles' ]. encode ( 'utf-8' ))
443 except ( OSError , IOError ):
444 self
. trouble ( u
'ERROR: Cannot write subtitles file ' + descfn
)
447 if self
. params
. get ( 'writeinfojson' , False ):
448 infofn
= filename
+ u
'.info.json'
449 self
. report_writeinfojson ( infofn
)
452 except ( NameError , AttributeError ):
453 self
. trouble ( u
'ERROR: No JSON encoder found. Update to Python 2.6+, setup a json module, or leave out --write-info-json.' )
456 infof
= open ( encodeFilename ( infofn
), 'wb' )
458 json_info_dict
= dict (( k
, v
) for k
, v
in info_dict
. iteritems () if not k
in ( 'urlhandle' ,))
459 json
. dump ( json_info_dict
, infof
)
462 except ( OSError , IOError ):
463 self
. trouble ( u
'ERROR: Cannot write metadata to JSON file ' + infofn
)
466 if not self
. params
. get ( 'skip_download' , False ):
467 if self
. params
. get ( 'nooverwrites' , False ) and os
. path
. exists ( encodeFilename ( filename
)):
471 success
= self
._ do
_ download
( filename
, info_dict
)
472 except ( OSError , IOError ) as err
:
473 raise UnavailableVideoError ()
474 except ( compat_urllib_error
. URLError
, compat_http_client
. HTTPException
, socket
. error
) as err
:
475 self
. trouble ( u
'ERROR: unable to download video data: %s ' % str ( err
))
477 except ( ContentTooShortError
, ) as err
:
478 self
. trouble ( u
'ERROR: content too short (expected %s bytes and served %s )' % ( err
. expected
, err
. downloaded
))
483 self
. post_process ( filename
, info_dict
)
484 except ( PostProcessingError
) as err
:
485 self
. trouble ( u
'ERROR: postprocessing: %s ' % str ( err
))
488 def download ( self
, url_list
):
489 """Download a given list of URLs."""
490 if len ( url_list
) > 1 and self
. fixed_template ():
491 raise SameFileError ( self
. params
[ 'outtmpl' ])
494 suitable_found
= False
496 # Go to next InfoExtractor if not suitable
497 if not ie
. suitable ( url
):
500 # Warn if the _WORKING attribute is False
502 self
. trouble ( u
'WARNING: the program functionality for this site has been marked as broken, '
503 u
'and will probably not work. If you want to go on, use the -i option.' )
505 # Suitable InfoExtractor found
506 suitable_found
= True
508 # Extract information from URL and process it
509 videos
= ie
. extract ( url
)
510 for video
in videos
or []:
511 video
[ 'extractor' ] = ie
. IE_NAME
513 self
. increment_downloads ()
514 self
. process_info ( video
)
515 except UnavailableVideoError
:
516 self
. trouble ( u
' \n ERROR: unable to download video' )
518 # Suitable InfoExtractor had been found; go to next URL
521 if not suitable_found
:
522 self
. trouble ( u
'ERROR: no suitable InfoExtractor: %s ' % url
)
524 return self
._ download
_ retcode
526 def post_process ( self
, filename
, ie_info
):
527 """Run the postprocessing chain on the given file."""
529 info
[ 'filepath' ] = filename
535 def _download_with_rtmpdump ( self
, filename
, url
, player_url
):
536 self
. report_destination ( filename
)
537 tmpfilename
= self
. temp_name ( filename
)
539 # Check for rtmpdump first
541 subprocess
. call ([ 'rtmpdump' , '-h' ], stdout
=( file ( os
. path
. devnull
, 'w' )), stderr
= subprocess
. STDOUT
)
542 except ( OSError , IOError ):
543 self
. trouble ( u
'ERROR: RTMP download detected but "rtmpdump" could not be run' )
546 # Download using rtmpdump. rtmpdump returns exit code 2 when
547 # the connection was interrumpted and resuming appears to be
548 # possible. This is part of rtmpdump's normal usage, AFAIK.
549 basic_args
= [ 'rtmpdump' , '-q' ] + [[], [ '-W' , player_url
]][ player_url
is not None ] + [ '-r' , url
, '-o' , tmpfilename
]
550 args
= basic_args
+ [[], [ '-e' , '-k' , '1' ]][ self
. params
. get ( 'continuedl' , False )]
551 if self
. params
. get ( 'verbose' , False ):
554 shell_quote
= lambda args
: ' ' . join ( map ( pipes
. quote
, args
))
557 self
. to_screen ( u
'[debug] rtmpdump command line: ' + shell_quote ( args
))
558 retval
= subprocess
. call ( args
)
559 while retval
== 2 or retval
== 1 :
560 prevsize
= os
. path
. getsize ( encodeFilename ( tmpfilename
))
561 self
. to_screen ( u
' \r [rtmpdump] %s bytes' % prevsize
, skip_eol
= True )
562 time
. sleep ( 5.0 ) # This seems to be needed
563 retval
= subprocess
. call ( basic_args
+ [ '-e' ] + [[], [ '-k' , '1' ]][ retval
== 1 ])
564 cursize
= os
. path
. getsize ( encodeFilename ( tmpfilename
))
565 if prevsize
== cursize
and retval
== 1 :
567 # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those
568 if prevsize
== cursize
and retval
== 2 and cursize
> 1024 :
569 self
. to_screen ( u
' \r [rtmpdump] Could not download the whole video. This can happen for some advertisements.' )
573 self
. to_screen ( u
' \r [rtmpdump] %s bytes' % os
. path
. getsize ( encodeFilename ( tmpfilename
)))
574 self
. try_rename ( tmpfilename
, filename
)
577 self
. trouble ( u
' \n ERROR: rtmpdump exited with code %d ' % retval
)
580 def _do_download ( self
, filename
, info_dict
):
581 url
= info_dict
[ 'url' ]
582 player_url
= info_dict
. get ( 'player_url' , None )
584 # Check file already present
585 if self
. params
. get ( 'continuedl' , False ) and os
. path
. isfile ( encodeFilename ( filename
)) and not self
. params
. get ( 'nopart' , False ):
586 self
. report_file_already_downloaded ( filename
)
589 # Attempt to download using rtmpdump
590 if url
. startswith ( 'rtmp' ):
591 return self
._ download
_ with
_ rtmpdump
( filename
, url
, player_url
)
593 tmpfilename
= self
. temp_name ( filename
)
596 # Do not include the Accept-Encoding header
597 headers
= { 'Youtubedl-no-compression' : 'True' }
598 basic_request
= compat_urllib_request
. Request ( url
, None , headers
)
599 request
= compat_urllib_request
. Request ( url
, None , headers
)
601 if self
. params
. get ( 'test' , False ):
602 request
. add_header ( 'Range' , 'bytes=0-10240' )
604 # Establish possible resume length
605 if os
. path
. isfile ( encodeFilename ( tmpfilename
)):
606 resume_len
= os
. path
. getsize ( encodeFilename ( tmpfilename
))
612 if self
. params
. get ( 'continuedl' , False ):
613 self
. report_resuming_byte ( resume_len
)
614 request
. add_header ( 'Range' , 'bytes= %d- ' % resume_len
)
620 retries
= self
. params
. get ( 'retries' , 0 )
621 while count
<= retries
:
622 # Establish connection
624 if count
== 0 and 'urlhandle' in info_dict
:
625 data
= info_dict
[ 'urlhandle' ]
626 data
= compat_urllib_request
. urlopen ( request
)
628 except ( compat_urllib_error
. HTTPError
, ) as err
:
629 if ( err
. code
< 500 or err
. code
>= 600 ) and err
. code
!= 416 :
630 # Unexpected HTTP error
632 elif err
. code
== 416 :
633 # Unable to resume (requested range not satisfiable)
635 # Open the connection again without the range header
636 data
= compat_urllib_request
. urlopen ( basic_request
)
637 content_length
= data
. info ()[ 'Content-Length' ]
638 except ( compat_urllib_error
. HTTPError
, ) as err
:
639 if err
. code
< 500 or err
. code
>= 600 :
642 # Examine the reported length
643 if ( content_length
is not None and
644 ( resume_len
- 100 < int ( content_length
) < resume_len
+ 100 )):
645 # The file had already been fully downloaded.
646 # Explanation to the above condition: in issue #175 it was revealed that
647 # YouTube sometimes adds or removes a few bytes from the end of the file,
648 # changing the file size slightly and causing problems for some users. So
649 # I decided to implement a suggested change and consider the file
650 # completely downloaded if the file size differs less than 100 bytes from
651 # the one in the hard drive.
652 self
. report_file_already_downloaded ( filename
)
653 self
. try_rename ( tmpfilename
, filename
)
656 # The length does not match, we start the download over
657 self
. report_unable_to_resume ()
663 self
. report_retry ( count
, retries
)
666 self
. trouble ( u
'ERROR: giving up after %s retries' % retries
)
669 data_len
= data
. info (). get ( 'Content-length' , None )
670 if data_len
is not None :
671 data_len
= int ( data_len
) + resume_len
672 data_len_str
= self
. format_bytes ( data_len
)
673 byte_counter
= 0 + resume_len
674 block_size
= self
. params
. get ( 'buffersize' , 1024 )
679 data_block
= data
. read ( block_size
)
681 if len ( data_block
) == 0 :
683 byte_counter
+= len ( data_block
)
685 # Open file just in time
688 ( stream
, tmpfilename
) = sanitize_open ( tmpfilename
, open_mode
)
689 assert stream
is not None
690 filename
= self
. undo_temp_name ( tmpfilename
)
691 self
. report_destination ( filename
)
692 except ( OSError , IOError ) as err
:
693 self
. trouble ( u
'ERROR: unable to open for writing: %s ' % str ( err
))
696 stream
. write ( data_block
)
697 except ( IOError , OSError ) as err
:
698 self
. trouble ( u
' \n ERROR: unable to write data: %s ' % str ( err
))
700 if not self
. params
. get ( 'noresizebuffer' , False ):
701 block_size
= self
. best_block_size ( after
- before
, len ( data_block
))
704 speed_str
= self
. calc_speed ( start
, time
. time (), byte_counter
- resume_len
)
706 self
. report_progress ( 'Unknown %' , data_len_str
, speed_str
, 'Unknown ETA' )
708 percent_str
= self
. calc_percent ( byte_counter
, data_len
)
709 eta_str
= self
. calc_eta ( start
, time
. time (), data_len
- resume_len
, byte_counter
- resume_len
)
710 self
. report_progress ( percent_str
, data_len_str
, speed_str
, eta_str
)
713 self
. slow_down ( start
, byte_counter
- resume_len
)
716 self
. trouble ( u
' \n ERROR: Did not get any data blocks' )
720 if data_len
is not None and byte_counter
!= data_len
:
721 raise ContentTooShortError ( byte_counter
, int ( data_len
))
722 self
. try_rename ( tmpfilename
, filename
)
724 # Update file modification time
725 if self
. params
. get ( 'updatetime' , True ):
726 info_dict
[ 'filetime' ] = self
. try_utime ( filename
, data
. info (). get ( 'last-modified' , None ))