exposing the test mode as --test (hidden and undocumented)
[youtube-dl] / youtube_dl / __init__.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import with_statement
5 from __future__ import absolute_import
6
7 __authors__  = (
8     'Ricardo Garcia Gonzalez',
9     'Danny Colligan',
10     'Benjamin Johnson',
11     'Vasyl\' Vavrychuk',
12     'Witold Baryluk',
13     'Paweł Paprota',
14     'Gergely Imreh',
15     'Rogério Brito',
16     'Philipp Hagemeister',
17     'Sören Schulze',
18     'Kevin Ngo',
19     'Ori Avtalion',
20     'shizeeg',
21     'Filippo Valsorda',
22     'Christian Albrecht',
23     )
24
25 __license__ = 'Public Domain'
26 __version__ = '2012.11.29'
27
28 UPDATE_URL = 'https://raw.github.com/rg3/youtube-dl/master/youtube-dl'
29 UPDATE_URL_VERSION = 'https://raw.github.com/rg3/youtube-dl/master/LATEST_VERSION'
30 UPDATE_URL_EXE = 'https://raw.github.com/rg3/youtube-dl/master/youtube-dl.exe'
31
32
33 import getpass
34 import optparse
35 import os
36 import re
37 import shlex
38 import socket
39 import subprocess
40 import sys
41 import warnings
42
43 from .utils import *
44 from .FileDownloader import *
45 from .InfoExtractors import *
46 from .PostProcessor import *
47
48 def updateSelf(downloader, filename):
49     ''' Update the program file with the latest version from the repository '''
50     # Note: downloader only used for options
51
52     if not os.access(filename, os.W_OK):
53         sys.exit('ERROR: no write permissions on %s' % filename)
54
55     downloader.to_screen(u'Updating to latest version...')
56
57     urlv = compat_urllib_request.urlopen(UPDATE_URL_VERSION)
58     newversion = urlv.read().strip()
59     if newversion == __version__:
60         downloader.to_screen(u'youtube-dl is up-to-date (' + __version__ + ')')
61         return
62     urlv.close()
63
64     if hasattr(sys, "frozen"): #py2exe
65         exe = os.path.abspath(filename)
66         directory = os.path.dirname(exe)
67         if not os.access(directory, os.W_OK):
68             sys.exit('ERROR: no write permissions on %s' % directory)
69
70         try:
71             urlh = compat_urllib_request.urlopen(UPDATE_URL_EXE)
72             newcontent = urlh.read()
73             urlh.close()
74             with open(exe + '.new', 'wb') as outf:
75                 outf.write(newcontent)
76         except (IOError, OSError) as err:
77             sys.exit('ERROR: unable to download latest version')
78
79         try:
80             bat = os.path.join(directory, 'youtube-dl-updater.bat')
81             b = open(bat, 'w')
82             b.write("""
83 echo Updating youtube-dl...
84 ping 127.0.0.1 -n 5 -w 1000 > NUL
85 move /Y "%s.new" "%s"
86 del "%s"
87             \n""" %(exe, exe, bat))
88             b.close()
89
90             os.startfile(bat)
91         except (IOError, OSError) as err:
92             sys.exit('ERROR: unable to overwrite current version')
93
94     else:
95         try:
96             urlh = compat_urllib_request.urlopen(UPDATE_URL)
97             newcontent = urlh.read()
98             urlh.close()
99         except (IOError, OSError) as err:
100             sys.exit('ERROR: unable to download latest version')
101
102         try:
103             with open(filename, 'wb') as outf:
104                 outf.write(newcontent)
105         except (IOError, OSError) as err:
106             sys.exit('ERROR: unable to overwrite current version')
107
108     downloader.to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.')
109
110 def parseOpts():
111     def _readOptions(filename_bytes):
112         try:
113             optionf = open(filename_bytes)
114         except IOError:
115             return [] # silently skip if file is not present
116         try:
117             res = []
118             for l in optionf:
119                 res += shlex.split(l, comments=True)
120         finally:
121             optionf.close()
122         return res
123
124     def _format_option_string(option):
125         ''' ('-o', '--option') -> -o, --format METAVAR'''
126
127         opts = []
128
129         if option._short_opts:
130             opts.append(option._short_opts[0])
131         if option._long_opts:
132             opts.append(option._long_opts[0])
133         if len(opts) > 1:
134             opts.insert(1, ', ')
135
136         if option.takes_value(): opts.append(' %s' % option.metavar)
137
138         return "".join(opts)
139
140     def _find_term_columns():
141         columns = os.environ.get('COLUMNS', None)
142         if columns:
143             return int(columns)
144
145         try:
146             sp = subprocess.Popen(['stty', 'size'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
147             out,err = sp.communicate()
148             return int(out.split()[1])
149         except:
150             pass
151         return None
152
153     max_width = 80
154     max_help_position = 80
155
156     # No need to wrap help messages if we're on a wide console
157     columns = _find_term_columns()
158     if columns: max_width = columns
159
160     fmt = optparse.IndentedHelpFormatter(width=max_width, max_help_position=max_help_position)
161     fmt.format_option_strings = _format_option_string
162
163     kw = {
164         'version'   : __version__,
165         'formatter' : fmt,
166         'usage' : '%prog [options] url [url...]',
167         'conflict_handler' : 'resolve',
168     }
169
170     parser = optparse.OptionParser(**kw)
171
172     # option groups
173     general        = optparse.OptionGroup(parser, 'General Options')
174     selection      = optparse.OptionGroup(parser, 'Video Selection')
175     authentication = optparse.OptionGroup(parser, 'Authentication Options')
176     video_format   = optparse.OptionGroup(parser, 'Video Format Options')
177     postproc       = optparse.OptionGroup(parser, 'Post-processing Options')
178     filesystem     = optparse.OptionGroup(parser, 'Filesystem Options')
179     verbosity      = optparse.OptionGroup(parser, 'Verbosity / Simulation Options')
180
181     general.add_option('-h', '--help',
182             action='help', help='print this help text and exit')
183     general.add_option('-v', '--version',
184             action='version', help='print program version and exit')
185     general.add_option('-U', '--update',
186             action='store_true', dest='update_self', help='update this program to latest version')
187     general.add_option('-i', '--ignore-errors',
188             action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
189     general.add_option('-r', '--rate-limit',
190             dest='ratelimit', metavar='LIMIT', help='download rate limit (e.g. 50k or 44.6m)')
191     general.add_option('-R', '--retries',
192             dest='retries', metavar='RETRIES', help='number of retries (default is %default)', default=10)
193     general.add_option('--buffer-size',
194             dest='buffersize', metavar='SIZE', help='size of download buffer (e.g. 1024 or 16k) (default is %default)', default="1024")
195     general.add_option('--no-resize-buffer',
196             action='store_true', dest='noresizebuffer',
197             help='do not automatically adjust the buffer size. By default, the buffer size is automatically resized from an initial value of SIZE.', default=False)
198     general.add_option('--dump-user-agent',
199             action='store_true', dest='dump_user_agent',
200             help='display the current browser identification', default=False)
201     general.add_option('--user-agent',
202             dest='user_agent', help='specify a custom user agent', metavar='UA')
203     general.add_option('--list-extractors',
204             action='store_true', dest='list_extractors',
205             help='List all supported extractors and the URLs they would handle', default=False)
206     general.add_option('--test', action='store_true', dest='test', default=False, help=optparse.SUPPRESS_HELP)
207
208     selection.add_option('--playlist-start',
209             dest='playliststart', metavar='NUMBER', help='playlist video to start at (default is %default)', default=1)
210     selection.add_option('--playlist-end',
211             dest='playlistend', metavar='NUMBER', help='playlist video to end at (default is last)', default=-1)
212     selection.add_option('--match-title', dest='matchtitle', metavar='REGEX',help='download only matching titles (regex or caseless sub-string)')
213     selection.add_option('--reject-title', dest='rejecttitle', metavar='REGEX',help='skip download for matching titles (regex or caseless sub-string)')
214     selection.add_option('--max-downloads', metavar='NUMBER', dest='max_downloads', help='Abort after downloading NUMBER files', default=None)
215
216     authentication.add_option('-u', '--username',
217             dest='username', metavar='USERNAME', help='account username')
218     authentication.add_option('-p', '--password',
219             dest='password', metavar='PASSWORD', help='account password')
220     authentication.add_option('-n', '--netrc',
221             action='store_true', dest='usenetrc', help='use .netrc authentication data', default=False)
222
223
224     video_format.add_option('-f', '--format',
225             action='store', dest='format', metavar='FORMAT', help='video format code')
226     video_format.add_option('--all-formats',
227             action='store_const', dest='format', help='download all available video formats', const='all')
228     video_format.add_option('--prefer-free-formats',
229             action='store_true', dest='prefer_free_formats', default=False, help='prefer free video formats unless a specific one is requested')
230     video_format.add_option('--max-quality',
231             action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download')
232     video_format.add_option('-F', '--list-formats',
233             action='store_true', dest='listformats', help='list all available formats (currently youtube only)')
234     video_format.add_option('--write-srt',
235             action='store_true', dest='writesubtitles',
236             help='write video closed captions to a .srt file (currently youtube only)', default=False)
237     video_format.add_option('--srt-lang',
238             action='store', dest='subtitleslang', metavar='LANG',
239             help='language of the closed captions to download (optional) use IETF language tags like \'en\'')
240
241
242     verbosity.add_option('-q', '--quiet',
243             action='store_true', dest='quiet', help='activates quiet mode', default=False)
244     verbosity.add_option('-s', '--simulate',
245             action='store_true', dest='simulate', help='do not download the video and do not write anything to disk', default=False)
246     verbosity.add_option('--skip-download',
247             action='store_true', dest='skip_download', help='do not download the video', default=False)
248     verbosity.add_option('-g', '--get-url',
249             action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
250     verbosity.add_option('-e', '--get-title',
251             action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
252     verbosity.add_option('--get-thumbnail',
253             action='store_true', dest='getthumbnail',
254             help='simulate, quiet but print thumbnail URL', default=False)
255     verbosity.add_option('--get-description',
256             action='store_true', dest='getdescription',
257             help='simulate, quiet but print video description', default=False)
258     verbosity.add_option('--get-filename',
259             action='store_true', dest='getfilename',
260             help='simulate, quiet but print output filename', default=False)
261     verbosity.add_option('--get-format',
262             action='store_true', dest='getformat',
263             help='simulate, quiet but print output format', default=False)
264     verbosity.add_option('--no-progress',
265             action='store_true', dest='noprogress', help='do not print progress bar', default=False)
266     verbosity.add_option('--console-title',
267             action='store_true', dest='consoletitle',
268             help='display progress in console titlebar', default=False)
269     verbosity.add_option('-v', '--verbose',
270             action='store_true', dest='verbose', help='print various debugging information', default=False)
271
272
273     filesystem.add_option('-t', '--title',
274             action='store_true', dest='usetitle', help='use title in file name', default=False)
275     filesystem.add_option('--id',
276             action='store_true', dest='useid', help='use video ID in file name', default=False)
277     filesystem.add_option('-l', '--literal',
278             action='store_true', dest='usetitle', help='[deprecated] alias of --title', default=False)
279     filesystem.add_option('-A', '--auto-number',
280             action='store_true', dest='autonumber',
281             help='number downloaded files starting from 00000', default=False)
282     filesystem.add_option('-o', '--output',
283             dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(title)s to get the title, %(uploader)s for the uploader name, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id and %% for a literal percent. Use - to output to stdout. Can also be used to download to a different directory, for example with -o \'/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s\' .')
284     filesystem.add_option('--restrict-filenames',
285             action='store_true', dest='restrictfilenames',
286             help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames', default=False)
287     filesystem.add_option('-a', '--batch-file',
288             dest='batchfile', metavar='FILE', help='file containing URLs to download (\'-\' for stdin)')
289     filesystem.add_option('-w', '--no-overwrites',
290             action='store_true', dest='nooverwrites', help='do not overwrite files', default=False)
291     filesystem.add_option('-c', '--continue',
292             action='store_true', dest='continue_dl', help='resume partially downloaded files', default=True)
293     filesystem.add_option('--no-continue',
294             action='store_false', dest='continue_dl',
295             help='do not resume partially downloaded files (restart from beginning)')
296     filesystem.add_option('--cookies',
297             dest='cookiefile', metavar='FILE', help='file to read cookies from and dump cookie jar in')
298     filesystem.add_option('--no-part',
299             action='store_true', dest='nopart', help='do not use .part files', default=False)
300     filesystem.add_option('--no-mtime',
301             action='store_false', dest='updatetime',
302             help='do not use the Last-modified header to set the file modification time', default=True)
303     filesystem.add_option('--write-description',
304             action='store_true', dest='writedescription',
305             help='write video description to a .description file', default=False)
306     filesystem.add_option('--write-info-json',
307             action='store_true', dest='writeinfojson',
308             help='write video metadata to a .info.json file', default=False)
309
310
311     postproc.add_option('-x', '--extract-audio', action='store_true', dest='extractaudio', default=False,
312             help='convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)')
313     postproc.add_option('--audio-format', metavar='FORMAT', dest='audioformat', default='best',
314             help='"best", "aac", "vorbis", "mp3", "m4a", or "wav"; best by default')
315     postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5',
316             help='ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5)')
317     postproc.add_option('-k', '--keep-video', action='store_true', dest='keepvideo', default=False,
318             help='keeps the video file on disk after the post-processing; the video is erased by default')
319
320
321     parser.add_option_group(general)
322     parser.add_option_group(selection)
323     parser.add_option_group(filesystem)
324     parser.add_option_group(verbosity)
325     parser.add_option_group(video_format)
326     parser.add_option_group(authentication)
327     parser.add_option_group(postproc)
328
329     xdg_config_home = os.environ.get('XDG_CONFIG_HOME')
330     if xdg_config_home:
331         userConf = os.path.join(xdg_config_home, 'youtube-dl.conf')
332     else:
333         userConf = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf')
334     argv = _readOptions('/etc/youtube-dl.conf') + _readOptions(userConf) + sys.argv[1:]
335     opts, args = parser.parse_args(argv)
336
337     return parser, opts, args
338
339 def gen_extractors():
340     """ Return a list of an instance of every supported extractor.
341     The order does matter; the first extractor matched is the one handling the URL.
342     """
343     return [
344         YoutubePlaylistIE(),
345         YoutubeChannelIE(),
346         YoutubeUserIE(),
347         YoutubeSearchIE(),
348         YoutubeIE(),
349         MetacafeIE(),
350         DailymotionIE(),
351         GoogleIE(),
352         GoogleSearchIE(),
353         PhotobucketIE(),
354         YahooIE(),
355         YahooSearchIE(),
356         DepositFilesIE(),
357         FacebookIE(),
358         BlipTVUserIE(),
359         BlipTVIE(),
360         VimeoIE(),
361         MyVideoIE(),
362         ComedyCentralIE(),
363         EscapistIE(),
364         CollegeHumorIE(),
365         XVideosIE(),
366         SoundcloudIE(),
367         InfoQIE(),
368         MixcloudIE(),
369         StanfordOpenClassroomIE(),
370         MTVIE(),
371         YoukuIE(),
372         XNXXIE(),
373         GooglePlusIE(),
374         ArteTvIE(),
375         GenericIE()
376     ]
377
378 def _real_main():
379     parser, opts, args = parseOpts()
380
381     # Open appropriate CookieJar
382     if opts.cookiefile is None:
383         jar = compat_cookiejar.CookieJar()
384     else:
385         try:
386             jar = compat_cookiejar.MozillaCookieJar(opts.cookiefile)
387             if os.path.isfile(opts.cookiefile) and os.access(opts.cookiefile, os.R_OK):
388                 jar.load()
389         except (IOError, OSError) as err:
390             sys.exit(u'ERROR: unable to open cookie file')
391     # Set user agent
392     if opts.user_agent is not None:
393         std_headers['User-Agent'] = opts.user_agent
394
395     # Dump user agent
396     if opts.dump_user_agent:
397         print(std_headers['User-Agent'])
398         sys.exit(0)
399
400     # Batch file verification
401     batchurls = []
402     if opts.batchfile is not None:
403         try:
404             if opts.batchfile == '-':
405                 batchfd = sys.stdin
406             else:
407                 batchfd = open(opts.batchfile, 'r')
408             batchurls = batchfd.readlines()
409             batchurls = [x.strip() for x in batchurls]
410             batchurls = [x for x in batchurls if len(x) > 0 and not re.search(r'^[#/;]', x)]
411         except IOError:
412             sys.exit(u'ERROR: batch file could not be read')
413     all_urls = batchurls + args
414     all_urls = [url.strip() for url in all_urls]
415
416     # General configuration
417     cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
418     proxy_handler = compat_urllib_request.ProxyHandler()
419     opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
420     compat_urllib_request.install_opener(opener)
421     socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
422
423     extractors = gen_extractors()
424
425     if opts.list_extractors:
426         for ie in extractors:
427             print(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else ''))
428             matchedUrls = filter(lambda url: ie.suitable(url), all_urls)
429             all_urls = filter(lambda url: url not in matchedUrls, all_urls)
430             for mu in matchedUrls:
431                 print(u'  ' + mu)
432         sys.exit(0)
433
434     # Conflicting, missing and erroneous options
435     if opts.usenetrc and (opts.username is not None or opts.password is not None):
436         parser.error(u'using .netrc conflicts with giving username/password')
437     if opts.password is not None and opts.username is None:
438         parser.error(u'account username missing')
439     if opts.outtmpl is not None and (opts.usetitle or opts.autonumber or opts.useid):
440         parser.error(u'using output template conflicts with using title, video ID or auto number')
441     if opts.usetitle and opts.useid:
442         parser.error(u'using title conflicts with using video ID')
443     if opts.username is not None and opts.password is None:
444         opts.password = getpass.getpass(u'Type account password and press return:')
445     if opts.ratelimit is not None:
446         numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
447         if numeric_limit is None:
448             parser.error(u'invalid rate limit specified')
449         opts.ratelimit = numeric_limit
450     if opts.retries is not None:
451         try:
452             opts.retries = int(opts.retries)
453         except (TypeError, ValueError) as err:
454             parser.error(u'invalid retry count specified')
455     if opts.buffersize is not None:
456         numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
457         if numeric_buffersize is None:
458             parser.error(u'invalid buffer size specified')
459         opts.buffersize = numeric_buffersize
460     try:
461         opts.playliststart = int(opts.playliststart)
462         if opts.playliststart <= 0:
463             raise ValueError(u'Playlist start must be positive')
464     except (TypeError, ValueError) as err:
465         parser.error(u'invalid playlist start number specified')
466     try:
467         opts.playlistend = int(opts.playlistend)
468         if opts.playlistend != -1 and (opts.playlistend <= 0 or opts.playlistend < opts.playliststart):
469             raise ValueError(u'Playlist end must be greater than playlist start')
470     except (TypeError, ValueError) as err:
471         parser.error(u'invalid playlist end number specified')
472     if opts.extractaudio:
473         if opts.audioformat not in ['best', 'aac', 'mp3', 'vorbis', 'm4a', 'wav']:
474             parser.error(u'invalid audio format specified')
475     if opts.audioquality:
476         opts.audioquality = opts.audioquality.strip('k').strip('K')
477         if not opts.audioquality.isdigit():
478             parser.error(u'invalid audio quality specified')
479
480     # File downloader
481     fd = FileDownloader({
482         'usenetrc': opts.usenetrc,
483         'username': opts.username,
484         'password': opts.password,
485         'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
486         'forceurl': opts.geturl,
487         'forcetitle': opts.gettitle,
488         'forcethumbnail': opts.getthumbnail,
489         'forcedescription': opts.getdescription,
490         'forcefilename': opts.getfilename,
491         'forceformat': opts.getformat,
492         'simulate': opts.simulate,
493         'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
494         'format': opts.format,
495         'format_limit': opts.format_limit,
496         'listformats': opts.listformats,
497         'outtmpl': ((opts.outtmpl is not None and opts.outtmpl.decode(preferredencoding()))
498             or (opts.format == '-1' and opts.usetitle and u'%(title)s-%(id)s-%(format)s.%(ext)s')
499             or (opts.format == '-1' and u'%(id)s-%(format)s.%(ext)s')
500             or (opts.usetitle and opts.autonumber and u'%(autonumber)s-%(title)s-%(id)s.%(ext)s')
501             or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
502             or (opts.useid and u'%(id)s.%(ext)s')
503             or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
504             or u'%(id)s.%(ext)s'),
505         'restrictfilenames': opts.restrictfilenames,
506         'ignoreerrors': opts.ignoreerrors,
507         'ratelimit': opts.ratelimit,
508         'nooverwrites': opts.nooverwrites,
509         'retries': opts.retries,
510         'buffersize': opts.buffersize,
511         'noresizebuffer': opts.noresizebuffer,
512         'continuedl': opts.continue_dl,
513         'noprogress': opts.noprogress,
514         'playliststart': opts.playliststart,
515         'playlistend': opts.playlistend,
516         'logtostderr': opts.outtmpl == '-',
517         'consoletitle': opts.consoletitle,
518         'nopart': opts.nopart,
519         'updatetime': opts.updatetime,
520         'writedescription': opts.writedescription,
521         'writeinfojson': opts.writeinfojson,
522         'writesubtitles': opts.writesubtitles,
523         'subtitleslang': opts.subtitleslang,
524         'matchtitle': opts.matchtitle,
525         'rejecttitle': opts.rejecttitle,
526         'max_downloads': opts.max_downloads,
527         'prefer_free_formats': opts.prefer_free_formats,
528         'verbose': opts.verbose,
529         'test': opts.test,
530         })
531
532     if opts.verbose:
533         fd.to_screen(u'[debug] Proxy map: ' + str(proxy_handler.proxies))
534
535     for extractor in extractors:
536         fd.add_info_extractor(extractor)
537
538     # PostProcessors
539     if opts.extractaudio:
540         fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, keepvideo=opts.keepvideo))
541
542     # Update version
543     if opts.update_self:
544         updateSelf(fd, sys.argv[0])
545
546     # Maybe do nothing
547     if len(all_urls) < 1:
548         if not opts.update_self:
549             parser.error(u'you must provide at least one URL')
550         else:
551             sys.exit()
552
553     try:
554         retcode = fd.download(all_urls)
555     except MaxDownloadsReached:
556         fd.to_screen(u'--max-download limit reached, aborting.')
557         retcode = 101
558
559     # Dump cookie jar if requested
560     if opts.cookiefile is not None:
561         try:
562             jar.save()
563         except (IOError, OSError) as err:
564             sys.exit(u'ERROR: unable to save cookie jar')
565
566     sys.exit(retcode)
567
568 def main():
569     try:
570         _real_main()
571     except DownloadError:
572         sys.exit(1)
573     except SameFileError:
574         sys.exit(u'ERROR: fixed output name but more than one file to download')
575     except KeyboardInterrupt:
576         sys.exit(u'\nERROR: Interrupted by user')