Better formatting (PEP 8)
[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 cookielib
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 urllib2
42 import warnings
43
44 from utils import *
45 from FileDownloader import *
46 from InfoExtractors import *
47 from PostProcessor import *
48
49 def updateSelf(downloader, filename):
50         ''' Update the program file with the latest version from the repository '''
51         # Note: downloader only used for options
52
53         if not os.access(filename, os.W_OK):
54                 sys.exit('ERROR: no write permissions on %s' % filename)
55
56         downloader.to_screen(u'Updating to latest version...')
57
58         urlv = urllib2.urlopen(UPDATE_URL_VERSION)
59         newversion = urlv.read().strip()
60         if newversion == __version__:
61                 downloader.to_screen(u'youtube-dl is up-to-date (' + __version__ + ')')
62                 return
63         urlv.close()
64
65         if hasattr(sys, "frozen"): #py2exe
66                 exe = os.path.abspath(filename)
67                 directory = os.path.dirname(exe)
68                 if not os.access(directory, os.W_OK):
69                         sys.exit('ERROR: no write permissions on %s' % directory)
70
71                 try:
72                         urlh = urllib2.urlopen(UPDATE_URL_EXE)
73                         newcontent = urlh.read()
74                         urlh.close()
75                         with open(exe + '.new', 'wb') as outf:
76                                 outf.write(newcontent)
77                 except (IOError, OSError), err:
78                         sys.exit('ERROR: unable to download latest version')
79
80                 try:
81                         bat = os.path.join(directory, 'youtube-dl-updater.bat')
82                         b = open(bat, 'w')
83                         b.write("""
84 echo Updating youtube-dl...
85 ping 127.0.0.1 -n 5 -w 1000 > NUL
86 move /Y "%s.new" "%s"
87 del "%s"
88                         \n""" %(exe, exe, bat))
89                         b.close()
90
91                         os.startfile(bat)
92                 except (IOError, OSError), err:
93                         sys.exit('ERROR: unable to overwrite current version')
94
95         else:
96                 try:
97                         urlh = urllib2.urlopen(UPDATE_URL)
98                         newcontent = urlh.read()
99                         urlh.close()
100                 except (IOError, OSError), err:
101                         sys.exit('ERROR: unable to download latest version')
102
103                 try:
104                         with open(filename, 'wb') as outf:
105                                 outf.write(newcontent)
106                 except (IOError, OSError), err:
107                         sys.exit('ERROR: unable to overwrite current version')
108
109         downloader.to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.')
110
111 def parseOpts():
112         def _readOptions(filename_bytes):
113                 try:
114                         optionf = open(filename_bytes)
115                 except IOError:
116                         return [] # silently skip if file is not present
117                 try:
118                         res = []
119                         for l in optionf:
120                                 res += shlex.split(l, comments=True)
121                 finally:
122                         optionf.close()
123                 return res
124
125         def _format_option_string(option):
126                 ''' ('-o', '--option') -> -o, --format METAVAR'''
127
128                 opts = []
129
130                 if option._short_opts:
131                         opts.append(option._short_opts[0])
132                 if option._long_opts:
133                         opts.append(option._long_opts[0])
134                 if len(opts) > 1:
135                         opts.insert(1, ', ')
136
137                 if option.takes_value(): opts.append(' %s' % option.metavar)
138
139                 return "".join(opts)
140
141         def _find_term_columns():
142                 columns = os.environ.get('COLUMNS', None)
143                 if columns:
144                         return int(columns)
145
146                 try:
147                         sp = subprocess.Popen(['stty', 'size'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
148                         out,err = sp.communicate()
149                         return int(out.split()[1])
150                 except:
151                         pass
152                 return None
153
154         max_width = 80
155         max_help_position = 80
156
157         # No need to wrap help messages if we're on a wide console
158         columns = _find_term_columns()
159         if columns: max_width = columns
160
161         fmt = optparse.IndentedHelpFormatter(width=max_width, max_help_position=max_help_position)
162         fmt.format_option_strings = _format_option_string
163
164         kw = {
165                 'version'   : __version__,
166                 'formatter' : fmt,
167                 'usage' : '%prog [options] url [url...]',
168                 'conflict_handler' : 'resolve',
169         }
170
171         parser = optparse.OptionParser(**kw)
172
173         # option groups
174         general        = optparse.OptionGroup(parser, 'General Options')
175         selection      = optparse.OptionGroup(parser, 'Video Selection')
176         authentication = optparse.OptionGroup(parser, 'Authentication Options')
177         video_format   = optparse.OptionGroup(parser, 'Video Format Options')
178         postproc       = optparse.OptionGroup(parser, 'Post-processing Options')
179         filesystem     = optparse.OptionGroup(parser, 'Filesystem Options')
180         verbosity      = optparse.OptionGroup(parser, 'Verbosity / Simulation Options')
181
182         general.add_option('-h', '--help',
183                         action='help', help='print this help text and exit')
184         general.add_option('-v', '--version',
185                         action='version', help='print program version and exit')
186         general.add_option('-U', '--update',
187                         action='store_true', dest='update_self', help='update this program to latest version')
188         general.add_option('-i', '--ignore-errors',
189                         action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
190         general.add_option('-r', '--rate-limit',
191                         dest='ratelimit', metavar='LIMIT', help='download rate limit (e.g. 50k or 44.6m)')
192         general.add_option('-R', '--retries',
193                         dest='retries', metavar='RETRIES', help='number of retries (default is %default)', default=10)
194         general.add_option('--buffer-size',
195                         dest='buffersize', metavar='SIZE', help='size of download buffer (e.g. 1024 or 16k) (default is %default)', default="1024")
196         general.add_option('--no-resize-buffer',
197                         action='store_true', dest='noresizebuffer',
198                         help='do not automatically adjust the buffer size. By default, the buffer size is automatically resized from an initial value of SIZE.', default=False)
199         general.add_option('--dump-user-agent',
200                         action='store_true', dest='dump_user_agent',
201                         help='display the current browser identification', default=False)
202         general.add_option('--user-agent',
203                         dest='user_agent', help='specify a custom user agent', metavar='UA')
204         general.add_option('--list-extractors',
205                         action='store_true', dest='list_extractors',
206                         help='List all supported extractors and the URLs they would handle', default=False)
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.')
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 = cookielib.CookieJar()
384         else:
385                 try:
386                         jar = cookielib.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), 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 = map(lambda url: url.strip(), all_urls)
415
416         # General configuration
417         cookie_processor = urllib2.HTTPCookieProcessor(jar)
418         proxy_handler = urllib2.ProxyHandler()
419         opener = urllib2.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
420         urllib2.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)
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 = long(opts.retries)
453                 except (TypeError, ValueError), 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), 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), 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                 })
530
531         if opts.verbose:
532                 fd.to_screen(u'[debug] Proxy map: ' + str(proxy_handler.proxies))
533
534         for extractor in extractors:
535                 fd.add_info_extractor(extractor)
536
537         # PostProcessors
538         if opts.extractaudio:
539                 fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, keepvideo=opts.keepvideo))
540
541         # Update version
542         if opts.update_self:
543                 updateSelf(fd, sys.argv[0])
544
545         # Maybe do nothing
546         if len(all_urls) < 1:
547                 if not opts.update_self:
548                         parser.error(u'you must provide at least one URL')
549                 else:
550                         sys.exit()
551
552         try:
553                 retcode = fd.download(all_urls)
554         except MaxDownloadsReached:
555                 fd.to_screen(u'--max-download limit reached, aborting.')
556                 retcode = 101
557
558         # Dump cookie jar if requested
559         if opts.cookiefile is not None:
560                 try:
561                         jar.save()
562                 except (IOError, OSError), err:
563                         sys.exit(u'ERROR: unable to save cookie jar')
564
565         sys.exit(retcode)
566
567 def main():
568         try:
569                 _real_main()
570         except DownloadError:
571                 sys.exit(1)
572         except SameFileError:
573                 sys.exit(u'ERROR: fixed output name but more than one file to download')
574         except KeyboardInterrupt:
575                 sys.exit(u'\nERROR: Interrupted by user')