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