]>
Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/YoutubeDL.py
9931c98e9fe02dc3fbcc90df7a01eaf9aa6e7fb9
2 # -*- coding: utf-8 -*-
4 from __future__
import absolute_import
16 from .extractor
import get_info_extractor
17 from .FileDownloader
import FileDownloader
20 class YoutubeDL(object):
23 YoutubeDL objects are the ones responsible of downloading the
24 actual video file and writing it to disk if the user has requested
25 it, among some other tasks. In most cases there should be one per
26 program. As, given a video URL, the downloader doesn't know how to
27 extract all the needed information, task that InfoExtractors do, it
28 has to pass the URL to one of them.
30 For this, YoutubeDL objects have a method that allows
31 InfoExtractors to be registered in a given order. When it is passed
32 a URL, the YoutubeDL object handles it to the first InfoExtractor it
33 finds that reports being able to handle it. The InfoExtractor extracts
34 all the information about the video or videos the URL refers to, and
35 YoutubeDL process the extracted information, possibly using a File
36 Downloader to download the video.
38 YoutubeDL objects accept a lot of parameters. In order not to saturate
39 the object constructor with arguments, it receives a dictionary of
40 options instead. These options are available through the params
41 attribute for the InfoExtractors to use. The YoutubeDL also
42 registers itself as the downloader in charge for the InfoExtractors
43 that are added to it, so this is a "mutual registration".
47 username: Username for authentication purposes.
48 password: Password for authentication purposes.
49 videopassword: Password for acces a video.
50 usenetrc: Use netrc for authentication instead.
51 verbose: Print additional info to stdout.
52 quiet: Do not print messages to stdout.
53 forceurl: Force printing final URL.
54 forcetitle: Force printing title.
55 forceid: Force printing ID.
56 forcethumbnail: Force printing thumbnail URL.
57 forcedescription: Force printing description.
58 forcefilename: Force printing final filename.
59 simulate: Do not download the video files.
60 format: Video format code.
61 format_limit: Highest quality format to try.
62 outtmpl: Template for output names.
63 restrictfilenames: Do not allow "&" and spaces in file names
64 ignoreerrors: Do not stop on download errors.
65 nooverwrites: Prevent overwriting files.
66 playliststart: Playlist item to start at.
67 playlistend: Playlist item to end at.
68 matchtitle: Download only matching titles.
69 rejecttitle: Reject downloads for matching titles.
70 logtostderr: Log messages to stderr instead of stdout.
71 writedescription: Write the video description to a .description file
72 writeinfojson: Write the video description to a .info.json file
73 writethumbnail: Write the thumbnail image to a file
74 writesubtitles: Write the video subtitles to a file
75 writeautomaticsub: Write the automatic subtitles to a file
76 allsubtitles: Downloads all the subtitles of the video
77 listsubtitles: Lists all available subtitles for the video
78 subtitlesformat: Subtitle format [srt/sbv/vtt] (default=srt)
79 subtitleslang: Language of the subtitles to download
80 keepvideo: Keep the video file after post-processing
81 daterange: A DateRange object, download only if the upload_date is in the range.
82 skip_download: Skip the actual download of the video file
84 The following parameters are not used by YoutubeDL itself, they are used by
86 nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
87 noresizebuffer, retries, continuedl, noprogress, consoletitle
93 _download_retcode
= None
97 def __init__(self
, params
):
98 """Create a FileDownloader object with the given options."""
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)]
106 self
.fd
= FileDownloader(self
, self
.params
)
108 if '%(stitle)s' in self
.params
['outtmpl']:
109 self
.report_warning(u
'%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
111 def add_info_extractor(self
, ie
):
112 """Add an InfoExtractor object to the end of the list."""
114 ie
.set_downloader(self
)
116 def add_post_processor(self
, pp
):
117 """Add a PostProcessor object to the end of the chain."""
119 pp
.set_downloader(self
)
121 def to_screen(self
, message
, skip_eol
=False):
122 """Print message to stdout if not in quiet mode."""
123 assert type(message
) == type(u
'')
124 if not self
.params
.get('quiet', False):
125 terminator
= [u
'\n', u
''][skip_eol
]
126 output
= message
+ terminator
127 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
128 output
= output
.encode(preferredencoding(), 'ignore')
129 self
._screen
_file
.write(output
)
130 self
._screen
_file
.flush()
132 def to_stderr(self
, message
):
133 """Print message to stderr."""
134 assert type(message
) == type(u
'')
135 output
= message
+ u
'\n'
136 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
137 output
= output
.encode(preferredencoding())
138 sys
.stderr
.write(output
)
140 def fixed_template(self
):
141 """Checks if the output template is fixed."""
142 return (re
.search(u
'(?u)%\\(.+?\\)s', self
.params
['outtmpl']) is None)
144 def trouble(self
, message
=None, tb
=None):
145 """Determine action to take when a download problem appears.
147 Depending on if the downloader has been configured to ignore
148 download errors or not, this method may throw an exception or
149 not when errors are found, after printing the message.
151 tb, if given, is additional traceback information.
153 if message
is not None:
154 self
.to_stderr(message
)
155 if self
.params
.get('verbose'):
157 if sys
.exc_info()[0]: # if .trouble has been called from an except block
159 if hasattr(sys
.exc_info()[1], 'exc_info') and sys
.exc_info()[1].exc_info
[0]:
160 tb
+= u
''.join(traceback
.format_exception(*sys
.exc_info()[1].exc_info
))
161 tb
+= compat_str(traceback
.format_exc())
163 tb_data
= traceback
.format_list(traceback
.extract_stack())
164 tb
= u
''.join(tb_data
)
166 if not self
.params
.get('ignoreerrors', False):
167 if sys
.exc_info()[0] and hasattr(sys
.exc_info()[1], 'exc_info') and sys
.exc_info()[1].exc_info
[0]:
168 exc_info
= sys
.exc_info()[1].exc_info
170 exc_info
= sys
.exc_info()
171 raise DownloadError(message
, exc_info
)
172 self
._download
_retcode
= 1
174 def report_warning(self
, message
):
176 Print the message to stderr, it will be prefixed with 'WARNING:'
177 If stderr is a tty file the 'WARNING:' will be colored
179 if sys
.stderr
.isatty() and os
.name
!= 'nt':
180 _msg_header
=u
'\033[0;33mWARNING:\033[0m'
182 _msg_header
=u
'WARNING:'
183 warning_message
=u
'%s %s' % (_msg_header
,message
)
184 self
.to_stderr(warning_message
)
186 def report_error(self
, message
, tb
=None):
188 Do the same as trouble, but prefixes the message with 'ERROR:', colored
189 in red if stderr is a tty file.
191 if sys
.stderr
.isatty() and os
.name
!= 'nt':
192 _msg_header
= u
'\033[0;31mERROR:\033[0m'
194 _msg_header
= u
'ERROR:'
195 error_message
= u
'%s %s' % (_msg_header
, message
)
196 self
.trouble(error_message
, tb
)
198 def slow_down(self
, start_time
, byte_counter
):
199 """Sleep if the download speed is over the rate limit."""
200 rate_limit
= self
.params
.get('ratelimit', None)
201 if rate_limit
is None or byte_counter
== 0:
204 elapsed
= now
- start_time
207 speed
= float(byte_counter
) / elapsed
208 if speed
> rate_limit
:
209 time
.sleep((byte_counter
- rate_limit
* (now
- start_time
)) / rate_limit
)
211 def report_writedescription(self
, descfn
):
212 """ Report that the description file is being written """
213 self
.to_screen(u
'[info] Writing video description to: ' + descfn
)
215 def report_writesubtitles(self
, sub_filename
):
216 """ Report that the subtitles file is being written """
217 self
.to_screen(u
'[info] Writing video subtitles to: ' + sub_filename
)
219 def report_writeinfojson(self
, infofn
):
220 """ Report that the metadata file has been written """
221 self
.to_screen(u
'[info] Video description metadata as JSON to: ' + infofn
)
223 def report_file_already_downloaded(self
, file_name
):
224 """Report file has already been fully downloaded."""
226 self
.to_screen(u
'[download] %s has already been downloaded' % file_name
)
227 except (UnicodeEncodeError) as err
:
228 self
.to_screen(u
'[download] The file has already been downloaded')
230 def increment_downloads(self
):
231 """Increment the ordinal that assigns a number to each file."""
232 self
._num
_downloads
+= 1
234 def prepare_filename(self
, info_dict
):
235 """Generate the output filename."""
237 template_dict
= dict(info_dict
)
239 template_dict
['epoch'] = int(time
.time())
240 autonumber_size
= self
.params
.get('autonumber_size')
241 if autonumber_size
is None:
243 autonumber_templ
= u
'%0' + str(autonumber_size
) + u
'd'
244 template_dict
['autonumber'] = autonumber_templ
% self
._num
_downloads
245 if template_dict
['playlist_index'] is not None:
246 template_dict
['playlist_index'] = u
'%05d' % template_dict
['playlist_index']
248 sanitize
= lambda k
,v
: sanitize_filename(
249 u
'NA' if v
is None else compat_str(v
),
250 restricted
=self
.params
.get('restrictfilenames'),
252 template_dict
= dict((k
, sanitize(k
, v
)) for k
,v
in template_dict
.items())
254 filename
= self
.params
['outtmpl'] % template_dict
256 except KeyError as err
:
257 self
.report_error(u
'Erroneous output template')
259 except ValueError as err
:
260 self
.report_error(u
'Insufficient system charset ' + repr(preferredencoding()))
263 def _match_entry(self
, info_dict
):
264 """ Returns None iff the file should be downloaded """
266 title
= info_dict
['title']
267 matchtitle
= self
.params
.get('matchtitle', False)
269 if not re
.search(matchtitle
, title
, re
.IGNORECASE
):
270 return u
'[download] "' + title
+ '" title did not match pattern "' + matchtitle
+ '"'
271 rejecttitle
= self
.params
.get('rejecttitle', False)
273 if re
.search(rejecttitle
, title
, re
.IGNORECASE
):
274 return u
'"' + title
+ '" title matched reject pattern "' + rejecttitle
+ '"'
275 date
= info_dict
.get('upload_date', None)
277 dateRange
= self
.params
.get('daterange', DateRange())
278 if date
not in dateRange
:
279 return u
'[download] %s upload date is not in range %s' % (date_from_str(date
).isoformat(), dateRange
)
282 def extract_info(self
, url
, download
=True, ie_key
=None, extra_info
={}):
284 Returns a list with a dictionary for each video we find.
285 If 'download', also downloads the videos.
286 extra_info is a dict containing the extra values to add to each result
290 ie
= get_info_extractor(ie_key
)()
291 ie
.set_downloader(self
)
297 if not ie
.suitable(url
):
301 self
.report_warning(u
'The program functionality for this site has been marked as broken, '
302 u
'and will probably not work.')
305 ie_result
= ie
.extract(url
)
306 if ie_result
is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
308 if isinstance(ie_result
, list):
309 # Backwards compatibility: old IE result format
310 for result
in ie_result
:
311 result
.update(extra_info
)
313 '_type': 'compat_list',
314 'entries': ie_result
,
317 ie_result
.update(extra_info
)
318 if 'extractor' not in ie_result
:
319 ie_result
['extractor'] = ie
.IE_NAME
320 return self
.process_ie_result(ie_result
, download
=download
)
321 except ExtractorError
as de
: # An error we somewhat expected
322 self
.report_error(compat_str(de
), de
.format_traceback())
324 except Exception as e
:
325 if self
.params
.get('ignoreerrors', False):
326 self
.report_error(compat_str(e
), tb
=compat_str(traceback
.format_exc()))
331 self
.report_error(u
'no suitable InfoExtractor: %s' % url
)
333 def process_ie_result(self
, ie_result
, download
=True, extra_info
={}):
335 Take the result of the ie(may be modified) and resolve all unresolved
336 references (URLs, playlist items).
338 It will also download the videos if 'download'.
339 Returns the resolved ie_result.
342 result_type
= ie_result
.get('_type', 'video') # If not given we suppose it's a video, support the default old system
343 if result_type
== 'video':
344 if 'playlist' not in ie_result
:
345 # It isn't part of a playlist
346 ie_result
['playlist'] = None
347 ie_result
['playlist_index'] = None
349 self
.process_info(ie_result
)
351 elif result_type
== 'url':
352 # We have to add extra_info to the results because it may be
353 # contained in a playlist
354 return self
.extract_info(ie_result
['url'],
356 ie_key
=ie_result
.get('ie_key'),
357 extra_info
=extra_info
)
358 elif result_type
== 'playlist':
359 # We process each entry in the playlist
360 playlist
= ie_result
.get('title', None) or ie_result
.get('id', None)
361 self
.to_screen(u
'[download] Downloading playlist: %s' % playlist
)
363 playlist_results
= []
365 n_all_entries
= len(ie_result
['entries'])
366 playliststart
= self
.params
.get('playliststart', 1) - 1
367 playlistend
= self
.params
.get('playlistend', -1)
369 if playlistend
== -1:
370 entries
= ie_result
['entries'][playliststart
:]
372 entries
= ie_result
['entries'][playliststart
:playlistend
]
374 n_entries
= len(entries
)
376 self
.to_screen(u
"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
377 (ie_result
['extractor'], playlist
, n_all_entries
, n_entries
))
379 for i
,entry
in enumerate(entries
,1):
380 self
.to_screen(u
'[download] Downloading video #%s of %s' %(i
, n_entries
))
382 'playlist': playlist
,
383 'playlist_index': i
+ playliststart
,
385 if not 'extractor' in entry
:
386 # We set the extractor, if it's an url it will be set then to
387 # the new extractor, but if it's already a video we must make
388 # sure it's present: see issue #877
389 entry
['extractor'] = ie_result
['extractor']
390 entry_result
= self
.process_ie_result(entry
,
393 playlist_results
.append(entry_result
)
394 ie_result
['entries'] = playlist_results
396 elif result_type
== 'compat_list':
398 r
.setdefault('extractor', ie_result
['extractor'])
400 ie_result
['entries'] = [
401 self
.process_ie_result(_fixup(r
), download
=download
)
402 for r
in ie_result
['entries']
406 raise Exception('Invalid result type: %s' % result_type
)
408 def process_info(self
, info_dict
):
409 """Process a single resolved IE result."""
411 assert info_dict
.get('_type', 'video') == 'video'
412 #We increment the download the download count here to match the previous behaviour.
413 self
.increment_downloads()
415 info_dict
['fulltitle'] = info_dict
['title']
416 if len(info_dict
['title']) > 200:
417 info_dict
['title'] = info_dict
['title'][:197] + u
'...'
419 # Keep for backwards compatibility
420 info_dict
['stitle'] = info_dict
['title']
422 if not 'format' in info_dict
:
423 info_dict
['format'] = info_dict
['ext']
425 reason
= self
._match
_entry
(info_dict
)
426 if reason
is not None:
427 self
.to_screen(u
'[download] ' + reason
)
430 max_downloads
= self
.params
.get('max_downloads')
431 if max_downloads
is not None:
432 if self
._num
_downloads
> int(max_downloads
):
433 raise MaxDownloadsReached()
435 filename
= self
.prepare_filename(info_dict
)
438 if self
.params
.get('forcetitle', False):
439 compat_print(info_dict
['title'])
440 if self
.params
.get('forceid', False):
441 compat_print(info_dict
['id'])
442 if self
.params
.get('forceurl', False):
443 compat_print(info_dict
['url'])
444 if self
.params
.get('forcethumbnail', False) and 'thumbnail' in info_dict
:
445 compat_print(info_dict
['thumbnail'])
446 if self
.params
.get('forcedescription', False) and 'description' in info_dict
:
447 compat_print(info_dict
['description'])
448 if self
.params
.get('forcefilename', False) and filename
is not None:
449 compat_print(filename
)
450 if self
.params
.get('forceformat', False):
451 compat_print(info_dict
['format'])
453 # Do nothing else if in simulate mode
454 if self
.params
.get('simulate', False):
461 dn
= os
.path
.dirname(encodeFilename(filename
))
462 if dn
!= '' and not os
.path
.exists(dn
):
464 except (OSError, IOError) as err
:
465 self
.report_error(u
'unable to create directory ' + compat_str(err
))
468 if self
.params
.get('writedescription', False):
470 descfn
= filename
+ u
'.description'
471 self
.report_writedescription(descfn
)
472 with io
.open(encodeFilename(descfn
), 'w', encoding
='utf-8') as descfile
:
473 descfile
.write(info_dict
['description'])
474 except (OSError, IOError):
475 self
.report_error(u
'Cannot write description file ' + descfn
)
478 if (self
.params
.get('writesubtitles', False) or self
.params
.get('writeautomaticsub')) and 'subtitles' in info_dict
and info_dict
['subtitles']:
479 # subtitles download errors are already managed as troubles in relevant IE
480 # that way it will silently go on when used with unsupporting IE
481 subtitle
= info_dict
['subtitles'][0]
482 (sub_error
, sub_lang
, sub
) = subtitle
483 sub_format
= self
.params
.get('subtitlesformat')
485 self
.report_warning("Some error while getting the subtitles")
488 sub_filename
= filename
.rsplit('.', 1)[0] + u
'.' + sub_lang
+ u
'.' + sub_format
489 self
.report_writesubtitles(sub_filename
)
490 with io
.open(encodeFilename(sub_filename
), 'w', encoding
='utf-8') as subfile
:
492 except (OSError, IOError):
493 self
.report_error(u
'Cannot write subtitles file ' + descfn
)
496 if self
.params
.get('allsubtitles', False) and 'subtitles' in info_dict
and info_dict
['subtitles']:
497 subtitles
= info_dict
['subtitles']
498 sub_format
= self
.params
.get('subtitlesformat')
499 for subtitle
in subtitles
:
500 (sub_error
, sub_lang
, sub
) = subtitle
502 self
.report_warning("Some error while getting the subtitles")
505 sub_filename
= filename
.rsplit('.', 1)[0] + u
'.' + sub_lang
+ u
'.' + sub_format
506 self
.report_writesubtitles(sub_filename
)
507 with io
.open(encodeFilename(sub_filename
), 'w', encoding
='utf-8') as subfile
:
509 except (OSError, IOError):
510 self
.report_error(u
'Cannot write subtitles file ' + descfn
)
513 if self
.params
.get('writeinfojson', False):
514 infofn
= filename
+ u
'.info.json'
515 self
.report_writeinfojson(infofn
)
517 json_info_dict
= dict((k
, v
) for k
,v
in info_dict
.items() if not k
in ['urlhandle'])
518 write_json_file(json_info_dict
, encodeFilename(infofn
))
519 except (OSError, IOError):
520 self
.report_error(u
'Cannot write metadata to JSON file ' + infofn
)
523 if self
.params
.get('writethumbnail', False):
524 if 'thumbnail' in info_dict
:
525 thumb_format
= info_dict
['thumbnail'].rpartition(u
'/')[2].rpartition(u
'.')[2]
528 thumb_filename
= filename
.rpartition('.')[0] + u
'.' + thumb_format
529 self
.to_screen(u
'[%s] %s: Downloading thumbnail ...' %
530 (info_dict
['extractor'], info_dict
['id']))
531 uf
= compat_urllib_request
.urlopen(info_dict
['thumbnail'])
532 with open(thumb_filename
, 'wb') as thumbf
:
533 shutil
.copyfileobj(uf
, thumbf
)
534 self
.to_screen(u
'[%s] %s: Writing thumbnail to: %s' %
535 (info_dict
['extractor'], info_dict
['id'], thumb_filename
))
537 if not self
.params
.get('skip_download', False):
538 if self
.params
.get('nooverwrites', False) and os
.path
.exists(encodeFilename(filename
)):
542 success
= self
.fd
._do
_download
(filename
, info_dict
)
543 except (OSError, IOError) as err
:
544 raise UnavailableVideoError()
545 except (compat_urllib_error
.URLError
, compat_http_client
.HTTPException
, socket
.error
) as err
:
546 self
.report_error(u
'unable to download video data: %s' % str(err
))
548 except (ContentTooShortError
, ) as err
:
549 self
.report_error(u
'content too short (expected %s bytes and served %s)' % (err
.expected
, err
.downloaded
))
554 self
.post_process(filename
, info_dict
)
555 except (PostProcessingError
) as err
:
556 self
.report_error(u
'postprocessing: %s' % str(err
))
559 def download(self
, url_list
):
560 """Download a given list of URLs."""
561 if len(url_list
) > 1 and self
.fixed_template():
562 raise SameFileError(self
.params
['outtmpl'])
566 #It also downloads the videos
567 videos
= self
.extract_info(url
)
568 except UnavailableVideoError
:
569 self
.report_error(u
'unable to download video')
570 except MaxDownloadsReached
:
571 self
.to_screen(u
'[info] Maximum number of downloaded files reached.')
574 return self
._download
_retcode
576 def post_process(self
, filename
, ie_info
):
577 """Run all the postprocessors on the given file."""
579 info
['filepath'] = filename
583 keep_video_wish
,new_info
= pp
.run(info
)
584 if keep_video_wish
is not None:
586 keep_video
= keep_video_wish
587 elif keep_video
is None:
588 # No clear decision yet, let IE decide
589 keep_video
= keep_video_wish
590 except PostProcessingError
as e
:
591 self
.to_stderr(u
'ERROR: ' + e
.msg
)
592 if keep_video
is False and not self
.params
.get('keepvideo', False):
594 self
.to_screen(u
'Deleting original file %s (pass -k to keep)' % filename
)
595 os
.remove(encodeFilename(filename
))
596 except (IOError, OSError):
597 self
.report_warning(u
'Unable to remove downloaded video file')