2 # -*- coding: utf-8 -*-
3 # Author: Ricardo Garcia Gonzalez
4 # License: Public domain code
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',
26 simple_title_chars = string.ascii_letters.decode('ascii') + string.digits.decode('ascii')
28 class DownloadError(Exception):
29 """Download Error exception.
31 This exception may be thrown by FileDownloader objects if they are not
32 configured to continue on errors. They will contain the appropriate
37 class SameFileError(Exception):
38 """Same File exception.
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.
45 class FileDownloader(object):
46 """File Downloader class.
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.
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.
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".
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.
87 def __init__(self, params):
88 """Create a FileDownloader object with the given options."""
90 self.set_params(params)
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 aggregate = ['%s%s' % (x, os.sep) for x in aggregate] # Finish names with separator
99 if not os.path.exists(dir):
103 def format_bytes(bytes):
109 exponent = long(math.log(float(bytes), 1024.0))
110 suffix = 'bkMGTPEZY'[exponent]
111 converted = float(bytes) / float(1024**exponent)
112 return '%.2f%s' % (converted, suffix)
115 def calc_percent(byte_counter, data_len):
118 return '%6s' % ('%3.1f%%' % (float(byte_counter) / float(data_len) * 100.0))
121 def calc_eta(start, now, total, current):
125 if current == 0 or dif < 0.001: # One millisecond
127 rate = float(current) / dif
128 eta = long((float(total) - float(current)) / rate)
129 (eta_mins, eta_secs) = divmod(eta, 60)
132 return '%02d:%02d' % (eta_mins, eta_secs)
135 def calc_speed(start, now, bytes):
137 if bytes == 0 or dif < 0.001: # One millisecond
138 return '%10s' % '---b/s'
139 return '%10s' % ('%s/s' % FileDownloader.format_bytes(float(bytes) / dif))
142 def best_block_size(elapsed_time, bytes):
143 new_min = max(bytes / 2.0, 1.0)
144 new_max = min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB
145 if elapsed_time < 0.001:
147 rate = bytes / elapsed_time
155 def parse_bytes(bytestr):
156 """Parse a string indicating a byte quantity into a long integer."""
157 matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
160 number = float(matchobj.group(1))
161 multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
162 return long(round(number * multiplier))
164 def set_params(self, params):
165 """Sets parameters."""
166 if type(params) != dict:
167 raise ValueError('params: dictionary expected')
168 self._params = params
170 def get_params(self):
171 """Get parameters."""
174 def add_info_extractor(self, ie):
175 """Add an InfoExtractor object to the end of the list."""
177 ie.set_downloader(self)
179 def to_stdout(self, message, skip_eol=False):
180 """Print message to stdout if not in quiet mode."""
181 if not self._params.get('quiet', False):
188 def to_stderr(self, message):
189 """Print message to stderr."""
190 print >>sys.stderr, message
192 def fixed_template(self):
193 """Checks if the output template is fixed."""
194 return (re.search(ur'(?u)%\(.+?\)s', self._params['outtmpl']) is None)
196 def trouble(self, message=None):
197 """Determine action to take when a download problem appears.
199 Depending on if the downloader has been configured to ignore
200 download errors or not, this method may throw an exception or
201 not when errors are found, after printing the message. If it
202 doesn't raise, it returns an error code suitable to be returned
203 later as a program exit code to indicate error.
205 if message is not None:
206 self.to_stderr(message)
207 if not self._params.get('ignoreerrors', False):
208 raise DownloadError(message)
211 def slow_down(self, start_time, byte_counter):
212 """Sleep if the download speed is over the rate limit."""
213 rate_limit = self._params.get('ratelimit', None)
214 if rate_limit is None or byte_counter == 0:
217 elapsed = now - start_time
220 speed = float(byte_counter) / elapsed
221 if speed > rate_limit:
222 time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
224 def report_destination(self, filename):
225 """Report destination filename."""
226 self.to_stdout(u'[download] Destination: %s' % filename)
228 def report_progress(self, percent_str, data_len_str, speed_str, eta_str):
229 """Report download progress."""
230 self.to_stdout(u'\r[download] %s of %s at %s ETA %s' %
231 (percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
233 def report_finish(self):
234 """Report download finished."""
237 def download(self, url_list):
238 """Download a given list of URLs."""
240 if len(url_list) > 1 and self.fixed_template():
241 raise SameFileError(self._params['outtmpl'])
244 suitable_found = False
246 if not ie.suitable(url):
248 # Suitable InfoExtractor found
249 suitable_found = True
250 all_results = ie.extract(url)
251 results = [x for x in all_results if x is not None]
252 if len(results) != len(all_results):
253 retcode = self.trouble()
255 if len(results) > 1 and self.fixed_template():
256 raise SameFileError(self._params['outtmpl'])
258 for result in results:
260 if self._params.get('forcetitle', False):
261 print result['title']
262 if self._params.get('forceurl', False):
265 # Do nothing else if in simulate mode
266 if self._params.get('simulate', False):
270 filename = self._params['outtmpl'] % result
271 self.report_destination(filename)
272 except (ValueError, KeyError), err:
273 retcode = self.trouble('ERROR: invalid output template: %s' % str(err))
276 self.pmkdir(filename)
277 except (OSError, IOError), err:
278 retcode = self.trouble('ERROR: unable to create directories: %s' % str(err))
281 outstream = open(filename, 'wb')
282 except (OSError, IOError), err:
283 retcode = self.trouble('ERROR: unable to open for writing: %s' % str(err))
286 self._do_download(outstream, result['url'])
288 except (OSError, IOError), err:
289 retcode = self.trouble('ERROR: unable to write video data: %s' % str(err))
291 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
292 retcode = self.trouble('ERROR: unable to download video data: %s' % str(err))
295 if not suitable_found:
296 retcode = self.trouble('ERROR: no suitable InfoExtractor: %s' % url)
300 def _do_download(self, stream, url):
301 request = urllib2.Request(url, None, std_headers)
302 data = urllib2.urlopen(request)
303 data_len = data.info().get('Content-length', None)
304 data_len_str = self.format_bytes(data_len)
310 percent_str = self.calc_percent(byte_counter, data_len)
311 eta_str = self.calc_eta(start, time.time(), data_len, byte_counter)
312 speed_str = self.calc_speed(start, time.time(), byte_counter)
313 self.report_progress(percent_str, data_len_str, speed_str, eta_str)
317 data_block = data.read(block_size)
319 data_block_len = len(data_block)
320 if data_block_len == 0:
322 byte_counter += data_block_len
323 stream.write(data_block)
324 block_size = self.best_block_size(after - before, data_block_len)
327 self.slow_down(start, byte_counter)
330 if data_len is not None and str(byte_counter) != data_len:
331 raise ValueError('Content too short: %s/%s bytes' % (byte_counter, data_len))
333 class InfoExtractor(object):
334 """Information Extractor class.
336 Information extractors are the classes that, given a URL, extract
337 information from the video (or videos) the URL refers to. This
338 information includes the real video URL, the video title and simplified
339 title, author and others. It is returned in a list of dictionaries when
340 calling its extract() method. It is a list because a URL can refer to
341 more than one video (think of playlists). The dictionaries must include
342 the following fields:
344 id: Video identifier.
345 url: Final video URL.
346 uploader: Nickname of the video uploader.
347 title: Literal title.
348 stitle: Simplified title.
349 ext: Video filename extension.
351 Subclasses of this one should re-define the _real_initialize() and
352 _real_extract() methods, as well as the suitable() static method.
353 Probably, they should also be instantiated and added to the main
360 def __init__(self, downloader=None):
361 """Constructor. Receives an optional downloader."""
363 self.set_downloader(downloader)
367 """Receives a URL and returns True if suitable for this IE."""
370 def initialize(self):
371 """Initializes an instance (authentication, etc)."""
373 self._real_initialize()
376 def extract(self, url):
377 """Extracts URL information and returns it in list of dicts."""
379 return self._real_extract(url)
381 def set_downloader(self, downloader):
382 """Sets the downloader for this IE."""
383 self._downloader = downloader
385 def to_stdout(self, message):
386 """Print message to stdout if downloader is not in quiet mode."""
387 if self._downloader is None or not self._downloader.get_params().get('quiet', False):
390 def to_stderr(self, message):
391 """Print message to stderr."""
392 print >>sys.stderr, message
394 def _real_initialize(self):
395 """Real initialization process. Redefine in subclasses."""
398 def _real_extract(self, url):
399 """Real extraction process. Redefine in subclasses."""
402 class YoutubeIE(InfoExtractor):
403 """Information extractor for youtube.com."""
405 _VALID_URL = r'^((?:http://)?(?:\w+\.)?youtube\.com/(?:(?:v/)|(?:(?:watch(?:\.php)?)?\?(?:.+&)?v=)))?([0-9A-Za-z_-]+)(?(1).+)?$'
406 _LOGIN_URL = 'http://www.youtube.com/login?next=/'
407 _AGE_URL = 'http://www.youtube.com/verify_age?next_url=/'
408 _NETRC_MACHINE = 'youtube'
412 return (re.match(YoutubeIE._VALID_URL, url) is not None)
414 def report_login(self):
415 """Report attempt to log in."""
416 self.to_stdout(u'[youtube] Logging in')
418 def report_age_confirmation(self):
419 """Report attempt to confirm age."""
420 self.to_stdout(u'[youtube] Confirming age')
422 def report_webpage_download(self, video_id):
423 """Report attempt to download webpage."""
424 self.to_stdout(u'[youtube] %s: Downloading video webpage' % video_id)
426 def report_information_extraction(self, video_id):
427 """Report attempt to extract video information."""
428 self.to_stdout(u'[youtube] %s: Extracting video information' % video_id)
430 def report_video_url(self, video_id, video_real_url):
431 """Report extracted video URL."""
432 self.to_stdout(u'[youtube] %s: URL: %s' % (video_id, video_real_url))
434 def _real_initialize(self):
435 if self._downloader is None:
440 downloader_params = self._downloader.get_params()
442 # Attempt to use provided username and password or .netrc data
443 if downloader_params.get('username', None) is not None:
444 username = downloader_params['username']
445 password = downloader_params['password']
446 elif downloader_params.get('usenetrc', False):
448 info = netrc.netrc().authenticators(self._NETRC_MACHINE)
453 raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
454 except (IOError, netrc.NetrcParseError), err:
455 self.to_stderr(u'WARNING: parsing .netrc: %s' % str(err))
458 # No authentication to be performed
464 'current_form': 'loginForm',
466 'action_login': 'Log In',
467 'username': username,
468 'password': password,
470 request = urllib2.Request(self._LOGIN_URL, urllib.urlencode(login_form), std_headers)
473 login_results = urllib2.urlopen(request).read()
474 if re.search(r'(?i)<form[^>]* name="loginForm"', login_results) is not None:
475 self.to_stderr(u'WARNING: unable to log in: bad username or password')
477 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
478 self.to_stderr(u'WARNING: unable to log in: %s' % str(err))
484 'action_confirm': 'Confirm',
486 request = urllib2.Request(self._AGE_URL, urllib.urlencode(age_form), std_headers)
488 self.report_age_confirmation()
489 age_results = urllib2.urlopen(request).read()
490 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
491 self.to_stderr(u'ERROR: unable to confirm age: %s' % str(err))
494 def _real_extract(self, url):
495 # Extract video id from URL
496 mobj = re.match(self._VALID_URL, url)
498 self.to_stderr(u'ERROR: invalid URL: %s' % url)
500 video_id = mobj.group(2)
502 # Downloader parameters
504 if self._downloader is not None:
505 params = self._downloader.get_params()
506 format_param = params.get('format', None)
509 video_extension = {'18': 'mp4', '17': '3gp'}.get(format_param, 'flv')
511 # Normalize URL, including format
512 normalized_url = 'http://www.youtube.com/watch?v=%s' % video_id
513 if format_param is not None:
514 normalized_url = '%s&fmt=%s' % (normalized_url, format_param)
515 request = urllib2.Request(normalized_url, None, std_headers)
517 self.report_webpage_download(video_id)
518 video_webpage = urllib2.urlopen(request).read()
519 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
520 self.to_stderr(u'ERROR: unable to download video webpage: %s' % str(err))
522 self.report_information_extraction(video_id)
525 mobj = re.search(r', "t": "([^"]+)"', video_webpage)
527 self.to_stderr(u'ERROR: unable to extract "t" parameter')
529 video_real_url = 'http://www.youtube.com/get_video?video_id=%s&t=%s' % (video_id, mobj.group(1))
530 if format_param is not None:
531 video_real_url = '%s&fmt=%s' % (video_real_url, format_param)
532 self.report_video_url(video_id, video_real_url)
535 mobj = re.search(r'More From: ([^<]*)<', video_webpage)
537 self.to_stderr(u'ERROR: unable to extract uploader nickname')
539 video_uploader = mobj.group(1)
542 mobj = re.search(r'(?im)<title>YouTube - ([^<]*)</title>', video_webpage)
544 self.to_stderr(u'ERROR: unable to extract video title')
546 video_title = mobj.group(1).decode('utf-8')
547 video_title = re.sub(ur'(?u)&(.+?);', lambda x: unichr(htmlentitydefs.name2codepoint[x.group(1)]), video_title)
548 video_title = video_title.replace(os.sep, u'%')
551 simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
552 simple_title = simple_title.strip(ur'_')
556 'id': video_id.decode('utf-8'),
557 'url': video_real_url.decode('utf-8'),
558 'uploader': video_uploader.decode('utf-8'),
559 'title': video_title,
560 'stitle': simple_title,
561 'ext': video_extension.decode('utf-8'),
564 class MetacafeIE(InfoExtractor):
565 """Information Extractor for metacafe.com."""
567 _VALID_URL = r'(?:http://)?(?:www\.)?metacafe\.com/watch/([^/]+)/([^/]+)/.*'
568 _DISCLAIMER = 'http://www.metacafe.com/disclaimer'
571 def __init__(self, youtube_ie, downloader=None):
572 InfoExtractor.__init__(self, downloader)
573 self._youtube_ie = youtube_ie
577 return (re.match(MetacafeIE._VALID_URL, url) is not None)
579 def report_disclaimer(self):
580 """Report disclaimer retrieval."""
581 self.to_stdout(u'[metacafe] Retrieving disclaimer')
583 def report_age_confirmation(self):
584 """Report attempt to confirm age."""
585 self.to_stdout(u'[metacafe] Confirming age')
587 def report_download_webpage(self, video_id):
588 """Report webpage download."""
589 self.to_stdout(u'[metacafe] %s: Downloading webpage' % video_id)
591 def report_extraction(self, video_id):
592 """Report information extraction."""
593 self.to_stdout(u'[metacafe] %s: Extracting information' % video_id)
595 def _real_initialize(self):
596 # Retrieve disclaimer
597 request = urllib2.Request(self._DISCLAIMER, None, std_headers)
599 self.report_disclaimer()
600 disclaimer = urllib2.urlopen(request).read()
601 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
602 self.to_stderr(u'ERROR: unable to retrieve disclaimer: %s' % str(err))
607 'allowAdultContent': '1',
608 'submit': "Continue - I'm over 18",
610 request = urllib2.Request('http://www.metacafe.com/watch/', urllib.urlencode(disclaimer_form), std_headers)
612 self.report_age_confirmation()
613 disclaimer = urllib2.urlopen(request).read()
614 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
615 self.to_stderr(u'ERROR: unable to confirm age: %s' % str(err))
618 def _real_extract(self, url):
619 # Extract id and simplified title from URL
620 mobj = re.match(self._VALID_URL, url)
622 self.to_stderr(u'ERROR: invalid URL: %s' % url)
625 video_id = mobj.group(1)
627 # Check if video comes from YouTube
628 mobj2 = re.match(r'^yt-(.*)$', video_id)
629 if mobj2 is not None:
630 return self._youtube_ie.extract('http://www.youtube.com/watch?v=%s' % mobj2.group(1))
632 simple_title = mobj.group(2).decode('utf-8')
633 video_extension = 'flv'
635 # Retrieve video webpage to extract further information
636 request = urllib2.Request('http://www.metacafe.com/watch/%s/' % video_id)
638 self.report_download_webpage(video_id)
639 webpage = urllib2.urlopen(request).read()
640 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
641 self.to_stderr(u'ERROR: unable retrieve video webpage: %s' % str(err))
644 # Extract URL, uploader and title from webpage
645 self.report_extraction(video_id)
646 mobj = re.search(r'(?m)"mediaURL":"(http.*?\.flv)"', webpage)
648 self.to_stderr(u'ERROR: unable to extract media URL')
650 mediaURL = mobj.group(1).replace('\\', '')
652 mobj = re.search(r'(?m)"gdaKey":"(.*?)"', webpage)
654 self.to_stderr(u'ERROR: unable to extract gdaKey')
656 gdaKey = mobj.group(1)
658 video_url = '%s?__gda__=%s' % (mediaURL, gdaKey)
660 mobj = re.search(r'(?im)<meta name="title" content="Metacafe - ([^"]+)"', webpage)
662 self.to_stderr(u'ERROR: unable to extract title')
664 video_title = mobj.group(1).decode('utf-8')
666 mobj = re.search(r'(?m)<li id="ChnlUsr">.*?Submitter:<br />(.*?)</li>', webpage)
668 self.to_stderr(u'ERROR: unable to extract uploader nickname')
670 video_uploader = re.sub(r'<.*?>', '', mobj.group(1))
674 'id': video_id.decode('utf-8'),
675 'url': video_url.decode('utf-8'),
676 'uploader': video_uploader.decode('utf-8'),
677 'title': video_title,
678 'stitle': simple_title,
679 'ext': video_extension.decode('utf-8'),
682 class YoutubePlaylistIE(InfoExtractor):
683 """Information Extractor for YouTube playlists."""
685 _VALID_URL = r'(?:http://)?(?:\w+\.)?youtube.com/view_play_list\?p=(.+)'
686 _TEMPLATE_URL = 'http://www.youtube.com/view_play_list?p=%s&page=%s'
687 _VIDEO_INDICATOR = r'/watch\?v=(.+?)&'
688 _MORE_PAGES_INDICATOR = r'class="pagerNotCurrent">Next</a>'
691 def __init__(self, youtube_ie, downloader=None):
692 InfoExtractor.__init__(self, downloader)
693 self._youtube_ie = youtube_ie
697 return (re.match(YoutubePlaylistIE._VALID_URL, url) is not None)
699 def report_download_page(self, playlist_id, pagenum):
700 """Report attempt to download playlist page with given number."""
701 self.to_stdout(u'[youtube] PL %s: Downloading page #%s' % (playlist_id, pagenum))
703 def _real_initialize(self):
704 self._youtube_ie.initialize()
706 def _real_extract(self, url):
707 # Extract playlist id
708 mobj = re.match(self._VALID_URL, url)
710 self.to_stderr(u'ERROR: invalid url: %s' % url)
713 # Download playlist pages
714 playlist_id = mobj.group(1)
719 self.report_download_page(playlist_id, pagenum)
720 request = urllib2.Request(self._TEMPLATE_URL % (playlist_id, pagenum), None, std_headers)
722 page = urllib2.urlopen(request).read()
723 except (urllib2.URLError, httplib.HTTPException, socket.error), err:
724 self.to_stderr(u'ERROR: unable to download webpage: %s' % str(err))
727 # Extract video identifiers
729 for mobj in re.finditer(self._VIDEO_INDICATOR, page):
730 ids_in_page.add(mobj.group(1))
731 video_ids.extend(list(ids_in_page))
733 if self._MORE_PAGES_INDICATOR not in page:
735 pagenum = pagenum + 1
739 information.extend(self._youtube_ie.extract('http://www.youtube.com/watch?v=%s' % id))
742 if __name__ == '__main__':
744 # Modules needed only when running the main program
748 # General configuration
749 urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler()))
750 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor()))
751 socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
754 parser = optparse.OptionParser(
755 usage='Usage: %prog [options] url...',
756 version='2008.07.22',
757 conflict_handler='resolve',
759 parser.add_option('-h', '--help',
760 action='help', help='print this help text and exit')
761 parser.add_option('-v', '--version',
762 action='version', help='print program version and exit')
763 parser.add_option('-u', '--username',
764 dest='username', metavar='UN', help='account username')
765 parser.add_option('-p', '--password',
766 dest='password', metavar='PW', help='account password')
767 parser.add_option('-o', '--output',
768 dest='outtmpl', metavar='TPL', help='output filename template')
769 parser.add_option('-q', '--quiet',
770 action='store_true', dest='quiet', help='activates quiet mode', default=False)
771 parser.add_option('-s', '--simulate',
772 action='store_true', dest='simulate', help='do not download video', default=False)
773 parser.add_option('-t', '--title',
774 action='store_true', dest='usetitle', help='use title in file name', default=False)
775 parser.add_option('-l', '--literal',
776 action='store_true', dest='useliteral', help='use literal title in file name', default=False)
777 parser.add_option('-n', '--netrc',
778 action='store_true', dest='usenetrc', help='use .netrc authentication data', default=False)
779 parser.add_option('-g', '--get-url',
780 action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
781 parser.add_option('-e', '--get-title',
782 action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
783 parser.add_option('-f', '--format',
784 dest='format', metavar='FMT', help='video format code')
785 parser.add_option('-b', '--best-quality',
786 action='store_const', dest='format', help='alias for -f 18', const='18')
787 parser.add_option('-m', '--mobile-version',
788 action='store_const', dest='format', help='alias for -f 17', const='17')
789 parser.add_option('-i', '--ignore-errors',
790 action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
791 parser.add_option('-r', '--rate-limit',
792 dest='ratelimit', metavar='L', help='download rate limit (e.g. 50k or 44.6m)')
793 (opts, args) = parser.parse_args()
795 # Conflicting, missing and erroneous options
797 sys.exit(u'ERROR: you must provide at least one URL')
798 if opts.usenetrc and (opts.username is not None or opts.password is not None):
799 sys.exit(u'ERROR: using .netrc conflicts with giving username/password')
800 if opts.password is not None and opts.username is None:
801 sys.exit(u'ERROR: account username missing')
802 if opts.outtmpl is not None and (opts.useliteral or opts.usetitle):
803 sys.exit(u'ERROR: using output template conflicts with using title or literal title')
804 if opts.usetitle and opts.useliteral:
805 sys.exit(u'ERROR: using title conflicts with using literal title')
806 if opts.username is not None and opts.password is None:
807 opts.password = getpass.getpass(u'Type account password and press return:')
808 if opts.ratelimit is not None:
809 numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
810 if numeric_limit is None:
811 sys.exit(u'ERROR: invalid rate limit specified')
812 opts.ratelimit = numeric_limit
814 # Information extractors
815 youtube_ie = YoutubeIE()
816 metacafe_ie = MetacafeIE(youtube_ie)
817 youtube_pl_ie = YoutubePlaylistIE(youtube_ie)
820 fd = FileDownloader({
821 'usenetrc': opts.usenetrc,
822 'username': opts.username,
823 'password': opts.password,
824 'quiet': (opts.quiet or opts.geturl or opts.gettitle),
825 'forceurl': opts.geturl,
826 'forcetitle': opts.gettitle,
827 'simulate': (opts.simulate or opts.geturl or opts.gettitle),
828 'format': opts.format,
829 'outtmpl': ((opts.outtmpl is not None and opts.outtmpl.decode())
830 or (opts.usetitle and u'%(stitle)s-%(id)s.%(ext)s')
831 or (opts.useliteral and u'%(title)s-%(id)s.%(ext)s')
832 or u'%(id)s.%(ext)s'),
833 'ignoreerrors': opts.ignoreerrors,
834 'ratelimit': opts.ratelimit,
836 fd.add_info_extractor(youtube_pl_ie)
837 fd.add_info_extractor(metacafe_ie)
838 fd.add_info_extractor(youtube_ie)
839 retcode = fd.download(args)
842 except DownloadError:
844 except SameFileError:
845 sys.exit(u'ERROR: fixed output name but more than one file to download')
846 except KeyboardInterrupt:
847 sys.exit(u'\nERROR: Interrupted by user')