3 from __future__ 
import unicode_literals
 
  12 from .common 
import InfoExtractor
, SearchInfoExtractor
 
  13 from .subtitles 
import SubtitlesInfoExtractor
 
  14 from ..jsinterp 
import JSInterpreter
 
  15 from ..swfinterp 
import SWFInterpreter
 
  20     compat_urllib_request
, 
  26     get_element_by_attribute
, 
  36 class YoutubeBaseInfoExtractor(InfoExtractor
): 
  37     """Provide base functions for Youtube extractors""" 
  38     _LOGIN_URL 
= 'https://accounts.google.com/ServiceLogin' 
  39     _TWOFACTOR_URL 
= 'https://accounts.google.com/SecondFactor' 
  40     _LANG_URL 
= r
'https://www.youtube.com/?hl=en&persist_hl=1&gl=US&persist_gl=1&opt_out_ackd=1' 
  41     _AGE_URL 
= 'https://www.youtube.com/verify_age?next_url=/&gl=US&hl=en' 
  42     _NETRC_MACHINE 
= 'youtube' 
  43     # If True it will raise an error if no login info is provided 
  44     _LOGIN_REQUIRED 
= False 
  46     def _set_language(self
): 
  47         return bool(self
._download
_webpage
( 
  49             note
='Setting language', errnote
='unable to set language', 
  54         Attempt to log in to YouTube. 
  55         True is returned if successful or skipped. 
  56         False is returned if login failed. 
  58         If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised. 
  60         (username
, password
) = self
._get
_login
_info
() 
  61         # No authentication to be performed 
  63             if self
._LOGIN
_REQUIRED
: 
  64                 raise ExtractorError('No login info available, needed for using %s.' % self
.IE_NAME
, expected
=True) 
  67         login_page 
= self
._download
_webpage
( 
  68             self
._LOGIN
_URL
, None, 
  69             note
='Downloading login page', 
  70             errnote
='unable to fetch login page', fatal
=False) 
  71         if login_page 
is False: 
  74         galx 
= self
._search
_regex
(r
'(?s)<input.+?name="GALX".+?value="(.+?)"', 
  75                                   login_page
, 'Login GALX parameter') 
  79                 'continue': 'https://www.youtube.com/signin?action_handle_signin=true&feature=sign_in_button&hl=en_US&nomobiletemp=1', 
  84                 'PersistentCookie': 'yes', 
  86                 'bgresponse': 'js_disabled', 
  87                 'checkConnection': '', 
  88                 'checkedDomains': 'youtube', 
 100         # Convert to UTF-8 *before* urlencode because Python 2.x's urlencode 
 102         login_form 
= dict((k
.encode('utf-8'), v
.encode('utf-8')) for k
,v 
in login_form_strs
.items()) 
 103         login_data 
= compat_urllib_parse
.urlencode(login_form
).encode('ascii') 
 105         req 
= compat_urllib_request
.Request(self
._LOGIN
_URL
, login_data
) 
 106         login_results 
= self
._download
_webpage
( 
 108             note
='Logging in', errnote
='unable to log in', fatal
=False) 
 109         if login_results 
is False: 
 112         if re
.search(r
'id="errormsg_0_Passwd"', login_results
) is not None: 
 113             raise ExtractorError('Please use your account password and a two-factor code instead of an application-specific password.', expected
=True) 
 116         # TODO add SMS and phone call support - these require making a request and then prompting the user 
 118         if re
.search(r
'(?i)<form[^>]* id="gaia_secondfactorform"', login_results
) is not None: 
 119             tfa_code 
= self
._get
_tfa
_info
() 
 122                 self
._downloader
.report_warning('Two-factor authentication required. Provide it with --twofactor <code>') 
 123                 self
._downloader
.report_warning('(Note that only TOTP (Google Authenticator App) codes work at this time.)') 
 126             # Unlike the first login form, secTok and timeStmp are both required for the TFA form 
 128             match 
= re
.search(r
'id="secTok"\n\s+value=\'(.+)\'/>', login_results, re.M | re.U) 
 130                 self._downloader.report_warning('Failed to get secTok 
- did the page structure change?
') 
 131             secTok = match.group(1) 
 132             match = re.search(r'id="timeStmp"\n\s
+value
=\'(.+)\'/>', login_results, re.M | re.U) 
 134                 self._downloader.report_warning('Failed to get timeStmp 
- did the page structure change?
') 
 135             timeStmp = match.group(1) 
 138                 'continue': 'https
://www
.youtube
.com
/signin?action_handle_signin
=true
&feature
=sign_in_button
&hl
=en_US
&nomobiletemp
=1', 
 140                 'smsUserPin
': tfa_code, 
 141                 'smsVerifyPin
': 'Verify
', 
 143                 'PersistentCookie
': 'yes
', 
 144                 'checkConnection
': '', 
 145                 'checkedDomains
': 'youtube
', 
 148                 'timeStmp
': timeStmp, 
 149                 'service
': 'youtube
', 
 152             tfa_form = dict((k.encode('utf
-8'), v.encode('utf
-8')) for k,v in tfa_form_strs.items()) 
 153             tfa_data = compat_urllib_parse.urlencode(tfa_form).encode('ascii
') 
 155             tfa_req = compat_urllib_request.Request(self._TWOFACTOR_URL, tfa_data) 
 156             tfa_results = self._download_webpage( 
 158                 note='Submitting TFA code
', errnote='unable to submit tfa
', fatal=False) 
 160             if tfa_results is False: 
 163             if re.search(r'(?i
)<form
[^
>]* id="gaia_secondfactorform"', tfa_results) is not None: 
 164                 self._downloader.report_warning('Two
-factor code expired
. Please 
try again
, or use a one
-use backup code instead
.') 
 166             if re.search(r'(?i
)<form
[^
>]* id="gaia_loginform"', tfa_results) is not None: 
 167                 self._downloader.report_warning('unable to log 
in - did the page structure change?
') 
 169             if re.search(r'smsauth
-interstitial
-reviewsettings
', tfa_results) is not None: 
 170                 self._downloader.report_warning('Your Google account has a security notice
. Please log 
in on your web browser
, resolve the notice
, and try again
.') 
 173         if re.search(r'(?i
)<form
[^
>]* id="gaia_loginform"', login_results) is not None: 
 174             self._downloader.report_warning('unable to log 
in: bad username 
or password
') 
 178     def _confirm_age(self): 
 181             'action_confirm
': 'Confirm
', 
 183         req = compat_urllib_request.Request(self._AGE_URL, 
 184             compat_urllib_parse.urlencode(age_form).encode('ascii
')) 
 186         self._download_webpage( 
 188             note='Confirming age
', errnote='Unable to confirm age
', 
 191     def _real_initialize(self): 
 192         if self._downloader is None: 
 194         if self._get_login_info()[0] is not None: 
 195             if not self._set_language(): 
 197         if not self._login(): 
 202 class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor): 
 203     IE_DESC = 'YouTube
.com
' 
 204     _VALID_URL = r"""(?x)^ 
 206                          (?:https?://|//)                                    # http(s):// or protocol-independent URL 
 207                          (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/| 
 208                             (?:www\.)?deturl\.com/www\.youtube\.com/| 
 209                             (?:www\.)?pwnyoutube\.com/| 
 210                             (?:www\.)?yourepeat\.com/| 
 211                             tube\.majestyc\.net/| 
 212                             youtube\.googleapis\.com/)                        # the various hostnames, with wildcard subdomains 
 213                          (?:.*?\#/)?                                          # handle anchor (#/) redirect urls 
 214                          (?:                                                  # the various things that can precede the ID: 
 215                              (?:(?:v|embed|e)/(?!videoseries))                # v/ or embed/ or e/ 
 216                              |(?:                                             # or the v= param in all its forms 
 217                                  (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)?  # preceding watch(_popup|.php) or nothing (like /?v=xxxx) 
 218                                  (?:\?|\#!?)                                  # the params delimiter ? or # or #! 
 219                                  (?:.*?&)?                                    # any other preceding param (like /?s=tuff&v=xxxx) 
 223                          |youtu\.be/                                          # just youtu.be/xxxx 
 224                          |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId= 
 226                      )?                                                       # all until now is optional -> you can pass the naked ID 
 227                      ([0-9A-Za-z_-]{11})                                      # here is it! the YouTube video ID 
 228                      (?!.*?&list=)                                            # combined list/video URLs are handled by the playlist IE 
 229                      (?(1).+)?                                                # if we found the ID, everything can follow 
 231     _NEXT_URL_RE = r'[\?&]next_url
=([^
&]+)' 
 233         '5': {'ext
': 'flv
', 'width
': 400, 'height
': 240}, 
 234         '6': {'ext
': 'flv
', 'width
': 450, 'height
': 270}, 
 235         '13': {'ext
': '3gp
'}, 
 236         '17': {'ext
': '3gp
', 'width
': 176, 'height
': 144}, 
 237         '18': {'ext
': 'mp4
', 'width
': 640, 'height
': 360}, 
 238         '22': {'ext
': 'mp4
', 'width
': 1280, 'height
': 720}, 
 239         '34': {'ext
': 'flv
', 'width
': 640, 'height
': 360}, 
 240         '35': {'ext
': 'flv
', 'width
': 854, 'height
': 480}, 
 241         '36': {'ext
': '3gp
', 'width
': 320, 'height
': 240}, 
 242         '37': {'ext
': 'mp4
', 'width
': 1920, 'height
': 1080}, 
 243         '38': {'ext
': 'mp4
', 'width
': 4096, 'height
': 3072}, 
 244         '43': {'ext
': 'webm
', 'width
': 640, 'height
': 360}, 
 245         '44': {'ext
': 'webm
', 'width
': 854, 'height
': 480}, 
 246         '45': {'ext
': 'webm
', 'width
': 1280, 'height
': 720}, 
 247         '46': {'ext
': 'webm
', 'width
': 1920, 'height
': 1080}, 
 251         '82': {'ext
': 'mp4
', 'height
': 360, 'format_note
': '3D
', 'preference
': -20}, 
 252         '83': {'ext
': 'mp4
', 'height
': 480, 'format_note
': '3D
', 'preference
': -20}, 
 253         '84': {'ext
': 'mp4
', 'height
': 720, 'format_note
': '3D
', 'preference
': -20}, 
 254         '85': {'ext
': 'mp4
', 'height
': 1080, 'format_note
': '3D
', 'preference
': -20}, 
 255         '100': {'ext
': 'webm
', 'height
': 360, 'format_note
': '3D
', 'preference
': -20}, 
 256         '101': {'ext
': 'webm
', 'height
': 480, 'format_note
': '3D
', 'preference
': -20}, 
 257         '102': {'ext
': 'webm
', 'height
': 720, 'format_note
': '3D
', 'preference
': -20}, 
 259         # Apple HTTP Live Streaming 
 260         '92': {'ext
': 'mp4
', 'height
': 240, 'format_note
': 'HLS
', 'preference
': -10}, 
 261         '93': {'ext
': 'mp4
', 'height
': 360, 'format_note
': 'HLS
', 'preference
': -10}, 
 262         '94': {'ext
': 'mp4
', 'height
': 480, 'format_note
': 'HLS
', 'preference
': -10}, 
 263         '95': {'ext
': 'mp4
', 'height
': 720, 'format_note
': 'HLS
', 'preference
': -10}, 
 264         '96': {'ext
': 'mp4
', 'height
': 1080, 'format_note
': 'HLS
', 'preference
': -10}, 
 265         '132': {'ext
': 'mp4
', 'height
': 240, 'format_note
': 'HLS
', 'preference
': -10}, 
 266         '151': {'ext
': 'mp4
', 'height
': 72, 'format_note
': 'HLS
', 'preference
': -10}, 
 269         '133': {'ext
': 'mp4
', 'height
': 240, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 270         '134': {'ext
': 'mp4
', 'height
': 360, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 271         '135': {'ext
': 'mp4
', 'height
': 480, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 272         '136': {'ext
': 'mp4
', 'height
': 720, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 273         '137': {'ext
': 'mp4
', 'height
': 1080, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 274         '138': {'ext
': 'mp4
', 'height
': 2160, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 275         '160': {'ext
': 'mp4
', 'height
': 144, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 276         '264': {'ext
': 'mp4
', 'height
': 1440, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 279         '139': {'ext
': 'm4a
', 'format_note
': 'DASH audio
', 'vcodec
': 'none
', 'abr
': 48, 'preference
': -50}, 
 280         '140': {'ext
': 'm4a
', 'format_note
': 'DASH audio
', 'vcodec
': 'none
', 'abr
': 128, 'preference
': -50}, 
 281         '141': {'ext
': 'm4a
', 'format_note
': 'DASH audio
', 'vcodec
': 'none
', 'abr
': 256, 'preference
': -50}, 
 284         '167': {'ext
': 'webm
', 'height
': 360, 'width
': 640, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'container
': 'webm
', 'vcodec
': 'VP8
', 'preference
': -40}, 
 285         '168': {'ext
': 'webm
', 'height
': 480, 'width
': 854, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'container
': 'webm
', 'vcodec
': 'VP8
', 'preference
': -40}, 
 286         '169': {'ext
': 'webm
', 'height
': 720, 'width
': 1280, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'container
': 'webm
', 'vcodec
': 'VP8
', 'preference
': -40}, 
 287         '170': {'ext
': 'webm
', 'height
': 1080, 'width
': 1920, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'container
': 'webm
', 'vcodec
': 'VP8
', 'preference
': -40}, 
 288         '218': {'ext
': 'webm
', 'height
': 480, 'width
': 854, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'container
': 'webm
', 'vcodec
': 'VP8
', 'preference
': -40}, 
 289         '219': {'ext
': 'webm
', 'height
': 480, 'width
': 854, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'container
': 'webm
', 'vcodec
': 'VP8
', 'preference
': -40}, 
 290         '278': {'ext
': 'webm
', 'height
': 144, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40, 'container
': 'webm
', 'vcodec
': 'VP9
'}, 
 291         '242': {'ext
': 'webm
', 'height
': 240, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 292         '243': {'ext
': 'webm
', 'height
': 360, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 293         '244': {'ext
': 'webm
', 'height
': 480, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 294         '245': {'ext
': 'webm
', 'height
': 480, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 295         '246': {'ext
': 'webm
', 'height
': 480, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 296         '247': {'ext
': 'webm
', 'height
': 720, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 297         '248': {'ext
': 'webm
', 'height
': 1080, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 298         '271': {'ext
': 'webm
', 'height
': 1440, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 299         '272': {'ext
': 'webm
', 'height
': 2160, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40}, 
 300         '302': {'ext
': 'webm
', 'height
': 720, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40, 'fps
': 60, 'vcodec
': 'VP9
'}, 
 301         '303': {'ext
': 'webm
', 'height
': 1080, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40, 'fps
': 60, 'vcodec
': 'VP9
'}, 
 304         '171': {'ext
': 'webm
', 'vcodec
': 'none
', 'format_note
': 'DASH audio
', 'abr
': 128, 'preference
': -50}, 
 305         '172': {'ext
': 'webm
', 'vcodec
': 'none
', 'format_note
': 'DASH audio
', 'abr
': 256, 'preference
': -50}, 
 308         '298': {'ext
': 'mov
', 'height
': 720, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40, 'fps
': 60, 'vcodec
': 'h264
'}, 
 309         '299': {'ext
': 'mov
', 'height
': 1080, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40, 'fps
': 60, 'vcodec
': 'h264
'}, 
 310         '266': {'ext
': 'mov
', 'height
': 2160, 'format_note
': 'DASH video
', 'acodec
': 'none
', 'preference
': -40, 'vcodec
': 'h264
'}, 
 313         '_rtmp
': {'protocol
': 'rtmp
'}, 
 319             'url
': 'http
://www
.youtube
.com
/watch?v
=BaW_jenozKc
', 
 323                 'title
': 'youtube
-dl test video 
"\'/\\ä↭𝕐', 
 324                 'uploader': 'Philipp Hagemeister', 
 325                 'uploader_id': 'phihag', 
 326                 'upload_date': '20121002', 
 327                 'description': 'test chars:  "\'/\\ä↭𝕐
\ntest URL
: https
://github
.com
/rg3
/youtube
-dl
/issues
/1892\n\nThis 
is a test video 
for youtube
-dl
.\n\nFor more information
, contact phihag
@phihag.de .', 
 328                 'categories
': ['Science 
& Technology
'], 
 330                 'dislike_count
': int, 
 334             'url
': 'http
://www
.youtube
.com
/watch?v
=UxxajLWwzqY
', 
 335             'note
': 'Test generic use_cipher_signature 
video (#897)', 
 339                 'upload_date': '20120506', 
 340                 'title': 'Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]', 
 341                 'description': 'md5:fea86fda2d5a5784273df5c7cc994d9f', 
 342                 'uploader': 'Icona Pop', 
 343                 'uploader_id': 'IconaPop', 
 347             'url': 'https://www.youtube.com/watch?v=07FYdnEawAQ', 
 348             'note': 'Test VEVO video with age protection (#956)', 
 352                 'upload_date': '20130703', 
 353                 'title': 'Justin Timberlake - Tunnel Vision (Explicit)', 
 354                 'description': 'md5:64249768eec3bc4276236606ea996373', 
 355                 'uploader': 'justintimberlakeVEVO', 
 356                 'uploader_id': 'justintimberlakeVEVO', 
 360             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ', 
 361             'note': 'Embed-only video (#1746)', 
 365                 'upload_date': '20120608', 
 366                 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012', 
 367                 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7', 
 368                 'uploader': 'SET India', 
 369                 'uploader_id': 'setindia' 
 373             'url': 'http://www.youtube.com/watch?v=a9LDPn-MO4I', 
 374             'note': '256k DASH audio (format 141) via DASH manifest', 
 378                 'upload_date': '20121002', 
 379                 'uploader_id': '8KVIDEO', 
 381                 'uploader': '8KVIDEO', 
 382                 'title': 'UHDTV TEST 8K VIDEO.mp4' 
 385                 'youtube_include_dash_manifest': True, 
 389         # DASH manifest with encrypted signature 
 391             'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA', 
 395                 'title': 'Afrojack - The Spark ft. Spree Wilson', 
 396                 'description': 'md5:9717375db5a9a3992be4668bbf3bc0a8', 
 397                 'uploader': 'AfrojackVEVO', 
 398                 'uploader_id': 'AfrojackVEVO', 
 399                 'upload_date': '20131011', 
 402                 'youtube_include_dash_manifest': True, 
 408     def __init__(self
, *args
, **kwargs
): 
 409         super(YoutubeIE
, self
).__init
__(*args
, **kwargs
) 
 410         self
._player
_cache 
= {} 
 412     def report_video_info_webpage_download(self
, video_id
): 
 413         """Report attempt to download video info webpage.""" 
 414         self
.to_screen('%s: Downloading video info webpage' % video_id
) 
 416     def report_information_extraction(self
, video_id
): 
 417         """Report attempt to extract video information.""" 
 418         self
.to_screen('%s: Extracting video information' % video_id
) 
 420     def report_unavailable_format(self
, video_id
, format
): 
 421         """Report extracted video URL.""" 
 422         self
.to_screen('%s: Format %s not available' % (video_id
, format
)) 
 424     def report_rtmp_download(self
): 
 425         """Indicate the download will use the RTMP protocol.""" 
 426         self
.to_screen('RTMP download detected') 
 428     def _signature_cache_id(self
, example_sig
): 
 429         """ Return a string representation of a signature """ 
 430         return '.'.join(compat_str(len(part
)) for part 
in example_sig
.split('.')) 
 432     def _extract_signature_function(self
, video_id
, player_url
, example_sig
): 
 434             r
'.*-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.(?P<ext>[a-z]+)$', 
 437             raise ExtractorError('Cannot identify player %r' % player_url
) 
 438         player_type 
= id_m
.group('ext') 
 439         player_id 
= id_m
.group('id') 
 441         # Read from filesystem cache 
 442         func_id 
= '%s_%s_%s' % ( 
 443             player_type
, player_id
, self
._signature
_cache
_id
(example_sig
)) 
 444         assert os
.path
.basename(func_id
) == func_id
 
 446         cache_spec 
= self
._downloader
.cache
.load('youtube-sigfuncs', func_id
) 
 447         if cache_spec 
is not None: 
 448             return lambda s
: ''.join(s
[i
] for i 
in cache_spec
) 
 450         if player_type 
== 'js': 
 451             code 
= self
._download
_webpage
( 
 452                 player_url
, video_id
, 
 453                 note
='Downloading %s player %s' % (player_type
, player_id
), 
 454                 errnote
='Download of %s failed' % player_url
) 
 455             res 
= self
._parse
_sig
_js
(code
) 
 456         elif player_type 
== 'swf': 
 457             urlh 
= self
._request
_webpage
( 
 458                 player_url
, video_id
, 
 459                 note
='Downloading %s player %s' % (player_type
, player_id
), 
 460                 errnote
='Download of %s failed' % player_url
) 
 462             res 
= self
._parse
_sig
_swf
(code
) 
 464             assert False, 'Invalid player type %r' % player_type
 
 466         if cache_spec 
is None: 
 467             test_string 
= ''.join(map(compat_chr
, range(len(example_sig
)))) 
 468             cache_res 
= res(test_string
) 
 469             cache_spec 
= [ord(c
) for c 
in cache_res
] 
 471         self
._downloader
.cache
.store('youtube-sigfuncs', func_id
, cache_spec
) 
 474     def _print_sig_code(self
, func
, example_sig
): 
 475         def gen_sig_code(idxs
): 
 476             def _genslice(start
, end
, step
): 
 477                 starts 
= '' if start 
== 0 else str(start
) 
 478                 ends 
= (':%d' % (end
+step
)) if end 
+ step 
>= 0 else ':' 
 479                 steps 
= '' if step 
== 1 else (':%d' % step
) 
 480                 return 's[%s%s%s]' % (starts
, ends
, steps
) 
 483             start 
= '(Never used)'  # Quelch pyflakes warnings - start will be 
 484                                     # set as soon as step is set 
 485             for i
, prev 
in zip(idxs
[1:], idxs
[:-1]): 
 489                     yield _genslice(start
, prev
, step
) 
 492                 if i 
- prev 
in [-1, 1]: 
 501                 yield _genslice(start
, i
, step
) 
 503         test_string 
= ''.join(map(compat_chr
, range(len(example_sig
)))) 
 504         cache_res 
= func(test_string
) 
 505         cache_spec 
= [ord(c
) for c 
in cache_res
] 
 506         expr_code 
= ' + '.join(gen_sig_code(cache_spec
)) 
 507         signature_id_tuple 
= '(%s)' % ( 
 508             ', '.join(compat_str(len(p
)) for p 
in example_sig
.split('.'))) 
 509         code 
= ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n' 
 510                 '    return %s\n') % (signature_id_tuple
, expr_code
) 
 511         self
.to_screen('Extracted signature function:\n' + code
) 
 513     def _parse_sig_js(self
, jscode
): 
 514         funcname 
= self
._search
_regex
( 
 515             r
'signature=([$a-zA-Z]+)', jscode
, 
 516              'Initial JS player signature function name') 
 518         jsi 
= JSInterpreter(jscode
) 
 519         initial_function 
= jsi
.extract_function(funcname
) 
 520         return lambda s
: initial_function([s
]) 
 522     def _parse_sig_swf(self
, file_contents
): 
 523         swfi 
= SWFInterpreter(file_contents
) 
 524         TARGET_CLASSNAME 
= 'SignatureDecipher' 
 525         searched_class 
= swfi
.extract_class(TARGET_CLASSNAME
) 
 526         initial_function 
= swfi
.extract_function(searched_class
, 'decipher') 
 527         return lambda s
: initial_function([s
]) 
 529     def _decrypt_signature(self
, s
, video_id
, player_url
, age_gate
=False): 
 530         """Turn the encrypted s field into a working signature""" 
 532         if player_url 
is None: 
 533             raise ExtractorError('Cannot decrypt signature without player_url') 
 535         if player_url
.startswith('//'): 
 536             player_url 
= 'https:' + player_url
 
 538             player_id 
= (player_url
, self
._signature
_cache
_id
(s
)) 
 539             if player_id 
not in self
._player
_cache
: 
 540                 func 
= self
._extract
_signature
_function
( 
 541                     video_id
, player_url
, s
 
 543                 self
._player
_cache
[player_id
] = func
 
 544             func 
= self
._player
_cache
[player_id
] 
 545             if self
._downloader
.params
.get('youtube_print_sig_code'): 
 546                 self
._print
_sig
_code
(func
, s
) 
 548         except Exception as e
: 
 549             tb 
= traceback
.format_exc() 
 550             raise ExtractorError( 
 551                 'Signature extraction failed: ' + tb
, cause
=e
) 
 553     def _get_available_subtitles(self
, video_id
, webpage
): 
 555             sub_list 
= self
._download
_webpage
( 
 556                 'https://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id
, 
 557                 video_id
, note
=False) 
 558         except ExtractorError 
as err
: 
 559             self
._downloader
.report_warning('unable to download video subtitles: %s' % compat_str(err
)) 
 561         lang_list 
= re
.findall(r
'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list
) 
 566             if lang 
in sub_lang_list
: 
 568             params 
= compat_urllib_parse
.urlencode({ 
 571                 'fmt': self
._downloader
.params
.get('subtitlesformat', 'srt'), 
 572                 'name': unescapeHTML(l
[0]).encode('utf-8'), 
 574             url 
= 'https://www.youtube.com/api/timedtext?' + params
 
 575             sub_lang_list
[lang
] = url
 
 576         if not sub_lang_list
: 
 577             self
._downloader
.report_warning('video doesn\'t have subtitles') 
 581     def _get_available_automatic_caption(self
, video_id
, webpage
): 
 582         """We need the webpage for getting the captions url, pass it as an 
 583            argument to speed up the process.""" 
 584         sub_format 
= self
._downloader
.params
.get('subtitlesformat', 'srt') 
 585         self
.to_screen('%s: Looking for automatic captions' % video_id
) 
 586         mobj 
= re
.search(r
';ytplayer.config = ({.*?});', webpage
) 
 587         err_msg 
= 'Couldn\'t find automatic captions for %s' % video_id
 
 589             self
._downloader
.report_warning(err_msg
) 
 591         player_config 
= json
.loads(mobj
.group(1)) 
 593             args 
= player_config
[u
'args'] 
 594             caption_url 
= args
[u
'ttsurl'] 
 595             timestamp 
= args
[u
'timestamp'] 
 596             # We get the available subtitles 
 597             list_params 
= compat_urllib_parse
.urlencode({ 
 602             list_url 
= caption_url 
+ '&' + list_params
 
 603             caption_list 
= self
._download
_xml
(list_url
, video_id
) 
 604             original_lang_node 
= caption_list
.find('track') 
 605             if original_lang_node 
is None or original_lang_node
.attrib
.get('kind') != 'asr' : 
 606                 self
._downloader
.report_warning('Video doesn\'t have automatic captions') 
 608             original_lang 
= original_lang_node
.attrib
['lang_code'] 
 611             for lang_node 
in caption_list
.findall('target'): 
 612                 sub_lang 
= lang_node
.attrib
['lang_code'] 
 613                 params 
= compat_urllib_parse
.urlencode({ 
 614                     'lang': original_lang
, 
 620                 sub_lang_list
[sub_lang
] = caption_url 
+ '&' + params
 
 622         # An extractor error can be raise by the download process if there are 
 623         # no automatic captions but there are subtitles 
 624         except (KeyError, ExtractorError
): 
 625             self
._downloader
.report_warning(err_msg
) 
 629     def extract_id(cls
, url
): 
 630         mobj 
= re
.match(cls
._VALID
_URL
, url
, re
.VERBOSE
) 
 632             raise ExtractorError('Invalid URL: %s' % url
) 
 633         video_id 
= mobj
.group(2) 
 636     def _extract_from_m3u8(self
, manifest_url
, video_id
): 
 638         def _get_urls(_manifest
): 
 639             lines 
= _manifest
.split('\n') 
 640             urls 
= filter(lambda l
: l 
and not l
.startswith('#'), 
 643         manifest 
= self
._download
_webpage
(manifest_url
, video_id
, 'Downloading formats manifest') 
 644         formats_urls 
= _get_urls(manifest
) 
 645         for format_url 
in formats_urls
: 
 646             itag 
= self
._search
_regex
(r
'itag/(\d+?)/', format_url
, 'itag') 
 647             url_map
[itag
] = format_url
 
 650     def _extract_annotations(self
, video_id
): 
 651         url 
= 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
 
 652         return self
._download
_webpage
(url
, video_id
, note
='Searching for annotations.', errnote
='Unable to download video annotations.') 
 654     def _real_extract(self
, url
): 
 656             'http' if self
._downloader
.params
.get('prefer_insecure', False) 
 659         # Extract original video URL from URL with redirection, like age verification, using next_url parameter 
 660         mobj 
= re
.search(self
._NEXT
_URL
_RE
, url
) 
 662             url 
= proto 
+ '://www.youtube.com/' + compat_urllib_parse
.unquote(mobj
.group(1)).lstrip('/') 
 663         video_id 
= self
.extract_id(url
) 
 666         url 
= proto 
+ '://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1' % video_id
 
 668             c 
for c 
in self
._downloader
.cookiejar
 
 669             if c
.domain 
== '.youtube.com' and c
.name 
== 'PREF'] 
 670         for pc 
in pref_cookies
: 
 671             if 'hl=' in pc
.value
: 
 672                 pc
.value 
= re
.sub(r
'hl=[^&]+', 'hl=en', pc
.value
) 
 677         video_webpage 
= self
._download
_webpage
(url
, video_id
) 
 679         # Attempt to extract SWF player URL 
 680         mobj 
= re
.search(r
'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage
) 
 682             player_url 
= re
.sub(r
'\\(.)', r
'\1', mobj
.group(1)) 
 687         self
.report_video_info_webpage_download(video_id
) 
 688         if re
.search(r
'player-age-gate-content">', video_webpage
) is not None: 
 689             self
.report_age_confirmation() 
 691             # We simulate the access to the video from www.youtube.com/v/{video_id} 
 692             # this can be viewed without login into Youtube 
 693             data 
= compat_urllib_parse
.urlencode({ 
 694                 'video_id': video_id
, 
 695                 'eurl': 'https://youtube.googleapis.com/v/' + video_id
, 
 696                 'sts': self
._search
_regex
( 
 697                     r
'"sts"\s*:\s*(\d+)', video_webpage
, 'sts'), 
 699             video_info_url 
= proto 
+ '://www.youtube.com/get_video_info?' + data
 
 700             video_info_webpage 
= self
._download
_webpage
(video_info_url
, video_id
, 
 702                                     errnote
='unable to download video info webpage') 
 703             video_info 
= compat_parse_qs(video_info_webpage
) 
 706             for el_type 
in ['&el=embedded', '&el=detailpage', '&el=vevo', '']: 
 707                 video_info_url 
= (proto 
+ '://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en' 
 708                         % (video_id
, el_type
)) 
 709                 video_info_webpage 
= self
._download
_webpage
(video_info_url
, video_id
, 
 711                                         errnote
='unable to download video info webpage') 
 712                 video_info 
= compat_parse_qs(video_info_webpage
) 
 713                 if 'token' in video_info
: 
 715         if 'token' not in video_info
: 
 716             if 'reason' in video_info
: 
 717                 raise ExtractorError( 
 718                     'YouTube said: %s' % video_info
['reason'][0], 
 719                     expected
=True, video_id
=video_id
) 
 721                 raise ExtractorError( 
 722                     '"token" parameter not in video info for unknown reason', 
 725         if 'view_count' in video_info
: 
 726             view_count 
= int(video_info
['view_count'][0]) 
 730         # Check for "rental" videos 
 731         if 'ypc_video_rental_bar_text' in video_info 
and 'author' not in video_info
: 
 732             raise ExtractorError('"rental" videos not supported') 
 734         # Start extracting information 
 735         self
.report_information_extraction(video_id
) 
 738         if 'author' not in video_info
: 
 739             raise ExtractorError('Unable to extract uploader name') 
 740         video_uploader 
= compat_urllib_parse
.unquote_plus(video_info
['author'][0]) 
 743         video_uploader_id 
= None 
 744         mobj 
= re
.search(r
'<link itemprop="url" href="http://www.youtube.com/(?:user|channel)/([^"]+)">', video_webpage
) 
 746             video_uploader_id 
= mobj
.group(1) 
 748             self
._downloader
.report_warning('unable to extract uploader nickname') 
 751         if 'title' in video_info
: 
 752             video_title 
= video_info
['title'][0] 
 754             self
._downloader
.report_warning('Unable to extract video title') 
 758         # We try first to get a high quality image: 
 759         m_thumb 
= re
.search(r
'<span itemprop="thumbnail".*?href="(.*?)">', 
 760                             video_webpage
, re
.DOTALL
) 
 761         if m_thumb 
is not None: 
 762             video_thumbnail 
= m_thumb
.group(1) 
 763         elif 'thumbnail_url' not in video_info
: 
 764             self
._downloader
.report_warning('unable to extract video thumbnail') 
 765             video_thumbnail 
= None 
 766         else:   # don't panic if we can't find it 
 767             video_thumbnail 
= compat_urllib_parse
.unquote_plus(video_info
['thumbnail_url'][0]) 
 771         mobj 
= re
.search(r
'(?s)id="eow-date.*?>(.*?)</span>', video_webpage
) 
 774                 r
'(?s)id="watch-uploader-info".*?>.*?(?:Published|Uploaded|Streamed live) on (.*?)</strong>', 
 777             upload_date 
= ' '.join(re
.sub(r
'[/,-]', r
' ', mobj
.group(1)).split()) 
 778             upload_date 
= unified_strdate(upload_date
) 
 780         m_cat_container 
= self
._search
_regex
( 
 781             r
'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>', 
 782             video_webpage
, 'categories', fatal
=False) 
 784             category 
= self
._html
_search
_regex
( 
 785                 r
'(?s)<a[^<]+>(.*?)</a>', m_cat_container
, 'category', 
 787             video_categories 
= None if category 
is None else [category
] 
 789             video_categories 
= None 
 792         video_description 
= get_element_by_id("eow-description", video_webpage
) 
 793         if video_description
: 
 794             video_description 
= re
.sub(r
'''(?x) 
 796                     (?:[a-zA-Z-]+="[^"]+"\s+)*? 
 798                     (?:[a-zA-Z-]+="[^"]+"\s+)*? 
 799                     class="yt-uix-redirect-link"\s*> 
 802             ''', r
'\1', video_description
) 
 803             video_description 
= clean_html(video_description
) 
 805             fd_mobj 
= re
.search(r
'<meta name="description" content="([^"]+)"', video_webpage
) 
 807                 video_description 
= unescapeHTML(fd_mobj
.group(1)) 
 809                 video_description 
= '' 
 811         def _extract_count(count_name
): 
 812             count 
= self
._search
_regex
( 
 813                 r
'id="watch-%s"[^>]*>.*?([\d,]+)\s*</span>' % re
.escape(count_name
), 
 814                 video_webpage
, count_name
, default
=None) 
 815             if count 
is not None: 
 816                 return int(count
.replace(',', '')) 
 818         like_count 
= _extract_count('like') 
 819         dislike_count 
= _extract_count('dislike') 
 822         video_subtitles 
= self
.extract_subtitles(video_id
, video_webpage
) 
 824         if self
._downloader
.params
.get('listsubtitles', False): 
 825             self
._list
_available
_subtitles
(video_id
, video_webpage
) 
 828         if 'length_seconds' not in video_info
: 
 829             self
._downloader
.report_warning('unable to extract video duration') 
 830             video_duration 
= None 
 832             video_duration 
= int(compat_urllib_parse
.unquote_plus(video_info
['length_seconds'][0])) 
 835         video_annotations 
= None 
 836         if self
._downloader
.params
.get('writeannotations', False): 
 837                 video_annotations 
= self
._extract
_annotations
(video_id
) 
 839         # Decide which formats to download 
 841             mobj 
= re
.search(r
';ytplayer\.config\s*=\s*({.*?});', video_webpage
) 
 843                 raise ValueError('Could not find vevo ID') 
 844             json_code 
= uppercase_escape(mobj
.group(1)) 
 845             ytplayer_config 
= json
.loads(json_code
) 
 846             args 
= ytplayer_config
['args'] 
 847             # Easy way to know if the 's' value is in url_encoded_fmt_stream_map 
 848             # this signatures are encrypted 
 849             if 'url_encoded_fmt_stream_map' not in args
: 
 850                 raise ValueError('No stream_map present')  # caught below 
 851             re_signature 
= re
.compile(r
'[&,]s=') 
 852             m_s 
= re_signature
.search(args
['url_encoded_fmt_stream_map']) 
 854                 self
.to_screen('%s: Encrypted signatures detected.' % video_id
) 
 855                 video_info
['url_encoded_fmt_stream_map'] = [args
['url_encoded_fmt_stream_map']] 
 856             m_s 
= re_signature
.search(args
.get('adaptive_fmts', '')) 
 858                 if 'adaptive_fmts' in video_info
: 
 859                     video_info
['adaptive_fmts'][0] += ',' + args
['adaptive_fmts'] 
 861                     video_info
['adaptive_fmts'] = [args
['adaptive_fmts']] 
 865         def _map_to_format_list(urlmap
): 
 867             for itag
, video_real_url 
in urlmap
.items(): 
 870                     'url': video_real_url
, 
 871                     'player_url': player_url
, 
 873                 if itag 
in self
._formats
: 
 874                     dct
.update(self
._formats
[itag
]) 
 878         if 'conn' in video_info 
and video_info
['conn'][0].startswith('rtmp'): 
 879             self
.report_rtmp_download() 
 881                 'format_id': '_rtmp', 
 883                 'url': video_info
['conn'][0], 
 884                 'player_url': player_url
, 
 886         elif len(video_info
.get('url_encoded_fmt_stream_map', [])) >= 1 or len(video_info
.get('adaptive_fmts', [])) >= 1: 
 887             encoded_url_map 
= video_info
.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info
.get('adaptive_fmts',[''])[0] 
 888             if 'rtmpe%3Dyes' in encoded_url_map
: 
 889                 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected
=True) 
 891             for url_data_str 
in encoded_url_map
.split(','): 
 892                 url_data 
= compat_parse_qs(url_data_str
) 
 893                 if 'itag' not in url_data 
or 'url' not in url_data
: 
 895                 format_id 
= url_data
['itag'][0] 
 896                 url 
= url_data
['url'][0] 
 898                 if 'sig' in url_data
: 
 899                     url 
+= '&signature=' + url_data
['sig'][0] 
 900                 elif 's' in url_data
: 
 901                     encrypted_sig 
= url_data
['s'][0] 
 904                         jsplayer_url_json 
= self
._search
_regex
( 
 905                             r
'"assets":.+?"js":\s*("[^"]+")', 
 906                             video_webpage
, 'JS player URL') 
 907                         player_url 
= json
.loads(jsplayer_url_json
) 
 908                     if player_url 
is None: 
 909                         player_url_json 
= self
._search
_regex
( 
 910                             r
'ytplayer\.config.*?"url"\s*:\s*("[^"]+")', 
 911                             video_webpage
, 'age gate player URL') 
 912                         player_url 
= json
.loads(player_url_json
) 
 914                     if self
._downloader
.params
.get('verbose'): 
 915                         if player_url 
is None: 
 916                             player_version 
= 'unknown' 
 917                             player_desc 
= 'unknown' 
 919                             if player_url
.endswith('swf'): 
 920                                 player_version 
= self
._search
_regex
( 
 921                                     r
'-(.+?)(?:/watch_as3)?\.swf$', player_url
, 
 922                                     'flash player', fatal
=False) 
 923                                 player_desc 
= 'flash player %s' % player_version
 
 925                                 player_version 
= self
._search
_regex
( 
 926                                     r
'html5player-([^/]+?)(?:/html5player)?\.js', 
 928                                     'html5 player', fatal
=False) 
 929                                 player_desc 
= 'html5 player %s' % player_version
 
 931                         parts_sizes 
= self
._signature
_cache
_id
(encrypted_sig
) 
 932                         self
.to_screen('{%s} signature length %s, %s' % 
 933                             (format_id
, parts_sizes
, player_desc
)) 
 935                     signature 
= self
._decrypt
_signature
( 
 936                         encrypted_sig
, video_id
, player_url
, age_gate
) 
 937                     url 
+= '&signature=' + signature
 
 938                 if 'ratebypass' not in url
: 
 939                     url 
+= '&ratebypass=yes' 
 940                 url_map
[format_id
] = url
 
 941             formats 
= _map_to_format_list(url_map
) 
 942         elif video_info
.get('hlsvp'): 
 943             manifest_url 
= video_info
['hlsvp'][0] 
 944             url_map 
= self
._extract
_from
_m
3u8(manifest_url
, video_id
) 
 945             formats 
= _map_to_format_list(url_map
) 
 947             raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info') 
 949         # Look for the DASH manifest 
 950         if self
._downloader
.params
.get('youtube_include_dash_manifest', True): 
 952                 # The DASH manifest used needs to be the one from the original video_webpage. 
 953                 # The one found in get_video_info seems to be using different signatures. 
 954                 # However, in the case of an age restriction there won't be any embedded dashmpd in the video_webpage. 
 955                 # Luckily, it seems, this case uses some kind of default signature (len == 86), so the 
 956                 # combination of get_video_info and the _static_decrypt_signature() decryption fallback will work here. 
 958                     dash_manifest_url 
= video_info
.get('dashmpd')[0] 
 960                     dash_manifest_url 
= ytplayer_config
['args']['dashmpd'] 
 961                 def decrypt_sig(mobj
): 
 963                     dec_s 
= self
._decrypt
_signature
(s
, video_id
, player_url
, age_gate
) 
 964                     return '/signature/%s' % dec_s
 
 965                 dash_manifest_url 
= re
.sub(r
'/s/([\w\.]+)', decrypt_sig
, dash_manifest_url
) 
 966                 dash_doc 
= self
._download
_xml
( 
 967                     dash_manifest_url
, video_id
, 
 968                     note
='Downloading DASH manifest', 
 969                     errnote
='Could not download DASH manifest') 
 970                 for r 
in dash_doc
.findall('.//{urn:mpeg:DASH:schema:MPD:2011}Representation'): 
 971                     url_el 
= r
.find('{urn:mpeg:DASH:schema:MPD:2011}BaseURL') 
 974                     format_id 
= r
.attrib
['id'] 
 975                     video_url 
= url_el
.text
 
 976                     filesize 
= int_or_none(url_el
.attrib
.get('{http://youtube.com/yt/2012/10/10}contentLength')) 
 978                         'format_id': format_id
, 
 980                         'width': int_or_none(r
.attrib
.get('width')), 
 981                         'tbr': int_or_none(r
.attrib
.get('bandwidth'), 1000), 
 982                         'asr': int_or_none(r
.attrib
.get('audioSamplingRate')), 
 983                         'filesize': filesize
, 
 986                         existing_format 
= next( 
 988                             if fo
['format_id'] == format_id
) 
 989                     except StopIteration: 
 990                         f
.update(self
._formats
.get(format_id
, {})) 
 993                         existing_format
.update(f
) 
 995             except (ExtractorError
, KeyError) as e
: 
 996                 self
.report_warning('Skipping DASH manifest: %s' % e
, video_id
) 
 998         self
._sort
_formats
(formats
) 
1002             'uploader':     video_uploader
, 
1003             'uploader_id':  video_uploader_id
, 
1004             'upload_date':  upload_date
, 
1005             'title':        video_title
, 
1006             'thumbnail':    video_thumbnail
, 
1007             'description':  video_description
, 
1008             'categories':   video_categories
, 
1009             'subtitles':    video_subtitles
, 
1010             'duration':     video_duration
, 
1011             'age_limit':    18 if age_gate 
else 0, 
1012             'annotations':  video_annotations
, 
1013             'webpage_url': proto 
+ '://www.youtube.com/watch?v=%s' % video_id
, 
1014             'view_count':   view_count
, 
1015             'like_count': like_count
, 
1016             'dislike_count': dislike_count
, 
1020 class YoutubePlaylistIE(YoutubeBaseInfoExtractor
): 
1021     IE_DESC 
= 'YouTube.com playlists' 
1022     _VALID_URL 
= r
"""(?x)(?: 
1027                            (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/videoseries) 
1028                            \? (?:.*?&)*? (?:p|a|list)= 
1032                             (?:PL|LL|EC|UU|FL|RD)?[0-9A-Za-z-_]{10,} 
1033                             # Top tracks, they can also include dots  
1038                         ((?:PL|LL|EC|UU|FL|RD)[0-9A-Za-z-_]{10,}) 
1040     _TEMPLATE_URL 
= 'https://www.youtube.com/playlist?list=%s' 
1041     _MORE_PAGES_INDICATOR 
= r
'data-link-type="next"' 
1042     _VIDEO_RE 
= r
'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&[^"]*?index=(?P<index>\d+)' 
1043     IE_NAME 
= 'youtube:playlist' 
1045         'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re', 
1047             'title': 'ytdl test PL', 
1049         'playlist_count': 3, 
1051         'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx', 
1053             'title': 'YDL_Empty_List', 
1055         'playlist_count': 0, 
1057         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.', 
1058         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC', 
1060             'title': '29C3: Not my department', 
1062         'playlist_count': 95, 
1064         'note': 'issue #673', 
1065         'url': 'PLBB231211A4F62143', 
1067             'title': '[OLD]Team Fortress 2 (Class-based LP)', 
1069         'playlist_mincount': 26, 
1071         'note': 'Large playlist', 
1072         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q', 
1074             'title': 'Uploads from Cauchemar', 
1076         'playlist_mincount': 799, 
1078         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl', 
1080             'title': 'YDL_safe_search', 
1082         'playlist_count': 2, 
1085         'url': 'http://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu', 
1086         'playlist_count': 4, 
1091         'note': 'Embedded SWF player', 
1092         'url': 'http://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0', 
1093         'playlist_count': 4, 
1099     def _real_initialize(self
): 
1102     def _ids_to_results(self
, ids
): 
1104             self
.url_result(vid_id
, 'Youtube', video_id
=vid_id
) 
1107     def _extract_mix(self
, playlist_id
): 
1108         # The mixes are generated from a a single video 
1109         # the id of the playlist is just 'RD' + video_id 
1110         url 
= 'https://youtube.com/watch?v=%s&list=%s' % (playlist_id
[-11:], playlist_id
) 
1111         webpage 
= self
._download
_webpage
( 
1112             url
, playlist_id
, 'Downloading Youtube mix') 
1113         search_title 
= lambda class_name
: get_element_by_attribute('class', class_name
, webpage
) 
1115             search_title('playlist-title') or 
1116             search_title('title long-title') or 
1117             search_title('title')) 
1118         title 
= clean_html(title_span
) 
1119         ids 
= orderedSet(re
.findall( 
1120             r
'''(?xs)data-video-username=".*?".*? 
1121                        href="/watch\?v=([0-9A-Za-z_-]{11})&[^"]*?list=%s''' % re
.escape(playlist_id
), 
1123         url_results 
= self
._ids
_to
_results
(ids
) 
1125         return self
.playlist_result(url_results
, playlist_id
, title
) 
1127     def _real_extract(self
, url
): 
1128         # Extract playlist id 
1129         mobj 
= re
.match(self
._VALID
_URL
, url
) 
1131             raise ExtractorError('Invalid URL: %s' % url
) 
1132         playlist_id 
= mobj
.group(1) or mobj
.group(2) 
1134         # Check if it's a video-specific URL 
1135         query_dict 
= compat_urlparse
.parse_qs(compat_urlparse
.urlparse(url
).query
) 
1136         if 'v' in query_dict
: 
1137             video_id 
= query_dict
['v'][0] 
1138             if self
._downloader
.params
.get('noplaylist'): 
1139                 self
.to_screen('Downloading just video %s because of --no-playlist' % video_id
) 
1140                 return self
.url_result(video_id
, 'Youtube', video_id
=video_id
) 
1142                 self
.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id
, video_id
)) 
1144         if playlist_id
.startswith('RD'): 
1145             # Mixes require a custom extraction process 
1146             return self
._extract
_mix
(playlist_id
) 
1147         if playlist_id
.startswith('TL'): 
1148             raise ExtractorError('For downloading YouTube.com top lists, use ' 
1149                 'the "yttoplist" keyword, for example "youtube-dl \'yttoplist:music:Top Tracks\'"', expected
=True) 
1151         url 
= self
._TEMPLATE
_URL 
% playlist_id
 
1152         page 
= self
._download
_webpage
(url
, playlist_id
) 
1153         more_widget_html 
= content_html 
= page
 
1155         # Check if the playlist exists or is private 
1156         if re
.search(r
'<div class="yt-alert-message">[^<]*?(The|This) playlist (does not exist|is private)[^<]*?</div>', page
) is not None: 
1157             raise ExtractorError( 
1158                 'The playlist doesn\'t exist or is private, use --username or ' 
1159                 '--netrc to access it.', 
1162         # Extract the video ids from the playlist pages 
1165         for page_num 
in itertools
.count(1): 
1166             matches 
= re
.finditer(self
._VIDEO
_RE
, content_html
) 
1167             # We remove the duplicates and the link with index 0 
1168             # (it's not the first video of the playlist) 
1169             new_ids 
= orderedSet(m
.group('id') for m 
in matches 
if m
.group('index') != '0') 
1172             mobj 
= re
.search(r
'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html
) 
1176             more 
= self
._download
_json
( 
1177                 'https://youtube.com/%s' % mobj
.group('more'), playlist_id
, 
1178                 'Downloading page #%s' % page_num
, 
1179                 transform_source
=uppercase_escape
) 
1180             content_html 
= more
['content_html'] 
1181             more_widget_html 
= more
['load_more_widget_html'] 
1183         playlist_title 
= self
._html
_search
_regex
( 
1184             r
'(?s)<h1 class="pl-header-title[^"]*">\s*(.*?)\s*</h1>', 
1187         url_results 
= self
._ids
_to
_results
(ids
) 
1188         return self
.playlist_result(url_results
, playlist_id
, playlist_title
) 
1191 class YoutubeTopListIE(YoutubePlaylistIE
): 
1192     IE_NAME 
= 'youtube:toplist' 
1193     IE_DESC 
= ('YouTube.com top lists, "yttoplist:{channel}:{list title}"' 
1194         ' (Example: "yttoplist:music:Top Tracks")') 
1195     _VALID_URL 
= r
'yttoplist:(?P<chann>.*?):(?P<title>.*?)$' 
1197         'url': 'yttoplist:music:Trending', 
1198         'playlist_mincount': 5, 
1199         'skip': 'Only works for logged-in users', 
1202     def _real_extract(self
, url
): 
1203         mobj 
= re
.match(self
._VALID
_URL
, url
) 
1204         channel 
= mobj
.group('chann') 
1205         title 
= mobj
.group('title') 
1206         query 
= compat_urllib_parse
.urlencode({'title': title
}) 
1207         channel_page 
= self
._download
_webpage
( 
1208             'https://www.youtube.com/%s' % channel
, title
) 
1209         link 
= self
._html
_search
_regex
( 
1211                 <a\s+href="([^"]+)".*?>\s* 
1212                 <span\s+class="branded-page-module-title-text">\s* 
1213                 <span[^>]*>.*?%s.*?</span>''' % re
.escape(query
), 
1214             channel_page
, 'list') 
1215         url 
= compat_urlparse
.urljoin('https://www.youtube.com/', link
) 
1217         video_re 
= r
'data-index="\d+".*?data-video-id="([0-9A-Za-z_-]{11})"' 
1219         # sometimes the webpage doesn't contain the videos 
1220         # retry until we get them 
1221         for i 
in itertools
.count(0): 
1222             msg 
= 'Downloading Youtube mix' 
1224                 msg 
+= ', retry #%d' % i
 
1226             webpage 
= self
._download
_webpage
(url
, title
, msg
) 
1227             ids 
= orderedSet(re
.findall(video_re
, webpage
)) 
1230         url_results 
= self
._ids
_to
_results
(ids
) 
1231         return self
.playlist_result(url_results
, playlist_title
=title
) 
1234 class YoutubeChannelIE(InfoExtractor
): 
1235     IE_DESC 
= 'YouTube.com channels' 
1236     _VALID_URL 
= r
"^(?:https?://)?(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/([0-9A-Za-z_-]+)" 
1237     _MORE_PAGES_INDICATOR 
= 'yt-uix-load-more' 
1238     _MORE_PAGES_URL 
= 'https://www.youtube.com/c4_browse_ajax?action_load_more_videos=1&flow=list&paging=%s&view=0&sort=da&channel_id=%s' 
1239     IE_NAME 
= 'youtube:channel' 
1241         'note': 'paginated channel', 
1242         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w', 
1243         'playlist_mincount': 91, 
1246     def extract_videos_from_page(self
, page
): 
1248         for mobj 
in re
.finditer(r
'href="/watch\?v=([0-9A-Za-z_-]+)&?', page
): 
1249             if mobj
.group(1) not in ids_in_page
: 
1250                 ids_in_page
.append(mobj
.group(1)) 
1253     def _real_extract(self
, url
): 
1254         # Extract channel id 
1255         mobj 
= re
.match(self
._VALID
_URL
, url
) 
1257             raise ExtractorError('Invalid URL: %s' % url
) 
1259         # Download channel page 
1260         channel_id 
= mobj
.group(1) 
1262         url 
= 'https://www.youtube.com/channel/%s/videos' % channel_id
 
1263         channel_page 
= self
._download
_webpage
(url
, channel_id
) 
1264         autogenerated 
= re
.search(r
'''(?x) 
1266                     channel-header-autogenerated-label| 
1267                     yt-channel-title-autogenerated 
1268                 )[^"]*"''', channel_page
) is not None 
1271             # The videos are contained in a single page 
1272             # the ajax pages can't be used, they are empty 
1273             video_ids 
= self
.extract_videos_from_page(channel_page
) 
1275             # Download all channel pages using the json-based channel_ajax query 
1276             for pagenum 
in itertools
.count(1): 
1277                 url 
= self
._MORE
_PAGES
_URL 
% (pagenum
, channel_id
) 
1278                 page 
= self
._download
_json
( 
1279                     url
, channel_id
, note
='Downloading page #%s' % pagenum
, 
1280                     transform_source
=uppercase_escape
) 
1282                 ids_in_page 
= self
.extract_videos_from_page(page
['content_html']) 
1283                 video_ids
.extend(ids_in_page
) 
1285                 if self
._MORE
_PAGES
_INDICATOR 
not in page
['load_more_widget_html']: 
1288         self
._downloader
.to_screen('[youtube] Channel %s: Found %i videos' % (channel_id
, len(video_ids
))) 
1290         url_entries 
= [self
.url_result(video_id
, 'Youtube', video_id
=video_id
) 
1291                        for video_id 
in video_ids
] 
1292         return self
.playlist_result(url_entries
, channel_id
) 
1295 class YoutubeUserIE(InfoExtractor
): 
1296     IE_DESC 
= 'YouTube.com user videos (URL or "ytuser" keyword)' 
1297     _VALID_URL 
= r
'(?:(?:(?:https?://)?(?:\w+\.)?youtube\.com/(?:user/)?(?!(?:attribution_link|watch|results)(?:$|[^a-z_A-Z0-9-])))|ytuser:)(?!feed/)([A-Za-z0-9_-]+)' 
1298     _TEMPLATE_URL 
= 'https://gdata.youtube.com/feeds/api/users/%s' 
1299     _GDATA_PAGE_SIZE 
= 50 
1300     _GDATA_URL 
= 'https://gdata.youtube.com/feeds/api/users/%s/uploads?max-results=%d&start-index=%d&alt=json' 
1301     IE_NAME 
= 'youtube:user' 
1304         'url': 'https://www.youtube.com/user/TheLinuxFoundation', 
1305         'playlist_mincount': 320, 
1307             'title': 'TheLinuxFoundation', 
1310         'url': 'ytuser:phihag', 
1311         'only_matching': True, 
1315     def suitable(cls
, url
): 
1316         # Don't return True if the url can be extracted with other youtube 
1317         # extractor, the regex would is too permissive and it would match. 
1318         other_ies 
= iter(klass 
for (name
, klass
) in globals().items() if name
.endswith('IE') and klass 
is not cls
) 
1319         if any(ie
.suitable(url
) for ie 
in other_ies
): return False 
1320         else: return super(YoutubeUserIE
, cls
).suitable(url
) 
1322     def _real_extract(self
, url
): 
1324         mobj 
= re
.match(self
._VALID
_URL
, url
) 
1326             raise ExtractorError('Invalid URL: %s' % url
) 
1328         username 
= mobj
.group(1) 
1330         # Download video ids using YouTube Data API. Result size per 
1331         # query is limited (currently to 50 videos) so we need to query 
1332         # page by page until there are no video ids - it means we got 
1335         def download_page(pagenum
): 
1336             start_index 
= pagenum 
* self
._GDATA
_PAGE
_SIZE 
+ 1 
1338             gdata_url 
= self
._GDATA
_URL 
% (username
, self
._GDATA
_PAGE
_SIZE
, start_index
) 
1339             page 
= self
._download
_webpage
( 
1340                 gdata_url
, username
, 
1341                 'Downloading video ids from %d to %d' % ( 
1342                     start_index
, start_index 
+ self
._GDATA
_PAGE
_SIZE
)) 
1345                 response 
= json
.loads(page
) 
1346             except ValueError as err
: 
1347                 raise ExtractorError('Invalid JSON in API response: ' + compat_str(err
)) 
1348             if 'entry' not in response
['feed']: 
1351             # Extract video identifiers 
1352             entries 
= response
['feed']['entry'] 
1353             for entry 
in entries
: 
1354                 title 
= entry
['title']['$t'] 
1355                 video_id 
= entry
['id']['$t'].split('/')[-1] 
1359                     'ie_key': 'Youtube', 
1363         url_results 
= OnDemandPagedList(download_page
, self
._GDATA
_PAGE
_SIZE
) 
1365         return self
.playlist_result(url_results
, playlist_title
=username
) 
1368 class YoutubeSearchIE(SearchInfoExtractor
): 
1369     IE_DESC 
= 'YouTube.com searches' 
1370     _API_URL 
= 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc' 
1372     IE_NAME 
= 'youtube:search' 
1373     _SEARCH_KEY 
= 'ytsearch' 
1375     def _get_n_results(self
, query
, n
): 
1376         """Get a specified number of results for a query""" 
1383         while (PAGE_SIZE 
* pagenum
) < limit
: 
1384             result_url 
= self
._API
_URL 
% ( 
1385                 compat_urllib_parse
.quote_plus(query
.encode('utf-8')), 
1386                 (PAGE_SIZE 
* pagenum
) + 1) 
1387             data_json 
= self
._download
_webpage
( 
1388                 result_url
, video_id
='query "%s"' % query
, 
1389                 note
='Downloading page %s' % (pagenum 
+ 1), 
1390                 errnote
='Unable to download API page') 
1391             data 
= json
.loads(data_json
) 
1392             api_response 
= data
['data'] 
1394             if 'items' not in api_response
: 
1395                 raise ExtractorError( 
1396                     '[youtube] No video results', expected
=True) 
1398             new_ids 
= list(video
['id'] for video 
in api_response
['items']) 
1399             video_ids 
+= new_ids
 
1401             limit 
= min(n
, api_response
['totalItems']) 
1404         if len(video_ids
) > n
: 
1405             video_ids 
= video_ids
[:n
] 
1406         videos 
= [self
.url_result(video_id
, 'Youtube', video_id
=video_id
) 
1407                   for video_id 
in video_ids
] 
1408         return self
.playlist_result(videos
, query
) 
1411 class YoutubeSearchDateIE(YoutubeSearchIE
): 
1412     IE_NAME 
= YoutubeSearchIE
.IE_NAME 
+ ':date' 
1413     _API_URL 
= 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc&orderby=published' 
1414     _SEARCH_KEY 
= 'ytsearchdate' 
1415     IE_DESC 
= 'YouTube.com searches, newest videos first' 
1418 class YoutubeSearchURLIE(InfoExtractor
): 
1419     IE_DESC 
= 'YouTube.com search URLs' 
1420     IE_NAME 
= 'youtube:search_url' 
1421     _VALID_URL 
= r
'https?://(?:www\.)?youtube\.com/results\?(.*?&)?search_query=(?P<query>[^&]+)(?:[&]|$)' 
1423         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video', 
1424         'playlist_mincount': 5, 
1426             'title': 'youtube-dl test video', 
1430     def _real_extract(self
, url
): 
1431         mobj 
= re
.match(self
._VALID
_URL
, url
) 
1432         query 
= compat_urllib_parse
.unquote_plus(mobj
.group('query')) 
1434         webpage 
= self
._download
_webpage
(url
, query
) 
1435         result_code 
= self
._search
_regex
( 
1436             r
'(?s)<ol class="item-section"(.*?)</ol>', webpage
, 'result HTML') 
1438         part_codes 
= re
.findall( 
1439             r
'(?s)<h3 class="yt-lockup-title">(.*?)</h3>', result_code
) 
1441         for part_code 
in part_codes
: 
1442             part_title 
= self
._html
_search
_regex
( 
1443                 [r
'(?s)title="([^"]+)"', r
'>([^<]+)</a>'], part_code
, 'item title', fatal
=False) 
1444             part_url_snippet 
= self
._html
_search
_regex
( 
1445                 r
'(?s)href="([^"]+)"', part_code
, 'item URL') 
1446             part_url 
= compat_urlparse
.urljoin( 
1447                 'https://www.youtube.com/', part_url_snippet
) 
1451                 'title': part_title
, 
1455             '_type': 'playlist', 
1461 class YoutubeShowIE(InfoExtractor
): 
1462     IE_DESC 
= 'YouTube.com (multi-season) shows' 
1463     _VALID_URL 
= r
'https?://www\.youtube\.com/show/(?P<id>[^?#]*)' 
1464     IE_NAME 
= 'youtube:show' 
1466         'url': 'http://www.youtube.com/show/airdisasters', 
1467         'playlist_mincount': 3, 
1469             'id': 'airdisasters', 
1470             'title': 'Air Disasters', 
1474     def _real_extract(self
, url
): 
1475         mobj 
= re
.match(self
._VALID
_URL
, url
) 
1476         playlist_id 
= mobj
.group('id') 
1477         webpage 
= self
._download
_webpage
( 
1478             url
, playlist_id
, 'Downloading show webpage') 
1479         # There's one playlist for each season of the show 
1480         m_seasons 
= list(re
.finditer(r
'href="(/playlist\?list=.*?)"', webpage
)) 
1481         self
.to_screen('%s: Found %s seasons' % (playlist_id
, len(m_seasons
))) 
1484                 'https://www.youtube.com' + season
.group(1), 'YoutubePlaylist') 
1485             for season 
in m_seasons
 
1487         title 
= self
._og
_search
_title
(webpage
, fatal
=False) 
1490             '_type': 'playlist', 
1497 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor
): 
1499     Base class for extractors that fetch info from 
1500     http://www.youtube.com/feed_ajax 
1501     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties. 
1503     _LOGIN_REQUIRED 
= True 
1504     # use action_load_personal_feed instead of action_load_system_feed 
1505     _PERSONAL_FEED 
= False 
1508     def _FEED_TEMPLATE(self
): 
1509         action 
= 'action_load_system_feed' 
1510         if self
._PERSONAL
_FEED
: 
1511             action 
= 'action_load_personal_feed' 
1512         return 'https://www.youtube.com/feed_ajax?%s=1&feed_name=%s&paging=%%s' % (action
, self
._FEED
_NAME
) 
1516         return 'youtube:%s' % self
._FEED
_NAME
 
1518     def _real_initialize(self
): 
1521     def _real_extract(self
, url
): 
1524         for i 
in itertools
.count(1): 
1525             info 
= self
._download
_json
(self
._FEED
_TEMPLATE 
% paging
, 
1526                                           '%s feed' % self
._FEED
_NAME
, 
1527                                           'Downloading page %s' % i
) 
1528             feed_html 
= info
.get('feed_html') or info
.get('content_html') 
1529             load_more_widget_html 
= info
.get('load_more_widget_html') or feed_html
 
1530             m_ids 
= re
.finditer(r
'"/watch\?v=(.*?)["&]', feed_html
) 
1531             ids 
= orderedSet(m
.group(1) for m 
in m_ids
) 
1532             feed_entries
.extend( 
1533                 self
.url_result(video_id
, 'Youtube', video_id
=video_id
) 
1534                 for video_id 
in ids
) 
1536                 r
'data-uix-load-more-href="/?[^"]+paging=(?P<paging>\d+)', 
1537                 load_more_widget_html
) 
1540             paging 
= mobj
.group('paging') 
1541         return self
.playlist_result(feed_entries
, playlist_title
=self
._PLAYLIST
_TITLE
) 
1543 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor
): 
1544     IE_DESC 
= 'YouTube.com recommended videos, "ytrec" keyword (requires authentication)' 
1545     _VALID_URL 
= r
'https?://www\.youtube\.com/feed/recommended|:ytrec(?:ommended)?' 
1546     _FEED_NAME 
= 'recommended' 
1547     _PLAYLIST_TITLE 
= 'Youtube Recommended videos' 
1549 class YoutubeWatchLaterIE(YoutubeFeedsInfoExtractor
): 
1550     IE_DESC 
= 'Youtube watch later list, "ytwatchlater" keyword (requires authentication)' 
1551     _VALID_URL 
= r
'https?://www\.youtube\.com/feed/watch_later|:ytwatchlater' 
1552     _FEED_NAME 
= 'watch_later' 
1553     _PLAYLIST_TITLE 
= 'Youtube Watch Later' 
1554     _PERSONAL_FEED 
= True 
1556 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor
): 
1557     IE_DESC 
= 'Youtube watch history, "ythistory" keyword (requires authentication)' 
1558     _VALID_URL 
= 'https?://www\.youtube\.com/feed/history|:ythistory' 
1559     _FEED_NAME 
= 'history' 
1560     _PERSONAL_FEED 
= True 
1561     _PLAYLIST_TITLE 
= 'Youtube Watch History' 
1563 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor
): 
1564     IE_NAME 
= 'youtube:favorites' 
1565     IE_DESC 
= 'YouTube.com favourite videos, "ytfav" keyword (requires authentication)' 
1566     _VALID_URL 
= r
'https?://www\.youtube\.com/my_favorites|:ytfav(?:ou?rites)?' 
1567     _LOGIN_REQUIRED 
= True 
1569     def _real_extract(self
, url
): 
1570         webpage 
= self
._download
_webpage
('https://www.youtube.com/my_favorites', 'Youtube Favourites videos') 
1571         playlist_id 
= self
._search
_regex
(r
'list=(.+?)["&]', webpage
, 'favourites playlist id') 
1572         return self
.url_result(playlist_id
, 'YoutubePlaylist') 
1575 class YoutubeSubscriptionsIE(YoutubePlaylistIE
): 
1576     IE_NAME 
= 'youtube:subscriptions' 
1577     IE_DESC 
= 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)' 
1578     _VALID_URL 
= r
'https?://www\.youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?' 
1581     def _real_extract(self
, url
): 
1582         title 
= 'Youtube Subscriptions' 
1583         page 
= self
._download
_webpage
('https://www.youtube.com/feed/subscriptions', title
) 
1585         # The extraction process is the same as for playlists, but the regex 
1586         # for the video ids doesn't contain an index 
1588         more_widget_html 
= content_html 
= page
 
1590         for page_num 
in itertools
.count(1): 
1591             matches 
= re
.findall(r
'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html
) 
1592             new_ids 
= orderedSet(matches
) 
1595             mobj 
= re
.search(r
'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html
) 
1599             more 
= self
._download
_json
( 
1600                 'https://youtube.com/%s' % mobj
.group('more'), title
, 
1601                 'Downloading page #%s' % page_num
, 
1602                 transform_source
=uppercase_escape
) 
1603             content_html 
= more
['content_html'] 
1604             more_widget_html 
= more
['load_more_widget_html'] 
1607             '_type': 'playlist', 
1609             'entries': self
._ids
_to
_results
(ids
), 
1613 class YoutubeTruncatedURLIE(InfoExtractor
): 
1614     IE_NAME 
= 'youtube:truncated_url' 
1615     IE_DESC 
= False  # Do not list 
1616     _VALID_URL 
= r
'''(?x) 
1617         (?:https?://)?[^/]+/watch\?(?: 
1619             annotation_id=annotation_[^&]+ 
1621         (?:https?://)?(?:www\.)?youtube\.com/attribution_link\?a=[^&]+$ 
1625         'url': 'http://www.youtube.com/watch?annotation_id=annotation_3951667041', 
1626         'only_matching': True, 
1628         'url': 'http://www.youtube.com/watch?', 
1629         'only_matching': True, 
1632     def _real_extract(self
, url
): 
1633         raise ExtractorError( 
1634             'Did you forget to quote the URL? Remember that & is a meta ' 
1635             'character in most shells, so you want to put the URL in quotes, ' 
1637             '"http://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" ' 
1638             ' or simply  youtube-dl BaW_jenozKc  .',