2 # -*- coding: utf-8 -*-
4 from __future__ import absolute_import, unicode_literals
33 compat_urllib_request,
59 UnavailableVideoError,
68 from .cache import Cache
69 from .extractor import get_info_extractor, gen_extractors
70 from .downloader import get_suitable_downloader
71 from .downloader.rtmp import rtmpdump_version
72 from .postprocessor import (
77 from .version import __version__
80 class YoutubeDL(object):
83 YoutubeDL objects are the ones responsible of downloading the
84 actual video file and writing it to disk if the user has requested
85 it, among some other tasks. In most cases there should be one per
86 program. As, given a video URL, the downloader doesn't know how to
87 extract all the needed information, task that InfoExtractors do, it
88 has to pass the URL to one of them.
90 For this, YoutubeDL objects have a method that allows
91 InfoExtractors to be registered in a given order. When it is passed
92 a URL, the YoutubeDL object handles it to the first InfoExtractor it
93 finds that reports being able to handle it. The InfoExtractor extracts
94 all the information about the video or videos the URL refers to, and
95 YoutubeDL process the extracted information, possibly using a File
96 Downloader to download the video.
98 YoutubeDL objects accept a lot of parameters. In order not to saturate
99 the object constructor with arguments, it receives a dictionary of
100 options instead. These options are available through the params
101 attribute for the InfoExtractors to use. The YoutubeDL also
102 registers itself as the downloader in charge for the InfoExtractors
103 that are added to it, so this is a "mutual registration".
107 username: Username for authentication purposes.
108 password: Password for authentication purposes.
109 videopassword: Password for acces a video.
110 usenetrc: Use netrc for authentication instead.
111 verbose: Print additional info to stdout.
112 quiet: Do not print messages to stdout.
113 no_warnings: Do not print out anything for warnings.
114 forceurl: Force printing final URL.
115 forcetitle: Force printing title.
116 forceid: Force printing ID.
117 forcethumbnail: Force printing thumbnail URL.
118 forcedescription: Force printing description.
119 forcefilename: Force printing final filename.
120 forceduration: Force printing duration.
121 forcejson: Force printing info_dict as JSON.
122 dump_single_json: Force printing the info_dict of the whole playlist
123 (or video) as a single JSON line.
124 simulate: Do not download the video files.
125 format: Video format code. See options.py for more information.
126 format_limit: Highest quality format to try.
127 outtmpl: Template for output names.
128 restrictfilenames: Do not allow "&" and spaces in file names
129 ignoreerrors: Do not stop on download errors.
130 nooverwrites: Prevent overwriting files.
131 playliststart: Playlist item to start at.
132 playlistend: Playlist item to end at.
133 playlistreverse: Download playlist items in reverse order.
134 matchtitle: Download only matching titles.
135 rejecttitle: Reject downloads for matching titles.
136 logger: Log messages to a logging.Logger instance.
137 logtostderr: Log messages to stderr instead of stdout.
138 writedescription: Write the video description to a .description file
139 writeinfojson: Write the video description to a .info.json file
140 writeannotations: Write the video annotations to a .annotations.xml file
141 writethumbnail: Write the thumbnail image to a file
142 writesubtitles: Write the video subtitles to a file
143 writeautomaticsub: Write the automatic subtitles to a file
144 allsubtitles: Downloads all the subtitles of the video
145 (requires writesubtitles or writeautomaticsub)
146 listsubtitles: Lists all available subtitles for the video
147 subtitlesformat: Subtitle format [srt/sbv/vtt] (default=srt)
148 subtitleslangs: List of languages of the subtitles to download
149 keepvideo: Keep the video file after post-processing
150 daterange: A DateRange object, download only if the upload_date is in the range.
151 skip_download: Skip the actual download of the video file
152 cachedir: Location of the cache files in the filesystem.
153 False to disable filesystem cache.
154 noplaylist: Download single video instead of a playlist if in doubt.
155 age_limit: An integer representing the user's age in years.
156 Unsuitable videos for the given age are skipped.
157 min_views: An integer representing the minimum view count the video
158 must have in order to not be skipped.
159 Videos without view count information are always
160 downloaded. None for no limit.
161 max_views: An integer representing the maximum view count.
162 Videos that are more popular than that are not
164 Videos without view count information are always
165 downloaded. None for no limit.
166 download_archive: File name of a file where all downloads are recorded.
167 Videos already present in the file are not downloaded
169 cookiefile: File name where cookies should be read from and dumped to.
170 nocheckcertificate:Do not verify SSL certificates
171 prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
172 At the moment, this is only supported by YouTube.
173 proxy: URL of the proxy server to use
174 socket_timeout: Time to wait for unresponsive hosts, in seconds
175 bidi_workaround: Work around buggy terminals without bidirectional text
176 support, using fridibi
177 debug_printtraffic:Print out sent and received HTTP traffic
178 include_ads: Download ads as well
179 default_search: Prepend this string if an input url is not valid.
180 'auto' for elaborate guessing
181 encoding: Use this encoding instead of the system-specified.
182 extract_flat: Do not resolve URLs, return the immediate result.
183 Pass in 'in_playlist' to only show this behavior for
185 postprocessors: A list of dictionaries, each with an entry
186 * key: The name of the postprocessor. See
187 youtube_dl/postprocessor/__init__.py for a list.
188 as well as any further keyword arguments for the
190 progress_hooks: A list of functions that get called on download
191 progress, with a dictionary with the entries
192 * filename: The final filename
193 * status: One of "downloading" and "finished"
195 The dict may also have some of the following entries:
197 * downloaded_bytes: Bytes on disk
198 * total_bytes: Size of the whole file, None if unknown
199 * tmpfilename: The filename we're currently writing to
200 * eta: The estimated time in seconds, None if unknown
201 * speed: The download speed in bytes/second, None if
204 Progress hooks are guaranteed to be called at least once
205 (with status "finished") if the download is successful.
206 merge_output_format: Extension to use when merging formats.
209 The following parameters are not used by YoutubeDL itself, they are used by
211 nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
212 noresizebuffer, retries, continuedl, noprogress, consoletitle
214 The following options are used by the post processors:
215 prefer_ffmpeg: If True, use ffmpeg instead of avconv if both are available,
216 otherwise prefer avconv.
217 exec_cmd: Arbitrary command to run after downloading
223 _download_retcode = None
224 _num_downloads = None
227 def __init__(self, params=None, auto_init=True):
228 """Create a FileDownloader object with the given options."""
232 self._ies_instances = {}
234 self._progress_hooks = []
235 self._download_retcode = 0
236 self._num_downloads = 0
237 self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
238 self._err_file = sys.stderr
240 self.cache = Cache(self)
242 if params.get('bidi_workaround', False):
245 master, slave = pty.openpty()
246 width = get_term_width()
250 width_args = ['-w', str(width)]
252 stdin=subprocess.PIPE,
254 stderr=self._err_file)
256 self._output_process = subprocess.Popen(
257 ['bidiv'] + width_args, **sp_kwargs
260 self._output_process = subprocess.Popen(
261 ['fribidi', '-c', 'UTF-8'] + width_args, **sp_kwargs)
262 self._output_channel = os.fdopen(master, 'rb')
263 except OSError as ose:
265 self.report_warning('Could not find fribidi executable, ignoring --bidi-workaround . Make sure that fribidi is an executable file in one of the directories in your $PATH.')
269 if (sys.version_info >= (3,) and sys.platform != 'win32' and
270 sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
271 and not params.get('restrictfilenames', False)):
272 # On Python 3, the Unicode filesystem API will throw errors (#1474)
274 'Assuming --restrict-filenames since file system encoding '
275 'cannot encode all characters. '
276 'Set the LC_ALL environment variable to fix this.')
277 self.params['restrictfilenames'] = True
279 if '%(stitle)s' in self.params.get('outtmpl', ''):
280 self.report_warning('%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
285 self.print_debug_header()
286 self.add_default_info_extractors()
288 for pp_def_raw in self.params.get('postprocessors', []):
289 pp_class = get_postprocessor(pp_def_raw['key'])
290 pp_def = dict(pp_def_raw)
292 pp = pp_class(self, **compat_kwargs(pp_def))
293 self.add_post_processor(pp)
295 for ph in self.params.get('progress_hooks', []):
296 self.add_progress_hook(ph)
298 def warn_if_short_id(self, argv):
299 # short YouTube ID starting with dash?
301 i for i, a in enumerate(argv)
302 if re.match(r'^-[0-9A-Za-z_-]{10}$', a)]
306 [a for i, a in enumerate(argv) if i not in idxs] +
307 ['--'] + [argv[i] for i in idxs]
310 'Long argument string detected. '
311 'Use -- to separate parameters and URLs, like this:\n%s\n' %
312 args_to_str(correct_argv))
314 def add_info_extractor(self, ie):
315 """Add an InfoExtractor object to the end of the list."""
317 self._ies_instances[ie.ie_key()] = ie
318 ie.set_downloader(self)
320 def get_info_extractor(self, ie_key):
322 Get an instance of an IE with name ie_key, it will try to get one from
323 the _ies list, if there's no instance it will create a new one and add
324 it to the extractor list.
326 ie = self._ies_instances.get(ie_key)
328 ie = get_info_extractor(ie_key)()
329 self.add_info_extractor(ie)
332 def add_default_info_extractors(self):
334 Add the InfoExtractors returned by gen_extractors to the end of the list
336 for ie in gen_extractors():
337 self.add_info_extractor(ie)
339 def add_post_processor(self, pp):
340 """Add a PostProcessor object to the end of the chain."""
342 pp.set_downloader(self)
344 def add_progress_hook(self, ph):
345 """Add the progress hook (currently only for the file downloader)"""
346 self._progress_hooks.append(ph)
348 def _bidi_workaround(self, message):
349 if not hasattr(self, '_output_channel'):
352 assert hasattr(self, '_output_process')
353 assert isinstance(message, compat_str)
354 line_count = message.count('\n') + 1
355 self._output_process.stdin.write((message + '\n').encode('utf-8'))
356 self._output_process.stdin.flush()
357 res = ''.join(self._output_channel.readline().decode('utf-8')
358 for _ in range(line_count))
359 return res[:-len('\n')]
361 def to_screen(self, message, skip_eol=False):
362 """Print message to stdout if not in quiet mode."""
363 return self.to_stdout(message, skip_eol, check_quiet=True)
365 def _write_string(self, s, out=None):
366 write_string(s, out=out, encoding=self.params.get('encoding'))
368 def to_stdout(self, message, skip_eol=False, check_quiet=False):
369 """Print message to stdout if not in quiet mode."""
370 if self.params.get('logger'):
371 self.params['logger'].debug(message)
372 elif not check_quiet or not self.params.get('quiet', False):
373 message = self._bidi_workaround(message)
374 terminator = ['\n', ''][skip_eol]
375 output = message + terminator
377 self._write_string(output, self._screen_file)
379 def to_stderr(self, message):
380 """Print message to stderr."""
381 assert isinstance(message, compat_str)
382 if self.params.get('logger'):
383 self.params['logger'].error(message)
385 message = self._bidi_workaround(message)
386 output = message + '\n'
387 self._write_string(output, self._err_file)
389 def to_console_title(self, message):
390 if not self.params.get('consoletitle', False):
392 if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
393 # c_wchar_p() might not be necessary if `message` is
394 # already of type unicode()
395 ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
396 elif 'TERM' in os.environ:
397 self._write_string('\033]0;%s\007' % message, self._screen_file)
399 def save_console_title(self):
400 if not self.params.get('consoletitle', False):
402 if 'TERM' in os.environ:
403 # Save the title on stack
404 self._write_string('\033[22;0t', self._screen_file)
406 def restore_console_title(self):
407 if not self.params.get('consoletitle', False):
409 if 'TERM' in os.environ:
410 # Restore the title from stack
411 self._write_string('\033[23;0t', self._screen_file)
414 self.save_console_title()
417 def __exit__(self, *args):
418 self.restore_console_title()
420 if self.params.get('cookiefile') is not None:
421 self.cookiejar.save()
423 def trouble(self, message=None, tb=None):
424 """Determine action to take when a download problem appears.
426 Depending on if the downloader has been configured to ignore
427 download errors or not, this method may throw an exception or
428 not when errors are found, after printing the message.
430 tb, if given, is additional traceback information.
432 if message is not None:
433 self.to_stderr(message)
434 if self.params.get('verbose'):
436 if sys.exc_info()[0]: # if .trouble has been called from an except block
438 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
439 tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
440 tb += compat_str(traceback.format_exc())
442 tb_data = traceback.format_list(traceback.extract_stack())
443 tb = ''.join(tb_data)
445 if not self.params.get('ignoreerrors', False):
446 if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
447 exc_info = sys.exc_info()[1].exc_info
449 exc_info = sys.exc_info()
450 raise DownloadError(message, exc_info)
451 self._download_retcode = 1
453 def report_warning(self, message):
455 Print the message to stderr, it will be prefixed with 'WARNING:'
456 If stderr is a tty file the 'WARNING:' will be colored
458 if self.params.get('logger') is not None:
459 self.params['logger'].warning(message)
461 if self.params.get('no_warnings'):
463 if self._err_file.isatty() and os.name != 'nt':
464 _msg_header = '\033[0;33mWARNING:\033[0m'
466 _msg_header = 'WARNING:'
467 warning_message = '%s %s' % (_msg_header, message)
468 self.to_stderr(warning_message)
470 def report_error(self, message, tb=None):
472 Do the same as trouble, but prefixes the message with 'ERROR:', colored
473 in red if stderr is a tty file.
475 if self._err_file.isatty() and os.name != 'nt':
476 _msg_header = '\033[0;31mERROR:\033[0m'
478 _msg_header = 'ERROR:'
479 error_message = '%s %s' % (_msg_header, message)
480 self.trouble(error_message, tb)
482 def report_file_already_downloaded(self, file_name):
483 """Report file has already been fully downloaded."""
485 self.to_screen('[download] %s has already been downloaded' % file_name)
486 except UnicodeEncodeError:
487 self.to_screen('[download] The file has already been downloaded')
489 def prepare_filename(self, info_dict):
490 """Generate the output filename."""
492 template_dict = dict(info_dict)
494 template_dict['epoch'] = int(time.time())
495 autonumber_size = self.params.get('autonumber_size')
496 if autonumber_size is None:
498 autonumber_templ = '%0' + str(autonumber_size) + 'd'
499 template_dict['autonumber'] = autonumber_templ % self._num_downloads
500 if template_dict.get('playlist_index') is not None:
501 template_dict['playlist_index'] = '%0*d' % (len(str(template_dict['n_entries'])), template_dict['playlist_index'])
502 if template_dict.get('resolution') is None:
503 if template_dict.get('width') and template_dict.get('height'):
504 template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
505 elif template_dict.get('height'):
506 template_dict['resolution'] = '%sp' % template_dict['height']
507 elif template_dict.get('width'):
508 template_dict['resolution'] = '?x%d' % template_dict['width']
510 sanitize = lambda k, v: sanitize_filename(
512 restricted=self.params.get('restrictfilenames'),
514 template_dict = dict((k, sanitize(k, v))
515 for k, v in template_dict.items()
517 template_dict = collections.defaultdict(lambda: 'NA', template_dict)
519 outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
520 tmpl = compat_expanduser(outtmpl)
521 filename = tmpl % template_dict
523 except ValueError as err:
524 self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
527 def _match_entry(self, info_dict):
528 """ Returns None iff the file should be downloaded """
530 video_title = info_dict.get('title', info_dict.get('id', 'video'))
531 if 'title' in info_dict:
532 # This can happen when we're just evaluating the playlist
533 title = info_dict['title']
534 matchtitle = self.params.get('matchtitle', False)
536 if not re.search(matchtitle, title, re.IGNORECASE):
537 return '"' + title + '" title did not match pattern "' + matchtitle + '"'
538 rejecttitle = self.params.get('rejecttitle', False)
540 if re.search(rejecttitle, title, re.IGNORECASE):
541 return '"' + title + '" title matched reject pattern "' + rejecttitle + '"'
542 date = info_dict.get('upload_date', None)
544 dateRange = self.params.get('daterange', DateRange())
545 if date not in dateRange:
546 return '%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
547 view_count = info_dict.get('view_count', None)
548 if view_count is not None:
549 min_views = self.params.get('min_views')
550 if min_views is not None and view_count < min_views:
551 return 'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
552 max_views = self.params.get('max_views')
553 if max_views is not None and view_count > max_views:
554 return 'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
555 if age_restricted(info_dict.get('age_limit'), self.params.get('age_limit')):
556 return 'Skipping "%s" because it is age restricted' % title
557 if self.in_download_archive(info_dict):
558 return '%s has already been recorded in archive' % video_title
562 def add_extra_info(info_dict, extra_info):
563 '''Set the keys from extra_info in info dict if they are missing'''
564 for key, value in extra_info.items():
565 info_dict.setdefault(key, value)
567 def extract_info(self, url, download=True, ie_key=None, extra_info={},
570 Returns a list with a dictionary for each video we find.
571 If 'download', also downloads the videos.
572 extra_info is a dict containing the extra values to add to each result
576 ies = [self.get_info_extractor(ie_key)]
581 if not ie.suitable(url):
585 self.report_warning('The program functionality for this site has been marked as broken, '
586 'and will probably not work.')
589 ie_result = ie.extract(url)
590 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
592 if isinstance(ie_result, list):
593 # Backwards compatibility: old IE result format
595 '_type': 'compat_list',
596 'entries': ie_result,
598 self.add_default_extra_info(ie_result, ie, url)
600 return self.process_ie_result(ie_result, download, extra_info)
603 except ExtractorError as de: # An error we somewhat expected
604 self.report_error(compat_str(de), de.format_traceback())
606 except MaxDownloadsReached:
608 except Exception as e:
609 if self.params.get('ignoreerrors', False):
610 self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
615 self.report_error('no suitable InfoExtractor for URL %s' % url)
617 def add_default_extra_info(self, ie_result, ie, url):
618 self.add_extra_info(ie_result, {
619 'extractor': ie.IE_NAME,
621 'webpage_url_basename': url_basename(url),
622 'extractor_key': ie.ie_key(),
625 def process_ie_result(self, ie_result, download=True, extra_info={}):
627 Take the result of the ie(may be modified) and resolve all unresolved
628 references (URLs, playlist items).
630 It will also download the videos if 'download'.
631 Returns the resolved ie_result.
634 result_type = ie_result.get('_type', 'video')
636 if result_type in ('url', 'url_transparent'):
637 extract_flat = self.params.get('extract_flat', False)
638 if ((extract_flat == 'in_playlist' and 'playlist' in extra_info) or
639 extract_flat is True):
640 if self.params.get('forcejson', False):
641 self.to_stdout(json.dumps(ie_result))
644 if result_type == 'video':
645 self.add_extra_info(ie_result, extra_info)
646 return self.process_video_result(ie_result, download=download)
647 elif result_type == 'url':
648 # We have to add extra_info to the results because it may be
649 # contained in a playlist
650 return self.extract_info(ie_result['url'],
652 ie_key=ie_result.get('ie_key'),
653 extra_info=extra_info)
654 elif result_type == 'url_transparent':
655 # Use the information from the embedding page
656 info = self.extract_info(
657 ie_result['url'], ie_key=ie_result.get('ie_key'),
658 extra_info=extra_info, download=False, process=False)
660 force_properties = dict(
661 (k, v) for k, v in ie_result.items() if v is not None)
662 for f in ('_type', 'url'):
663 if f in force_properties:
664 del force_properties[f]
665 new_result = info.copy()
666 new_result.update(force_properties)
668 assert new_result.get('_type') != 'url_transparent'
670 return self.process_ie_result(
671 new_result, download=download, extra_info=extra_info)
672 elif result_type == 'playlist' or result_type == 'multi_video':
673 # We process each entry in the playlist
674 playlist = ie_result.get('title', None) or ie_result.get('id', None)
675 self.to_screen('[download] Downloading playlist: %s' % playlist)
677 playlist_results = []
679 playliststart = self.params.get('playliststart', 1) - 1
680 playlistend = self.params.get('playlistend', None)
681 # For backwards compatibility, interpret -1 as whole list
682 if playlistend == -1:
685 ie_entries = ie_result['entries']
686 if isinstance(ie_entries, list):
687 n_all_entries = len(ie_entries)
688 entries = ie_entries[playliststart:playlistend]
689 n_entries = len(entries)
691 "[%s] playlist %s: Collected %d video ids (downloading %d of them)" %
692 (ie_result['extractor'], playlist, n_all_entries, n_entries))
693 elif isinstance(ie_entries, PagedList):
694 entries = ie_entries.getslice(
695 playliststart, playlistend)
696 n_entries = len(entries)
698 "[%s] playlist %s: Downloading %d videos" %
699 (ie_result['extractor'], playlist, n_entries))
701 entries = list(itertools.islice(
702 ie_entries, playliststart, playlistend))
703 n_entries = len(entries)
705 "[%s] playlist %s: Downloading %d videos" %
706 (ie_result['extractor'], playlist, n_entries))
708 if self.params.get('playlistreverse', False):
709 entries = entries[::-1]
711 for i, entry in enumerate(entries, 1):
712 self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
714 'n_entries': n_entries,
715 'playlist': playlist,
716 'playlist_id': ie_result.get('id'),
717 'playlist_title': ie_result.get('title'),
718 'playlist_index': i + playliststart,
719 'extractor': ie_result['extractor'],
720 'webpage_url': ie_result['webpage_url'],
721 'webpage_url_basename': url_basename(ie_result['webpage_url']),
722 'extractor_key': ie_result['extractor_key'],
725 reason = self._match_entry(entry)
726 if reason is not None:
727 self.to_screen('[download] ' + reason)
730 entry_result = self.process_ie_result(entry,
733 playlist_results.append(entry_result)
734 ie_result['entries'] = playlist_results
736 elif result_type == 'compat_list':
738 'Extractor %s returned a compat_list result. '
739 'It needs to be updated.' % ie_result.get('extractor'))
745 'extractor': ie_result['extractor'],
746 'webpage_url': ie_result['webpage_url'],
747 'webpage_url_basename': url_basename(ie_result['webpage_url']),
748 'extractor_key': ie_result['extractor_key'],
752 ie_result['entries'] = [
753 self.process_ie_result(_fixup(r), download, extra_info)
754 for r in ie_result['entries']
758 raise Exception('Invalid result type: %s' % result_type)
760 def select_format(self, format_spec, available_formats):
761 if format_spec == 'best' or format_spec is None:
762 return available_formats[-1]
763 elif format_spec == 'worst':
764 return available_formats[0]
765 elif format_spec == 'bestaudio':
767 f for f in available_formats
768 if f.get('vcodec') == 'none']
770 return audio_formats[-1]
771 elif format_spec == 'worstaudio':
773 f for f in available_formats
774 if f.get('vcodec') == 'none']
776 return audio_formats[0]
777 elif format_spec == 'bestvideo':
779 f for f in available_formats
780 if f.get('acodec') == 'none']
782 return video_formats[-1]
783 elif format_spec == 'worstvideo':
785 f for f in available_formats
786 if f.get('acodec') == 'none']
788 return video_formats[0]
790 extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
791 if format_spec in extensions:
792 filter_f = lambda f: f['ext'] == format_spec
794 filter_f = lambda f: f['format_id'] == format_spec
795 matches = list(filter(filter_f, available_formats))
800 def process_video_result(self, info_dict, download=True):
801 assert info_dict.get('_type', 'video') == 'video'
803 if 'id' not in info_dict:
804 raise ExtractorError('Missing "id" field in extractor result')
805 if 'title' not in info_dict:
806 raise ExtractorError('Missing "title" field in extractor result')
808 if 'playlist' not in info_dict:
809 # It isn't part of a playlist
810 info_dict['playlist'] = None
811 info_dict['playlist_index'] = None
813 thumbnails = info_dict.get('thumbnails')
815 thumbnails.sort(key=lambda t: (
816 t.get('width'), t.get('height'), t.get('url')))
818 if 'width' in t and 'height' in t:
819 t['resolution'] = '%dx%d' % (t['width'], t['height'])
821 if thumbnails and 'thumbnail' not in info_dict:
822 info_dict['thumbnail'] = thumbnails[-1]['url']
824 if 'display_id' not in info_dict and 'id' in info_dict:
825 info_dict['display_id'] = info_dict['id']
827 if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
828 # Working around negative timestamps in Windows
829 # (see http://bugs.python.org/issue1646728)
830 if info_dict['timestamp'] < 0 and os.name == 'nt':
831 info_dict['timestamp'] = 0
832 upload_date = datetime.datetime.utcfromtimestamp(
833 info_dict['timestamp'])
834 info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
836 # This extractors handle format selection themselves
837 if info_dict['extractor'] in ['Youku']:
839 self.process_info(info_dict)
842 # We now pick which formats have to be downloaded
843 if info_dict.get('formats') is None:
844 # There's only one format available
845 formats = [info_dict]
847 formats = info_dict['formats']
850 raise ExtractorError('No video formats found!')
852 # We check that all the formats have the format and format_id fields
853 for i, format in enumerate(formats):
854 if 'url' not in format:
855 raise ExtractorError('Missing "url" key in result (index %d)' % i)
857 if format.get('format_id') is None:
858 format['format_id'] = compat_str(i)
859 if format.get('format') is None:
860 format['format'] = '{id} - {res}{note}'.format(
861 id=format['format_id'],
862 res=self.format_resolution(format),
863 note=' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
865 # Automatically determine file extension if missing
866 if 'ext' not in format:
867 format['ext'] = determine_ext(format['url']).lower()
869 format_limit = self.params.get('format_limit', None)
871 formats = list(takewhile_inclusive(
872 lambda f: f['format_id'] != format_limit, formats
875 # TODO Central sorting goes here
877 if formats[0] is not info_dict:
878 # only set the 'formats' fields if the original info_dict list them
879 # otherwise we end up with a circular reference, the first (and unique)
880 # element in the 'formats' field in info_dict is info_dict itself,
881 # wich can't be exported to json
882 info_dict['formats'] = formats
883 if self.params.get('listformats', None):
884 self.list_formats(info_dict)
887 req_format = self.params.get('format')
888 if req_format is None:
890 formats_to_download = []
891 # The -1 is for supporting YoutubeIE
892 if req_format in ('-1', 'all'):
893 formats_to_download = formats
895 for rfstr in req_format.split(','):
896 # We can accept formats requested in the format: 34/5/best, we pick
897 # the first that is available, starting from left
898 req_formats = rfstr.split('/')
899 for rf in req_formats:
900 if re.match(r'.+?\+.+?', rf) is not None:
901 # Two formats have been requested like '137+139'
902 format_1, format_2 = rf.split('+')
903 formats_info = (self.select_format(format_1, formats),
904 self.select_format(format_2, formats))
905 if all(formats_info):
906 # The first format must contain the video and the
908 if formats_info[0].get('vcodec') == 'none':
909 self.report_error('The first format must '
910 'contain the video, try using '
911 '"-f %s+%s"' % (format_2, format_1))
914 formats_info[0]['ext']
915 if self.params.get('merge_output_format') is None
916 else self.params['merge_output_format'])
918 'requested_formats': formats_info,
920 'ext': formats_info[0]['ext'],
921 'width': formats_info[0].get('width'),
922 'height': formats_info[0].get('height'),
923 'resolution': formats_info[0].get('resolution'),
924 'fps': formats_info[0].get('fps'),
925 'vcodec': formats_info[0].get('vcodec'),
926 'vbr': formats_info[0].get('vbr'),
927 'acodec': formats_info[1].get('acodec'),
928 'abr': formats_info[1].get('abr'),
932 selected_format = None
934 selected_format = self.select_format(rf, formats)
935 if selected_format is not None:
936 formats_to_download.append(selected_format)
938 if not formats_to_download:
939 raise ExtractorError('requested format not available',
943 if len(formats_to_download) > 1:
944 self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
945 for format in formats_to_download:
946 new_info = dict(info_dict)
947 new_info.update(format)
948 self.process_info(new_info)
949 # We update the info dict with the best quality format (backwards compatibility)
950 info_dict.update(formats_to_download[-1])
953 def process_info(self, info_dict):
954 """Process a single resolved IE result."""
956 assert info_dict.get('_type', 'video') == 'video'
958 max_downloads = self.params.get('max_downloads')
959 if max_downloads is not None:
960 if self._num_downloads >= int(max_downloads):
961 raise MaxDownloadsReached()
963 info_dict['fulltitle'] = info_dict['title']
964 if len(info_dict['title']) > 200:
965 info_dict['title'] = info_dict['title'][:197] + '...'
967 # Keep for backwards compatibility
968 info_dict['stitle'] = info_dict['title']
970 if 'format' not in info_dict:
971 info_dict['format'] = info_dict['ext']
973 reason = self._match_entry(info_dict)
974 if reason is not None:
975 self.to_screen('[download] ' + reason)
978 self._num_downloads += 1
980 filename = self.prepare_filename(info_dict)
983 if self.params.get('forcetitle', False):
984 self.to_stdout(info_dict['fulltitle'])
985 if self.params.get('forceid', False):
986 self.to_stdout(info_dict['id'])
987 if self.params.get('forceurl', False):
988 if info_dict.get('requested_formats') is not None:
989 for f in info_dict['requested_formats']:
990 self.to_stdout(f['url'] + f.get('play_path', ''))
992 # For RTMP URLs, also include the playpath
993 self.to_stdout(info_dict['url'] + info_dict.get('play_path', ''))
994 if self.params.get('forcethumbnail', False) and info_dict.get('thumbnail') is not None:
995 self.to_stdout(info_dict['thumbnail'])
996 if self.params.get('forcedescription', False) and info_dict.get('description') is not None:
997 self.to_stdout(info_dict['description'])
998 if self.params.get('forcefilename', False) and filename is not None:
999 self.to_stdout(filename)
1000 if self.params.get('forceduration', False) and info_dict.get('duration') is not None:
1001 self.to_stdout(formatSeconds(info_dict['duration']))
1002 if self.params.get('forceformat', False):
1003 self.to_stdout(info_dict['format'])
1004 if self.params.get('forcejson', False):
1005 info_dict['_filename'] = filename
1006 self.to_stdout(json.dumps(info_dict))
1007 if self.params.get('dump_single_json', False):
1008 info_dict['_filename'] = filename
1010 # Do nothing else if in simulate mode
1011 if self.params.get('simulate', False):
1014 if filename is None:
1018 dn = os.path.dirname(encodeFilename(filename))
1019 if dn and not os.path.exists(dn):
1021 except (OSError, IOError) as err:
1022 self.report_error('unable to create directory ' + compat_str(err))
1025 if self.params.get('writedescription', False):
1026 descfn = filename + '.description'
1027 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
1028 self.to_screen('[info] Video description is already present')
1029 elif info_dict.get('description') is None:
1030 self.report_warning('There\'s no description to write.')
1033 self.to_screen('[info] Writing video description to: ' + descfn)
1034 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
1035 descfile.write(info_dict['description'])
1036 except (OSError, IOError):
1037 self.report_error('Cannot write description file ' + descfn)
1040 if self.params.get('writeannotations', False):
1041 annofn = filename + '.annotations.xml'
1042 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
1043 self.to_screen('[info] Video annotations are already present')
1046 self.to_screen('[info] Writing video annotations to: ' + annofn)
1047 with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
1048 annofile.write(info_dict['annotations'])
1049 except (KeyError, TypeError):
1050 self.report_warning('There are no annotations to write.')
1051 except (OSError, IOError):
1052 self.report_error('Cannot write annotations file: ' + annofn)
1055 subtitles_are_requested = any([self.params.get('writesubtitles', False),
1056 self.params.get('writeautomaticsub')])
1058 if subtitles_are_requested and 'subtitles' in info_dict and info_dict['subtitles']:
1059 # subtitles download errors are already managed as troubles in relevant IE
1060 # that way it will silently go on when used with unsupporting IE
1061 subtitles = info_dict['subtitles']
1062 sub_format = self.params.get('subtitlesformat', 'srt')
1063 for sub_lang in subtitles.keys():
1064 sub = subtitles[sub_lang]
1068 sub_filename = subtitles_filename(filename, sub_lang, sub_format)
1069 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(sub_filename)):
1070 self.to_screen('[info] Video subtitle %s.%s is already_present' % (sub_lang, sub_format))
1072 self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
1073 with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
1075 except (OSError, IOError):
1076 self.report_error('Cannot write subtitles file ' + sub_filename)
1079 if self.params.get('writeinfojson', False):
1080 infofn = os.path.splitext(filename)[0] + '.info.json'
1081 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
1082 self.to_screen('[info] Video description metadata is already present')
1084 self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
1086 write_json_file(info_dict, infofn)
1087 except (OSError, IOError):
1088 self.report_error('Cannot write metadata to JSON file ' + infofn)
1091 if self.params.get('writethumbnail', False):
1092 if info_dict.get('thumbnail') is not None:
1093 thumb_format = determine_ext(info_dict['thumbnail'], 'jpg')
1094 thumb_filename = os.path.splitext(filename)[0] + '.' + thumb_format
1095 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
1096 self.to_screen('[%s] %s: Thumbnail is already present' %
1097 (info_dict['extractor'], info_dict['id']))
1099 self.to_screen('[%s] %s: Downloading thumbnail ...' %
1100 (info_dict['extractor'], info_dict['id']))
1102 uf = self.urlopen(info_dict['thumbnail'])
1103 with open(thumb_filename, 'wb') as thumbf:
1104 shutil.copyfileobj(uf, thumbf)
1105 self.to_screen('[%s] %s: Writing thumbnail to: %s' %
1106 (info_dict['extractor'], info_dict['id'], thumb_filename))
1107 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1108 self.report_warning('Unable to download thumbnail "%s": %s' %
1109 (info_dict['thumbnail'], compat_str(err)))
1111 if not self.params.get('skip_download', False):
1112 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
1117 fd = get_suitable_downloader(info)(self, self.params)
1118 for ph in self._progress_hooks:
1119 fd.add_progress_hook(ph)
1120 if self.params.get('verbose'):
1121 self.to_stdout('[debug] Invoking downloader on %r' % info.get('url'))
1122 return fd.download(name, info)
1123 if info_dict.get('requested_formats') is not None:
1126 merger = FFmpegMergerPP(self, not self.params.get('keepvideo'))
1127 if not merger._executable:
1129 self.report_warning('You have requested multiple '
1130 'formats but ffmpeg or avconv are not installed.'
1131 ' The formats won\'t be merged')
1133 postprocessors = [merger]
1134 for f in info_dict['requested_formats']:
1135 new_info = dict(info_dict)
1137 fname = self.prepare_filename(new_info)
1138 fname = prepend_extension(fname, 'f%s' % f['format_id'])
1139 downloaded.append(fname)
1140 partial_success = dl(fname, new_info)
1141 success = success and partial_success
1142 info_dict['__postprocessors'] = postprocessors
1143 info_dict['__files_to_merge'] = downloaded
1145 # Just a single file
1146 success = dl(filename, info_dict)
1147 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1148 self.report_error('unable to download video data: %s' % str(err))
1150 except (OSError, IOError) as err:
1151 raise UnavailableVideoError(err)
1152 except (ContentTooShortError, ) as err:
1153 self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
1158 self.post_process(filename, info_dict)
1159 except (PostProcessingError) as err:
1160 self.report_error('postprocessing: %s' % str(err))
1162 self.record_download_archive(info_dict)
1164 def download(self, url_list):
1165 """Download a given list of URLs."""
1166 outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
1167 if (len(url_list) > 1 and
1169 and self.params.get('max_downloads') != 1):
1170 raise SameFileError(outtmpl)
1172 for url in url_list:
1174 # It also downloads the videos
1175 res = self.extract_info(url)
1176 except UnavailableVideoError:
1177 self.report_error('unable to download video')
1178 except MaxDownloadsReached:
1179 self.to_screen('[info] Maximum number of downloaded files reached.')
1182 if self.params.get('dump_single_json', False):
1183 self.to_stdout(json.dumps(res))
1185 return self._download_retcode
1187 def download_with_info_file(self, info_filename):
1188 with io.open(info_filename, 'r', encoding='utf-8') as f:
1191 self.process_ie_result(info, download=True)
1192 except DownloadError:
1193 webpage_url = info.get('webpage_url')
1194 if webpage_url is not None:
1195 self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
1196 return self.download([webpage_url])
1199 return self._download_retcode
1201 def post_process(self, filename, ie_info):
1202 """Run all the postprocessors on the given file."""
1203 info = dict(ie_info)
1204 info['filepath'] = filename
1207 if ie_info.get('__postprocessors') is not None:
1208 pps_chain.extend(ie_info['__postprocessors'])
1209 pps_chain.extend(self._pps)
1210 for pp in pps_chain:
1212 keep_video_wish, new_info = pp.run(info)
1213 if keep_video_wish is not None:
1215 keep_video = keep_video_wish
1216 elif keep_video is None:
1217 # No clear decision yet, let IE decide
1218 keep_video = keep_video_wish
1219 except PostProcessingError as e:
1220 self.report_error(e.msg)
1221 if keep_video is False and not self.params.get('keepvideo', False):
1223 self.to_screen('Deleting original file %s (pass -k to keep)' % filename)
1224 os.remove(encodeFilename(filename))
1225 except (IOError, OSError):
1226 self.report_warning('Unable to remove downloaded video file')
1228 def _make_archive_id(self, info_dict):
1229 # Future-proof against any change in case
1230 # and backwards compatibility with prior versions
1231 extractor = info_dict.get('extractor_key')
1232 if extractor is None:
1233 if 'id' in info_dict:
1234 extractor = info_dict.get('ie_key') # key in a playlist
1235 if extractor is None:
1236 return None # Incomplete video information
1237 return extractor.lower() + ' ' + info_dict['id']
1239 def in_download_archive(self, info_dict):
1240 fn = self.params.get('download_archive')
1244 vid_id = self._make_archive_id(info_dict)
1246 return False # Incomplete video information
1249 with locked_file(fn, 'r', encoding='utf-8') as archive_file:
1250 for line in archive_file:
1251 if line.strip() == vid_id:
1253 except IOError as ioe:
1254 if ioe.errno != errno.ENOENT:
1258 def record_download_archive(self, info_dict):
1259 fn = self.params.get('download_archive')
1262 vid_id = self._make_archive_id(info_dict)
1264 with locked_file(fn, 'a', encoding='utf-8') as archive_file:
1265 archive_file.write(vid_id + '\n')
1268 def format_resolution(format, default='unknown'):
1269 if format.get('vcodec') == 'none':
1271 if format.get('resolution') is not None:
1272 return format['resolution']
1273 if format.get('height') is not None:
1274 if format.get('width') is not None:
1275 res = '%sx%s' % (format['width'], format['height'])
1277 res = '%sp' % format['height']
1278 elif format.get('width') is not None:
1279 res = '?x%d' % format['width']
1284 def _format_note(self, fdict):
1286 if fdict.get('ext') in ['f4f', 'f4m']:
1287 res += '(unsupported) '
1288 if fdict.get('format_note') is not None:
1289 res += fdict['format_note'] + ' '
1290 if fdict.get('tbr') is not None:
1291 res += '%4dk ' % fdict['tbr']
1292 if fdict.get('container') is not None:
1295 res += '%s container' % fdict['container']
1296 if (fdict.get('vcodec') is not None and
1297 fdict.get('vcodec') != 'none'):
1300 res += fdict['vcodec']
1301 if fdict.get('vbr') is not None:
1303 elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
1305 if fdict.get('vbr') is not None:
1306 res += '%4dk' % fdict['vbr']
1307 if fdict.get('fps') is not None:
1308 res += ', %sfps' % fdict['fps']
1309 if fdict.get('acodec') is not None:
1312 if fdict['acodec'] == 'none':
1315 res += '%-5s' % fdict['acodec']
1316 elif fdict.get('abr') is not None:
1320 if fdict.get('abr') is not None:
1321 res += '@%3dk' % fdict['abr']
1322 if fdict.get('asr') is not None:
1323 res += ' (%5dHz)' % fdict['asr']
1324 if fdict.get('filesize') is not None:
1327 res += format_bytes(fdict['filesize'])
1328 elif fdict.get('filesize_approx') is not None:
1331 res += '~' + format_bytes(fdict['filesize_approx'])
1334 def list_formats(self, info_dict):
1335 def line(format, idlen=20):
1336 return (('%-' + compat_str(idlen + 1) + 's%-10s%-12s%s') % (
1337 format['format_id'],
1339 self.format_resolution(format),
1340 self._format_note(format),
1343 formats = info_dict.get('formats', [info_dict])
1344 idlen = max(len('format code'),
1345 max(len(f['format_id']) for f in formats))
1347 line(f, idlen) for f in formats
1348 if f.get('preference') is None or f['preference'] >= -1000]
1349 if len(formats) > 1:
1350 formats_s[0] += (' ' if self._format_note(formats[0]) else '') + '(worst)'
1351 formats_s[-1] += (' ' if self._format_note(formats[-1]) else '') + '(best)'
1353 header_line = line({
1354 'format_id': 'format code', 'ext': 'extension',
1355 'resolution': 'resolution', 'format_note': 'note'}, idlen=idlen)
1356 self.to_screen('[info] Available formats for %s:\n%s\n%s' %
1357 (info_dict['id'], header_line, '\n'.join(formats_s)))
1359 def urlopen(self, req):
1360 """ Start an HTTP download """
1362 # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
1363 # always respected by websites, some tend to give out URLs with non percent-encoded
1364 # non-ASCII characters (see telemb.py, ard.py [#3412])
1365 # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
1366 # To work around aforementioned issue we will replace request's original URL with
1367 # percent-encoded one
1368 req_is_string = isinstance(req, basestring if sys.version_info < (3, 0) else compat_str)
1369 url = req if req_is_string else req.get_full_url()
1370 url_escaped = escape_url(url)
1372 # Substitute URL if any change after escaping
1373 if url != url_escaped:
1377 req = compat_urllib_request.Request(
1378 url_escaped, data=req.data, headers=req.headers,
1379 origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
1381 return self._opener.open(req, timeout=self._socket_timeout)
1383 def print_debug_header(self):
1384 if not self.params.get('verbose'):
1387 if type('') is not compat_str:
1388 # Python 2.6 on SLES11 SP1 (https://github.com/rg3/youtube-dl/issues/3326)
1389 self.report_warning(
1390 'Your Python is broken! Update to a newer and supported version')
1392 stdout_encoding = getattr(
1393 sys.stdout, 'encoding', 'missing (%s)' % type(sys.stdout).__name__)
1395 '[debug] Encodings: locale %s, fs %s, out %s, pref %s\n' % (
1396 locale.getpreferredencoding(),
1397 sys.getfilesystemencoding(),
1399 self.get_encoding()))
1400 write_string(encoding_str, encoding=None)
1402 self._write_string('[debug] youtube-dl version ' + __version__ + '\n')
1404 sp = subprocess.Popen(
1405 ['git', 'rev-parse', '--short', 'HEAD'],
1406 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1407 cwd=os.path.dirname(os.path.abspath(__file__)))
1408 out, err = sp.communicate()
1409 out = out.decode().strip()
1410 if re.match('[0-9a-f]+', out):
1411 self._write_string('[debug] Git HEAD: ' + out + '\n')
1417 self._write_string('[debug] Python version %s - %s\n' % (
1418 platform.python_version(), platform_name()))
1420 exe_versions = FFmpegPostProcessor.get_versions()
1421 exe_versions['rtmpdump'] = rtmpdump_version()
1422 exe_str = ', '.join(
1424 for exe, v in sorted(exe_versions.items())
1429 self._write_string('[debug] exe versions: %s\n' % exe_str)
1432 for handler in self._opener.handlers:
1433 if hasattr(handler, 'proxies'):
1434 proxy_map.update(handler.proxies)
1435 self._write_string('[debug] Proxy map: ' + compat_str(proxy_map) + '\n')
1437 def _setup_opener(self):
1438 timeout_val = self.params.get('socket_timeout')
1439 self._socket_timeout = 600 if timeout_val is None else float(timeout_val)
1441 opts_cookiefile = self.params.get('cookiefile')
1442 opts_proxy = self.params.get('proxy')
1444 if opts_cookiefile is None:
1445 self.cookiejar = compat_cookiejar.CookieJar()
1447 self.cookiejar = compat_cookiejar.MozillaCookieJar(
1449 if os.access(opts_cookiefile, os.R_OK):
1450 self.cookiejar.load()
1452 cookie_processor = compat_urllib_request.HTTPCookieProcessor(
1454 if opts_proxy is not None:
1455 if opts_proxy == '':
1458 proxies = {'http': opts_proxy, 'https': opts_proxy}
1460 proxies = compat_urllib_request.getproxies()
1461 # Set HTTPS proxy to HTTP one if given (https://github.com/rg3/youtube-dl/issues/805)
1462 if 'http' in proxies and 'https' not in proxies:
1463 proxies['https'] = proxies['http']
1464 proxy_handler = compat_urllib_request.ProxyHandler(proxies)
1466 debuglevel = 1 if self.params.get('debug_printtraffic') else 0
1467 https_handler = make_HTTPS_handler(
1468 self.params.get('nocheckcertificate', False), debuglevel=debuglevel)
1469 ydlh = YoutubeDLHandler(debuglevel=debuglevel)
1470 opener = compat_urllib_request.build_opener(
1471 https_handler, proxy_handler, cookie_processor, ydlh)
1472 # Delete the default user-agent header, which would otherwise apply in
1473 # cases where our custom HTTP handler doesn't come into play
1474 # (See https://github.com/rg3/youtube-dl/issues/1309 for details)
1475 opener.addheaders = []
1476 self._opener = opener
1478 def encode(self, s):
1479 if isinstance(s, bytes):
1480 return s # Already encoded
1483 return s.encode(self.get_encoding())
1484 except UnicodeEncodeError as err:
1485 err.reason = err.reason + '. Check your system encoding configuration or use the --encoding option.'
1488 def get_encoding(self):
1489 encoding = self.params.get('encoding')
1490 if encoding is None:
1491 encoding = preferredencoding()