4dea34376bba4b87dfcf4ff7a47fd570ec906549
[youtube-dl] / youtube-dl
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # Author: Ricardo Garcia Gonzalez
4 # License: Public domain code
5 import htmlentitydefs
6 import httplib
7 import math
8 import netrc
9 import os
10 import os.path
11 import re
12 import socket
13 import string
14 import sys
15 import time
16 import urllib
17 import urllib2
18
19 std_headers = { 
20         'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1',
21         'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
22         'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
23         'Accept-Language': 'en-us,en;q=0.5',
24 }
25
26 simple_title_chars = string.ascii_letters.decode('ascii') + string.digits.decode('ascii')
27
28 class DownloadError(Exception):
29         """Download Error exception.
30         
31         This exception may be thrown by FileDownloader objects if they are not
32         configured to continue on errors. They will contain the appropriate
33         error message.
34         """
35         pass
36
37 class SameFileError(Exception):
38         """Same File exception.
39
40         This exception will be thrown by FileDownloader objects if they detect
41         multiple files would have to be downloaded to the same file on disk.
42         """
43         pass
44
45 class FileDownloader(object):
46         """File Downloader class.
47
48         File downloader objects are the ones responsible of downloading the
49         actual video file and writing it to disk if the user has requested
50         it, among some other tasks. In most cases there should be one per
51         program. As, given a video URL, the downloader doesn't know how to
52         extract all the needed information, task that InfoExtractors do, it
53         has to pass the URL to one of them.
54
55         For this, file downloader objects have a method that allows
56         InfoExtractors to be registered in a given order. When it is passed
57         a URL, the file downloader handles it to the first InfoExtractor it
58         finds that reports being able to handle it. The InfoExtractor returns
59         all the information to the FileDownloader and the latter downloads the
60         file or does whatever it's instructed to do.
61
62         File downloaders accept a lot of parameters. In order not to saturate
63         the object constructor with arguments, it receives a dictionary of
64         options instead. These options are available through the get_params()
65         method for the InfoExtractors to use. The FileDownloader also registers
66         itself as the downloader in charge for the InfoExtractors that are
67         added to it, so this is a "mutual registration".
68
69         Available options:
70
71         username:       Username for authentication purposes.
72         password:       Password for authentication purposes.
73         usenetrc:       Use netrc for authentication instead.
74         quiet:          Do not print messages to stdout.
75         forceurl:       Force printing final URL.
76         forcetitle:     Force printing title.
77         simulate:       Do not download the video files.
78         format:         Video format code.
79         outtmpl:        Template for output names.
80         ignoreerrors:   Do not stop on download errors.
81         ratelimit:      Download speed limit, in bytes/sec.
82         """
83
84         _params = None
85         _ies = []
86
87         def __init__(self, params):
88                 """Create a FileDownloader object with the given options."""
89                 self._ies = []
90                 self.set_params(params)
91         
92         @staticmethod
93         def pmkdir(filename):
94                 """Create directory components in filename. Similar to Unix "mkdir -p"."""
95                 components = filename.split(os.sep)
96                 aggregate = [os.sep.join(components[0:x]) for x in xrange(1, len(components))]
97                 for dir in aggregate:
98                         if not os.path.exists(dir):
99                                 os.mkdir(dir)
100         
101         @staticmethod
102         def format_bytes(bytes):
103                 if bytes is None:
104                         return 'N/A'
105                 if bytes == 0:
106                         exponent = 0
107                 else:
108                         exponent = long(math.log(float(bytes), 1024.0))
109                 suffix = 'bkMGTPEZY'[exponent]
110                 converted = float(bytes) / float(1024**exponent)
111                 return '%.2f%s' % (converted, suffix)
112
113         @staticmethod
114         def calc_percent(byte_counter, data_len):
115                 if data_len is None:
116                         return '---.-%'
117                 return '%6s' % ('%3.1f%%' % (float(byte_counter) / float(data_len) * 100.0))
118
119         @staticmethod
120         def calc_eta(start, now, total, current):
121                 if total is None:
122                         return '--:--'
123                 dif = now - start
124                 if current == 0 or dif < 0.001: # One millisecond
125                         return '--:--'
126                 rate = float(current) / dif
127                 eta = long((float(total) - float(current)) / rate)
128                 (eta_mins, eta_secs) = divmod(eta, 60)
129                 if eta_mins > 99:
130                         return '--:--'
131                 return '%02d:%02d' % (eta_mins, eta_secs)
132
133         @staticmethod
134         def calc_speed(start, now, bytes):
135                 dif = now - start
136                 if bytes == 0 or dif < 0.001: # One millisecond
137                         return '%10s' % '---b/s'
138                 return '%10s' % ('%s/s' % FileDownloader.format_bytes(float(bytes) / dif))
139
140         @staticmethod
141         def best_block_size(elapsed_time, bytes):
142                 new_min = max(bytes / 2.0, 1.0)
143                 new_max = min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB
144                 if elapsed_time < 0.001:
145                         return int(new_max)
146                 rate = bytes / elapsed_time
147                 if rate > new_max:
148                         return int(new_max)
149                 if rate < new_min:
150                         return int(new_min)
151                 return int(rate)
152
153         @staticmethod
154         def parse_bytes(bytestr):
155                 """Parse a string indicating a byte quantity into a long integer."""
156                 matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
157                 if matchobj is None:
158                         return None
159                 number = float(matchobj.group(1))
160                 multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
161                 return long(round(number * multiplier))
162
163         def set_params(self, params):
164                 """Sets parameters."""
165                 if type(params) != dict:
166                         raise ValueError('params: dictionary expected')
167                 self._params = params
168         
169         def get_params(self):
170                 """Get parameters."""
171                 return self._params
172
173         def add_info_extractor(self, ie):
174                 """Add an InfoExtractor object to the end of the list."""
175                 self._ies.append(ie)
176                 ie.set_downloader(self)
177         
178         def to_stdout(self, message, skip_eol=False):
179                 """Print message to stdout if not in quiet mode."""
180                 if not self._params.get('quiet', False):
181                         sys.stdout.write('%s%s' % (message, ['\n', ''][skip_eol]))
182                         sys.stdout.flush()
183         
184         def to_stderr(self, message):
185                 """Print message to stderr."""
186                 sys.stderr.write('%s\n' % message)
187         
188         def fixed_template(self):
189                 """Checks if the output template is fixed."""
190                 return (re.search(ur'(?u)%\(.+?\)s', self._params['outtmpl']) is None)
191
192         def trouble(self, message=None):
193                 """Determine action to take when a download problem appears.
194
195                 Depending on if the downloader has been configured to ignore
196                 download errors or not, this method may throw an exception or
197                 not when errors are found, after printing the message. If it
198                 doesn't raise, it returns an error code suitable to be returned
199                 later as a program exit code to indicate error.
200                 """
201                 if message is not None:
202                         self.to_stderr(message)
203                 if not self._params.get('ignoreerrors', False):
204                         raise DownloadError(message)
205                 return 1
206
207         def slow_down(self, start_time, byte_counter):
208                 """Sleep if the download speed is over the rate limit."""
209                 rate_limit = self._params.get('ratelimit', None)
210                 if rate_limit is None or byte_counter == 0:
211                         return
212                 now = time.time()
213                 elapsed = now - start_time
214                 if elapsed <= 0.0:
215                         return
216                 speed = float(byte_counter) / elapsed
217                 if speed > rate_limit:
218                         time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
219
220         def report_destination(self, filename):
221                 """Report destination filename."""
222                 self.to_stdout('[download] Destination: %s' % filename)
223         
224         def report_progress(self, percent_str, data_len_str, speed_str, eta_str):
225                 """Report download progress."""
226                 self.to_stdout('\r[download] %s of %s at %s ETA %s' %
227                                 (percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
228         
229         def report_finish(self):
230                 """Report download finished."""
231                 self.to_stdout('')
232
233         def download(self, url_list):
234                 """Download a given list of URLs."""
235                 retcode = 0
236                 if len(url_list) > 1 and self.fixed_template():
237                         raise SameFileError(self._params['outtmpl'])
238
239                 for url in url_list:
240                         suitable_found = False
241                         for ie in self._ies:
242                                 if not ie.suitable(url):
243                                         continue
244                                 # Suitable InfoExtractor found
245                                 suitable_found = True
246                                 all_results = ie.extract(url)
247                                 results = [x for x in all_results if x is not None]
248                                 if len(results) != len(all_results):
249                                         retcode = self.trouble()
250
251                                 if len(results) > 1 and self.fixed_template():
252                                         raise SameFileError(self._params['outtmpl'])
253
254                                 for result in results:
255
256                                         # Forced printings
257                                         if self._params.get('forcetitle', False):
258                                                 print result['title']
259                                         if self._params.get('forceurl', False):
260                                                 print result['url']
261                                                 
262                                         # Do nothing else if in simulate mode
263                                         if self._params.get('simulate', False):
264                                                 continue
265
266                                         try:
267                                                 filename = self._params['outtmpl'] % result
268                                                 self.report_destination(filename)
269                                         except (ValueError, KeyError), err:
270                                                 retcode = self.trouble('ERROR: invalid output template: %s' % str(err))
271                                                 continue
272                                         try:
273                                                 self.pmkdir(filename)
274                                         except (OSError, IOError), err:
275                                                 retcode = self.trouble('ERROR: unable to create directories: %s' % str(err))
276                                                 continue
277                                         try:
278                                                 outstream = open(filename, 'wb')
279                                         except (OSError, IOError), err:
280                                                 retcode = self.trouble('ERROR: unable to open for writing: %s' % str(err))
281                                                 continue
282                                         try:
283                                                 self._do_download(outstream, result['url'])
284                                                 outstream.close()
285                                         except (OSError, IOError), err:
286                                                 retcode = self.trouble('ERROR: unable to write video data: %s' % str(err))
287                                                 continue
288                                         except (urllib2.URLError, httplib.HTTPException, socket.error), err:
289                                                 retcode = self.trouble('ERROR: unable to download video data: %s' % str(err))
290                                                 continue
291                                 break
292                         if not suitable_found:
293                                 retcode = self.trouble('ERROR: no suitable InfoExtractor: %s' % url)
294
295                 return retcode
296         
297         def _do_download(self, stream, url):
298                 request = urllib2.Request(url, None, std_headers)
299                 data = urllib2.urlopen(request)
300                 data_len = data.info().get('Content-length', None)
301                 data_len_str = self.format_bytes(data_len)
302                 byte_counter = 0
303                 block_size = 1024
304                 start = time.time()
305                 while True:
306                         # Progress message
307                         percent_str = self.calc_percent(byte_counter, data_len)
308                         eta_str = self.calc_eta(start, time.time(), data_len, byte_counter)
309                         speed_str = self.calc_speed(start, time.time(), byte_counter)
310                         self.report_progress(percent_str, data_len_str, speed_str, eta_str)
311
312                         # Download and write
313                         before = time.time()
314                         data_block = data.read(block_size)
315                         after = time.time()
316                         data_block_len = len(data_block)
317                         if data_block_len == 0:
318                                 break
319                         byte_counter += data_block_len
320                         stream.write(data_block)
321                         block_size = self.best_block_size(after - before, data_block_len)
322
323                         # Apply rate limit
324                         self.slow_down(start, byte_counter)
325
326                 self.report_finish()
327                 if data_len is not None and str(byte_counter) != data_len:
328                         raise ValueError('Content too short: %s/%s bytes' % (byte_counter, data_len))
329
330 class InfoExtractor(object):
331         """Information Extractor class.
332
333         Information extractors are the classes that, given a URL, extract
334         information from the video (or videos) the URL refers to. This
335         information includes the real video URL, the video title and simplified
336         title, author and others. It is returned in a list of dictionaries when
337         calling its extract() method. It is a list because a URL can refer to
338         more than one video (think of playlists). The dictionaries must include
339         the following fields:
340
341         id:             Video identifier.
342         url:            Final video URL.
343         uploader:       Nickname of the video uploader.
344         title:          Literal title.
345         stitle:         Simplified title.
346         ext:            Video filename extension.
347
348         Subclasses of this one should re-define the _real_initialize() and
349         _real_extract() methods, as well as the suitable() static method.
350         Probably, they should also be instantiated and added to the main
351         downloader.
352         """
353
354         _ready = False
355         _downloader = None
356
357         def __init__(self, downloader=None):
358                 """Constructor. Receives an optional downloader."""
359                 self._ready = False
360                 self.set_downloader(downloader)
361
362         @staticmethod
363         def suitable(url):
364                 """Receives a URL and returns True if suitable for this IE."""
365                 return True
366
367         def initialize(self):
368                 """Initializes an instance (authentication, etc)."""
369                 if not self._ready:
370                         self._real_initialize()
371                         self._ready = True
372
373         def extract(self, url):
374                 """Extracts URL information and returns it in list of dicts."""
375                 self.initialize()
376                 return self._real_extract(url)
377
378         def set_downloader(self, downloader):
379                 """Sets the downloader for this IE."""
380                 self._downloader = downloader
381         
382         def to_stdout(self, message):
383                 """Print message to stdout if downloader is not in quiet mode."""
384                 if self._downloader is None or not self._downloader.get_params().get('quiet', False):
385                         print message
386         
387         def to_stderr(self, message):
388                 """Print message to stderr."""
389                 sys.stderr.write('%s\n' % message)
390
391         def _real_initialize(self):
392                 """Real initialization process. Redefine in subclasses."""
393                 pass
394
395         def _real_extract(self, url):
396                 """Real extraction process. Redefine in subclasses."""
397                 pass
398
399 class YoutubeIE(InfoExtractor):
400         """Information extractor for youtube.com."""
401
402         _LOGIN_URL = 'http://www.youtube.com/login?next=/'
403         _AGE_URL = 'http://www.youtube.com/verify_age?next_url=/'
404         _NETRC_MACHINE = 'youtube'
405
406         def report_login(self):
407                 """Report attempt to log in."""
408                 self.to_stdout('[youtube] Logging in')
409         
410         def report_age_confirmation(self):
411                 """Report attempt to confirm age."""
412                 self.to_stdout('[youtube] Confirming age')
413         
414         def report_webpage_download(self, video_id):
415                 """Report attempt to download webpage."""
416                 self.to_stdout('[youtube] %s: Downloading video webpage' % video_id)
417         
418         def report_information_extraction(self, video_id):
419                 """Report attempt to extract video information."""
420                 self.to_stdout('[youtube] %s: Extracting video information' % video_id)
421         
422         def report_video_url(self, video_id, video_real_url):
423                 """Report extracted video URL."""
424                 self.to_stdout('[youtube] %s: URL: %s' % (video_id, video_real_url))
425
426         def _real_initialize(self):
427                 if self._downloader is None:
428                         return
429
430                 username = None
431                 password = None
432                 downloader_params = self._downloader.get_params()
433
434                 # Attempt to use provided username and password or .netrc data
435                 if downloader_params.get('username', None) is not None:
436                         username = downloader_params['username']
437                         password = downloader_params['password']
438                 elif downloader_params.get('usenetrc', False):
439                         try:
440                                 info = netrc.netrc().authenticators(self._NETRC_MACHINE)
441                                 if info is not None:
442                                         username = info[0]
443                                         password = info[2]
444                                 else:
445                                         raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
446                         except (IOError, netrc.NetrcParseError), err:
447                                 self.to_stderr('WARNING: parsing .netrc: %s' % str(err))
448                                 return
449
450                 # No authentication to be performed
451                 if username is None:
452                         return
453
454                 # Log in
455                 login_form = {
456                                 'current_form': 'loginForm',
457                                 'next':         '/',
458                                 'action_login': 'Log In',
459                                 'username':     username,
460                                 'password':     password,
461                                 }
462                 request = urllib2.Request(self._LOGIN_URL, urllib.urlencode(login_form), std_headers)
463                 try:
464                         self.report_login()
465                         login_results = urllib2.urlopen(request).read()
466                         if re.search(r'(?i)<form[^>]* name="loginForm"', login_results) is not None:
467                                 self.to_stderr('WARNING: unable to log in: bad username or password')
468                                 return
469                 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
470                         self.to_stderr('WARNING: unable to log in: %s' % str(err))
471                         return
472         
473                 # Confirm age
474                 age_form = {
475                                 'next_url':             '/',
476                                 'action_confirm':       'Confirm',
477                                 }
478                 request = urllib2.Request(self._AGE_URL, urllib.urlencode(age_form), std_headers)
479                 try:
480                         self.report_age_confirmation()
481                         age_results = urllib2.urlopen(request).read()
482                 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
483                         self.to_stderr('ERROR: unable to confirm age: %s' % str(err))
484                         return
485
486         def _real_extract(self, url):
487                 # Extract video id from URL
488                 mobj = re.match(r'^((?:http://)?(?:\w+\.)?youtube\.com/(?:(?:v/)|(?:(?:watch(?:\.php)?)?\?(?:.+&)?v=)))?([0-9A-Za-z_-]+)(?(1).+)?$', url)
489                 if mobj is None:
490                         self.to_stderr('ERROR: invalid URL: %s' % url)
491                         return [None]
492                 video_id = mobj.group(2)
493
494                 # Downloader parameters
495                 format_param = None
496                 if self._downloader is not None:
497                         params = self._downloader.get_params()
498                         format_param = params.get('format', None)
499
500                 # Extension
501                 video_extension = {'18': 'mp4', '17': '3gp'}.get(format_param, 'flv')
502
503                 # Normalize URL, including format
504                 normalized_url = 'http://www.youtube.com/watch?v=%s' % video_id
505                 if format_param is not None:
506                         normalized_url = '%s&fmt=%s' % (normalized_url, format_param)
507                 request = urllib2.Request(normalized_url, None, std_headers)
508                 try:
509                         self.report_webpage_download(video_id)
510                         video_webpage = urllib2.urlopen(request).read()
511                 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
512                         self.to_stderr('ERROR: unable to download video webpage: %s' % str(err))
513                         return [None]
514                 self.report_information_extraction(video_id)
515                 
516                 # "t" param
517                 mobj = re.search(r', "t": "([^"]+)"', video_webpage)
518                 if mobj is None:
519                         self.to_stderr('ERROR: unable to extract "t" parameter')
520                         return [None]
521                 video_real_url = 'http://www.youtube.com/get_video?video_id=%s&t=%s' % (video_id, mobj.group(1))
522                 if format_param is not None:
523                         video_real_url = '%s&fmt=%s' % (video_real_url, format_param)
524                 self.report_video_url(video_id, video_real_url)
525
526                 # uploader
527                 mobj = re.search(r'More From: ([^<]*)<', video_webpage)
528                 if mobj is None:
529                         self.to_stderr('ERROR: unable to extract uploader nickname')
530                         return [None]
531                 video_uploader = mobj.group(1)
532
533                 # title
534                 mobj = re.search(r'(?im)<title>YouTube - ([^<]*)</title>', video_webpage)
535                 if mobj is None:
536                         self.to_stderr('ERROR: unable to extract video title')
537                         return [None]
538                 video_title = mobj.group(1).decode('utf-8')
539                 video_title = re.sub(ur'(?u)&(.+?);', lambda x: unichr(htmlentitydefs.name2codepoint[x.group(1)]), video_title)
540                 video_title = video_title.replace(os.sep, u'%')
541
542                 # simplified title
543                 simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
544                 simple_title = simple_title.strip(ur'_')
545
546                 # Return information
547                 return [{
548                         'id':           video_id,
549                         'url':          video_real_url,
550                         'uploader':     video_uploader,
551                         'title':        video_title,
552                         'stitle':       simple_title,
553                         'ext':          video_extension,
554                         }]
555
556 if __name__ == '__main__':
557         try:
558                 # Modules needed only when running the main program
559                 import getpass
560                 import optparse
561
562                 # General configuration
563                 urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler()))
564                 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor()))
565                 socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
566
567                 # Parse command line
568                 parser = optparse.OptionParser(
569                                 usage='Usage: %prog [options] url...',
570                                 version='2008.07.22',
571                                 conflict_handler='resolve',
572                                 )
573                 parser.add_option('-h', '--help',
574                                 action='help', help='print this help text and exit')
575                 parser.add_option('-v', '--version',
576                                 action='version', help='print program version and exit')
577                 parser.add_option('-u', '--username',
578                                 dest='username', metavar='UN', help='account username')
579                 parser.add_option('-p', '--password',
580                                 dest='password', metavar='PW', help='account password')
581                 parser.add_option('-o', '--output',
582                                 dest='outtmpl', metavar='TPL', help='output filename template')
583                 parser.add_option('-q', '--quiet',
584                                 action='store_true', dest='quiet', help='activates quiet mode', default=False)
585                 parser.add_option('-s', '--simulate',
586                                 action='store_true', dest='simulate', help='do not download video', default=False)
587                 parser.add_option('-t', '--title',
588                                 action='store_true', dest='usetitle', help='use title in file name', default=False)
589                 parser.add_option('-l', '--literal',
590                                 action='store_true', dest='useliteral', help='use literal title in file name', default=False)
591                 parser.add_option('-n', '--netrc',
592                                 action='store_true', dest='usenetrc', help='use .netrc authentication data', default=False)
593                 parser.add_option('-g', '--get-url',
594                                 action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
595                 parser.add_option('-e', '--get-title',
596                                 action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
597                 parser.add_option('-f', '--format',
598                                 dest='format', metavar='FMT', help='video format code')
599                 parser.add_option('-b', '--best-quality',
600                                 action='store_const', dest='format', help='alias for -f 18', const='18')
601                 parser.add_option('-m', '--mobile-version',
602                                 action='store_const', dest='format', help='alias for -f 17', const='17')
603                 parser.add_option('-i', '--ignore-errors',
604                                 action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
605                 parser.add_option('-r', '--rate-limit',
606                                 dest='ratelimit', metavar='L', help='download rate limit (e.g. 50k or 44.6m)')
607                 (opts, args) = parser.parse_args()
608
609                 # Conflicting, missing and erroneous options
610                 if len(args) < 1:
611                         sys.exit('ERROR: you must provide at least one URL')
612                 if opts.usenetrc and (opts.username is not None or opts.password is not None):
613                         sys.exit('ERROR: using .netrc conflicts with giving username/password')
614                 if opts.password is not None and opts.username is None:
615                         sys.exit('ERROR: account username missing')
616                 if opts.outtmpl is not None and (opts.useliteral or opts.usetitle):
617                         sys.exit('ERROR: using output template conflicts with using title or literal title')
618                 if opts.usetitle and opts.useliteral:
619                         sys.exit('ERROR: using title conflicts with using literal title')
620                 if opts.username is not None and opts.password is None:
621                         opts.password = getpass.getpass('Type account password and press return:')
622                 if opts.ratelimit is not None:
623                         numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
624                         if numeric_limit is None:
625                                 sys.exit('ERROR: invalid rate limit specified')
626                         opts.ratelimit = numeric_limit
627
628                 # Information extractors
629                 youtube_ie = YoutubeIE()
630
631                 # File downloader
632                 fd = FileDownloader({
633                         'usenetrc': opts.usenetrc,
634                         'username': opts.username,
635                         'password': opts.password,
636                         'quiet': (opts.quiet or opts.geturl or opts.gettitle),
637                         'forceurl': opts.geturl,
638                         'forcetitle': opts.gettitle,
639                         'simulate': (opts.simulate or opts.geturl or opts.gettitle),
640                         'format': opts.format,
641                         'outtmpl': ((opts.outtmpl is not None and opts.outtmpl)
642                                 or (opts.usetitle and '%(stitle)s-%(id)s.%(ext)s')
643                                 or (opts.useliteral and '%(title)s-%(id)s.%(ext)s')
644                                 or '%(id)s.%(ext)s'),
645                         'ignoreerrors': opts.ignoreerrors,
646                         'ratelimit': opts.ratelimit,
647                         })
648                 fd.add_info_extractor(youtube_ie)
649                 retcode = fd.download(args)
650                 sys.exit(retcode)
651
652         except DownloadError:
653                 sys.exit(1)
654         except SameFileError:
655                 sys.exit('ERROR: fixed output name but more than one file to download')
656         except KeyboardInterrupt:
657                 sys.exit('\nERROR: Interrupted by user')