nficano commited on
Commit
6960bc1
·
1 Parent(s): 038cd4f

initial commit on complete rewrite

Browse files
pytube/__init__.py CHANGED
@@ -1,21 +1,2 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
  # flake8: noqa
4
- __title__ = 'pytube'
5
- __version__ = '6.4.3'
6
- __author__ = 'Nick Ficano'
7
- __license__ = 'MIT License'
8
- __copyright__ = 'Copyright 2017 Nick Ficano'
9
-
10
- from .api import YouTube
11
-
12
- # Set default logging handler to avoid "No handler found" warnings.
13
- import logging
14
- try: # Python 2.7+
15
- from logging import NullHandler
16
- except ImportError:
17
- class NullHandler(logging.Handler):
18
- def emit(self, record):
19
- pass
20
-
21
- logging.getLogger(__name__).addHandler(NullHandler())
 
 
 
1
  # flake8: noqa
2
+ from pytube.__main__ import YouTube
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytube/__main__.py CHANGED
@@ -1,150 +1,92 @@
1
- #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
- from __future__ import print_function
4
-
5
- import argparse
6
- import os
7
- import sys
8
- from pprint import pprint
9
-
10
- from . import YouTube
11
- from .exceptions import PytubeError
12
- from .utils import FullPaths
13
- from .utils import print_status
14
-
15
-
16
- def main():
17
- parser = argparse.ArgumentParser(description='YouTube video downloader')
18
- parser.add_argument(
19
- 'url',
20
- help='The URL of the Video to be downloaded',
21
- )
22
- parser.add_argument(
23
- '--extension',
24
- '-e',
25
- dest='ext',
26
- help='The requested format of the video',
27
- )
28
- parser.add_argument(
29
- '--resolution',
30
- '-r',
31
- dest='res',
32
- help='The requested resolution',
33
- )
34
- parser.add_argument(
35
- '--path',
36
- '-p',
37
- action=FullPaths,
38
- default=os.getcwd(),
39
- dest='path',
40
- help='The path to save the video to.',
41
- )
42
- parser.add_argument(
43
- '--filename',
44
- '-f',
45
- dest='filename',
46
- help='The filename, without extension, to save the video in.',
47
- )
48
- parser.add_argument(
49
- '--show_available',
50
- '-s',
51
- action='store_true',
52
- dest='show_available',
53
- help='Prints a list of available formats for download.',
54
- )
55
-
56
- args = parser.parse_args()
57
-
58
- try:
59
- yt = YouTube(args.url)
60
- videos = []
61
- for i, video in enumerate(yt.get_videos()):
62
- ext = video.extension
63
- res = video.resolution
64
- videos.append((ext, res))
65
- except PytubeError:
66
- print('Incorrect video URL.')
67
- sys.exit(1)
68
-
69
- if args.show_available:
70
- print_available_vids(videos)
71
- sys.exit(0)
72
-
73
- if args.filename:
74
- yt.set_filename(args.filename)
75
-
76
- if args.ext or args.res:
77
- if not all([args.ext, args.res]):
78
- print(
79
- 'Make sure you give either of the below specified '
80
- 'format/resolution combination.',
81
- )
82
- print_available_vids(videos)
83
- sys.exit(1)
84
-
85
- if args.ext and args.res:
86
- # There's only ope video that matches both so get it
87
- vid = yt.get(args.ext, args.res)
88
- # Check if there's a video returned
89
- if not vid:
90
- print(
91
- "There's no video with the specified format/resolution "
92
- 'combination.',
93
  )
94
- pprint(videos)
95
- sys.exit(1)
96
-
97
- elif args.ext:
98
- # There are several videos with the same extension
99
- videos = yt.filter(extension=args.ext)
100
- # Check if we have a video
101
- if not videos:
102
- print('There are no videos in the specified format.')
103
- sys.exit(1)
104
- # Select the highest resolution one
105
- vid = max(videos)
106
- elif args.res:
107
- # There might be several videos in the same resolution
108
- videos = yt.filter(resolution=args.res)
109
- # Check if we have a video
110
- if not videos:
111
- print(
112
- 'There are no videos in the specified in the specified '
113
- 'resolution.',
114
- )
115
- sys.exit(1)
116
- # Select the highest resolution one
117
- vid = max(videos)
118
- else:
119
- # If nothing is specified get the highest resolution one
120
- print_available_vids(videos)
121
- while True:
122
- try:
123
- choice = int(input('Enter choice: '))
124
- vid = yt.get(*videos[choice])
125
- break
126
- except (ValueError, IndexError):
127
- print('Requires an integer in range 0-{}'
128
- .format(len(videos) - 1))
129
- except KeyboardInterrupt:
130
- sys.exit(2)
131
-
132
- try:
133
- vid.download(path=args.path, on_progress=print_status)
134
- except KeyboardInterrupt:
135
- print('Download interrupted.')
136
- sys.exit(1)
137
 
 
 
 
 
138
 
139
- def print_available_vids(videos):
140
- formatString = '{:<2} {:<15} {:<15}'
141
- print(formatString.format('', 'Resolution', 'Extension'))
142
- print('-' * 28)
143
- print('\n'.join([
144
- formatString.format(index, *formatTuple)
145
- for index, formatTuple in enumerate(videos)
146
- ]))
147
 
 
 
148
 
149
- if __name__ == '__main__':
150
- main()
 
 
1
  # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.__main__
4
+ ~~~~~~~~~~~~~~~
5
+
6
+ This module implements the core interface for pytube.
7
+
8
+ """
9
+ import json
10
+
11
+ from pytube import download
12
+ from pytube import extract
13
+ from pytube import mixins
14
+ from pytube.query import StreamQuery
15
+ from pytube.streams import Stream
16
+
17
+
18
+ class YouTube:
19
+ def __init__(
20
+ self, url=None, defer_init=False, on_progress_callback=None,
21
+ on_complete_callback=None,
22
+ ):
23
+ self.js = None
24
+ self.js_url = None
25
+ self.vid_info = None
26
+ self.vid_info_url = None
27
+ self.watch_html = None
28
+ self.player_config = None
29
+ self.fmt_streams = []
30
+ self.video_id = extract.video_id(url)
31
+ self.watch_url = extract.watch_url(self.video_id)
32
+ self.shared_stream_state = {
33
+ 'on_progress': on_progress_callback,
34
+ 'on_complete': on_complete_callback,
35
+ }
36
+
37
+ if url and not defer_init:
38
+ self.init()
39
+
40
+ def init(self):
41
+ self.prefetch()
42
+ self.vid_info = extract.decode_video_info(self.vid_info)
43
+
44
+ trad_fmts = 'url_encoded_fmt_stream_map'
45
+ dash_fmts = 'adaptive_fmts'
46
+ mixins.apply_fmt_decoder(self.vid_info, trad_fmts)
47
+ mixins.apply_fmt_decoder(self.vid_info, dash_fmts)
48
+ mixins.apply_fmt_decoder(self.player_config['args'], trad_fmts)
49
+ mixins.apply_fmt_decoder(self.player_config['args'], dash_fmts)
50
+ mixins.apply_cipher(self.player_config['args'], trad_fmts, self.js)
51
+ mixins.apply_cipher(self.player_config['args'], dash_fmts, self.js)
52
+ mixins.apply(self.player_config['args'], 'player_response', json.loads)
53
+ self.build_stream_objects(trad_fmts)
54
+ self.build_stream_objects(dash_fmts)
55
+
56
+ def prefetch(self):
57
+ self.watch_html = download.get(url=self.watch_url)
58
+ self.vid_info_url = extract.video_info_url(
59
+ video_id=self.video_id,
60
+ watch_url=self.watch_url,
61
+ watch_html=self.watch_html,
62
+ )
63
+ self.js_url = extract.js_url(self.watch_html)
64
+ self.js = download.get(url=self.js_url)
65
+ self.vid_info = download.get(url=self.vid_info_url)
66
+ self.player_config = extract.get_ytplayer_config(self.watch_html)
67
+
68
+ def build_stream_objects(self, fmt):
69
+ streams = self.player_config['args'][fmt]
70
+ for stream in streams:
71
+ video = Stream(
72
+ stream=stream,
73
+ player_config=self.player_config,
74
+ shared_stream_state=self.shared_stream_state,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  )
76
+ self.fmt_streams.append(video)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
+ @property
79
+ def streams(self):
80
+ """Interface to query non-dash streams."""
81
+ return StreamQuery([s for s in self.fmt_streams if not s.is_dash])
82
 
83
+ @property
84
+ def dash_streams(self):
85
+ """Interface to query dash streams."""
86
+ return StreamQuery([s for s in self.fmt_streams if s.is_dash])
 
 
 
 
87
 
88
+ def register_on_progress_callback(self, fn):
89
+ self.shared_stream_state['on_progress'] = fn
90
 
91
+ def register_on_complete_callback(self, fn):
92
+ self.shared_stream_state['on_complete'] = fn
pytube/api.py DELETED
@@ -1,475 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- from __future__ import absolute_import
4
-
5
- import json
6
- import logging
7
- import re
8
- import warnings
9
- from collections import defaultdict
10
-
11
- from .compat import parse_qs
12
- from .compat import unquote
13
- from .compat import urlopen
14
- from .compat import urlparse
15
- from .exceptions import AgeRestricted
16
- from .exceptions import CipherError
17
- from .exceptions import DoesNotExist
18
- from .exceptions import MultipleObjectsReturned
19
- from .exceptions import PytubeError
20
- from .jsinterp import JSInterpreter
21
- from .models import Video
22
- from .utils import safe_filename
23
-
24
- log = logging.getLogger(__name__)
25
-
26
- # YouTube quality and codecs id map.
27
- QUALITY_PROFILES = {
28
- # flash
29
- 5: ('flv', '240p', 'Sorenson H.263', 'N/A', '0.25', 'MP3', '64'),
30
-
31
- # 3gp
32
- 17: ('3gp', '144p', 'MPEG-4 Visual', 'Simple', '0.05', 'AAC', '24'),
33
- 36: ('3gp', '240p', 'MPEG-4 Visual', 'Simple', '0.17', 'AAC', '38'),
34
-
35
- # webm
36
- 43: ('webm', '360p', 'VP8', 'N/A', '0.5', 'Vorbis', '128'),
37
- 100: ('webm', '360p', 'VP8', '3D', 'N/A', 'Vorbis', '128'),
38
-
39
- # mpeg4
40
- 18: ('mp4', '360p', 'H.264', 'Baseline', '0.5', 'AAC', '96'),
41
- 22: ('mp4', '720p', 'H.264', 'High', '2-2.9', 'AAC', '192'),
42
- 82: ('mp4', '360p', 'H.264', '3D', '0.5', 'AAC', '96'),
43
- 83: ('mp4', '240p', 'H.264', '3D', '0.5', 'AAC', '96'),
44
- 84: ('mp4', '720p', 'H.264', '3D', '2-2.9', 'AAC', '152'),
45
- 85: ('mp4', '1080p', 'H.264', '3D', '2-2.9', 'AAC', '152'),
46
-
47
- 160: ('mp4', '144p', 'H.264', 'Main', '0.1', '', ''),
48
- 133: ('mp4', '240p', 'H.264', 'Main', '0.2-0.3', '', ''),
49
- 134: ('mp4', '360p', 'H.264', 'Main', '0.3-0.4', '', ''),
50
- 135: ('mp4', '480p', 'H.264', 'Main', '0.5-1', '', ''),
51
- 136: ('mp4', '720p', 'H.264', 'Main', '1-1.5', '', ''),
52
- 298: ('mp4', '720p HFR', 'H.264', 'Main', '3-3.5', '', ''),
53
-
54
- 137: ('mp4', '1080p', 'H.264', 'High', '2.5-3', '', ''),
55
- 299: ('mp4', '1080p HFR', 'H.264', 'High', '5.5', '', ''),
56
- 264: ('mp4', '2160p-2304p', 'H.264', 'High', '12.5-16', '', ''),
57
- 266: ('mp4', '2160p-4320p', 'H.264', 'High', '13.5-25', '', ''),
58
-
59
- 242: ('webm', '240p', 'vp9', 'n/a', '0.1-0.2', '', ''),
60
- 243: ('webm', '360p', 'vp9', 'n/a', '0.25', '', ''),
61
- 244: ('webm', '480p', 'vp9', 'n/a', '0.5', '', ''),
62
- 247: ('webm', '720p', 'vp9', 'n/a', '0.7-0.8', '', ''),
63
- 248: ('webm', '1080p', 'vp9', 'n/a', '1.5', '', ''),
64
- 271: ('webm', '1440p', 'vp9', 'n/a', '9', '', ''),
65
- 278: ('webm', '144p 15 fps', 'vp9', 'n/a', '0.08', '', ''),
66
- 302: ('webm', '720p HFR', 'vp9', 'n/a', '2.5', '', ''),
67
- 303: ('webm', '1080p HFR', 'vp9', 'n/a', '5', '', ''),
68
- 308: ('webm', '1440p HFR', 'vp9', 'n/a', '10', '', ''),
69
- 313: ('webm', '2160p', 'vp9', 'n/a', '13-15', '', ''),
70
- 315: ('webm', '2160p HFR', 'vp9', 'n/a', '20-25', '', ''),
71
- }
72
-
73
- # The keys corresponding to the quality/codec map above.
74
- QUALITY_PROFILE_KEYS = (
75
- 'extension',
76
- 'resolution',
77
- 'video_codec',
78
- 'profile',
79
- 'video_bitrate',
80
- 'audio_codec',
81
- 'audio_bitrate',
82
- )
83
-
84
-
85
- class YouTube(object):
86
- """Class representation of a single instance of a YouTube session.
87
- """
88
-
89
- def __init__(self, url=None):
90
- """Initializes YouTube API wrapper.
91
-
92
- :param str url:
93
- The url to the YouTube video.
94
- """
95
- self._filename = None
96
- self._video_url = None
97
- self._js_cache = None
98
- self._videos = []
99
- if url:
100
- self.from_url(url)
101
-
102
- @property
103
- def url(self):
104
- """Gets the video url."""
105
- return self._video_url
106
-
107
- @url.setter
108
- def url(self, url):
109
- """Sets the url for the video (This method is deprecated. Use
110
- `from_url()` instead).
111
-
112
- :param str url:
113
- The url to the YouTube video.
114
- """
115
- warnings.warn(
116
- 'url setter deprecated, use `from_url()` '
117
- 'instead.', DeprecationWarning,
118
- )
119
- self.from_url(url)
120
-
121
- @property
122
- def video_id(self):
123
- """Gets the video id by parsing and extracting it from the url."""
124
- parts = urlparse(self._video_url)
125
- qs = getattr(parts, 'query')
126
- if qs:
127
- video_id = parse_qs(qs).get('v')
128
- if video_id:
129
- return video_id.pop()
130
-
131
- @property
132
- def filename(self):
133
- """Gets the filename of the video. If it hasn't been defined by the
134
- user, the title will instead be used.
135
- """
136
- if not self._filename:
137
- self._filename = safe_filename(self.title)
138
- log.debug("generated 'safe' filename: %s", self._filename)
139
- return self._filename
140
-
141
- @filename.setter
142
- def filename(self, filename):
143
- """Sets the filename (This method is deprecated. Use `set_filename()`
144
- instead).
145
-
146
- :param str filename:
147
- The filename of the video.
148
- """
149
- warnings.warn(
150
- 'filename setter deprecated. Use `set_filename()` '
151
- 'instead.', DeprecationWarning,
152
- )
153
- self.set_filename(filename)
154
-
155
- def set_filename(self, filename):
156
- """Sets the filename of the video.
157
-
158
- :param str filename:
159
- The filename of the video.
160
- """
161
- # TODO: Check if the filename contains the file extension and either
162
- # strip it or raise an exception.
163
- self._filename = filename
164
- if self.get_videos():
165
- for video in self.get_videos():
166
- video.filename = filename
167
- return True
168
-
169
- def get_videos(self):
170
- """Gets all videos."""
171
- return self._videos
172
-
173
- @property
174
- def videos(self):
175
- """Gets all videos. (This method is deprecated. Use `get_videos()`
176
- instead.
177
- """
178
- warnings.warn(
179
- 'videos property deprecated. Use `get_videos()` '
180
- 'instead.', DeprecationWarning,
181
- )
182
- return self._videos
183
-
184
- def from_url(self, url):
185
- """Sets the url for the video.
186
-
187
- :param str url:
188
- The url to the YouTube video.
189
- """
190
- self._video_url = url
191
-
192
- # Reset the filename and videos list in case the same instance is
193
- # reused.
194
- self._filename = None
195
- self._videos = []
196
-
197
- # Get the video details.
198
- video_data = self.get_video_data()
199
-
200
- # Set the title from the title.
201
- self.title = video_data.get('args', {}).get('title')
202
-
203
- # Rewrite and add the url to the javascript file, we'll need to fetch
204
- # this if YouTube doesn't provide us with the signature.
205
- js_partial_url = video_data.get('assets', {}).get('js')
206
- if js_partial_url.startswith('//'):
207
- js_url = 'http:' + js_partial_url
208
- elif js_partial_url.startswith('/'):
209
- js_url = 'https://youtube.com' + js_partial_url
210
-
211
- # Just make these easily accessible as variables.
212
- stream_map = video_data.get('args', {}).get('stream_map')
213
- video_urls = stream_map.get('url')
214
-
215
- # For each video url, identify the quality profile and add it to list
216
- # of available videos.
217
- for i, url in enumerate(video_urls):
218
- log.debug('attempting to get quality profile from url: %s', url)
219
- try:
220
- itag, quality_profile = self._get_quality_profile_from_url(url)
221
- if not quality_profile:
222
- log.warn('unable to identify profile for itag=%s', itag)
223
- continue
224
- except (TypeError, KeyError) as e:
225
- log.exception('passing on exception %s', e)
226
- continue
227
-
228
- # Check if we have the signature, otherwise we'll need to get the
229
- # cipher from the js.
230
- if 'signature=' not in url:
231
- log.debug(
232
- 'signature not in url, attempting to resolve the '
233
- 'cipher.',
234
- )
235
- signature = self._get_cipher(stream_map['s'][i], js_url)
236
- url = '{0}&signature={1}'.format(url, signature)
237
- self._add_video(url, self.filename, **quality_profile)
238
- # Clear the cached js. Make sure to keep this at the end of
239
- # `from_url()` so we can mock inject the js in unit tests.
240
- self._js_cache = None
241
-
242
- def get(self, extension=None, resolution=None, profile=None):
243
- """Gets a single video given a file extention (and/or resolution
244
- and/or quality profile).
245
-
246
- :param str extention:
247
- The desired file extention (e.g.: mp4, flv).
248
- :param str resolution:
249
- The desired video broadcasting standard (e.g.: 720p, 1080p)
250
- :param str profile:
251
- The desired quality profile (this is subjective, I don't recommend
252
- using it).
253
- """
254
- result = []
255
- for v in self.get_videos():
256
- if extension and v.extension != extension:
257
- continue
258
- elif resolution and v.resolution != resolution:
259
- continue
260
- elif profile and v.profile != profile:
261
- continue
262
- else:
263
- result.append(v)
264
- matches = len(result)
265
- if matches <= 0:
266
- raise DoesNotExist('No videos met this criteria.')
267
- elif matches == 1:
268
- return result[0]
269
- else:
270
- raise MultipleObjectsReturned('Multiple videos met this criteria.')
271
-
272
- def filter(self, extension=None, resolution=None, profile=None):
273
- """Gets a filtered list of videos given a file extention and/or
274
- resolution and/or quality profile.
275
-
276
- :param str extention:
277
- The desired file extention (e.g.: mp4, flv).
278
- :param str resolution:
279
- The desired video broadcasting standard (e.g.: 720p, 1080p)
280
- :param str profile:
281
- The desired quality profile (this is subjective, I don't recommend
282
- using it).
283
- """
284
- results = []
285
- for v in self.get_videos():
286
- if extension and v.extension != extension:
287
- continue
288
- elif resolution and v.resolution != resolution:
289
- continue
290
- elif profile and v.profile != profile:
291
- continue
292
- else:
293
- results.append(v)
294
- return results
295
-
296
- def get_video_data(self):
297
- """Gets the page and extracts out the video data."""
298
- # Reset the filename incase it was previously set.
299
- self.title = None
300
- response = urlopen(self.url)
301
- if not response:
302
- raise PytubeError('Unable to open url: {0}'.format(self.url))
303
-
304
- html = response.read()
305
- if isinstance(html, str):
306
- restriction_pattern = 'og:restrictions:age'
307
- else:
308
- restriction_pattern = bytes('og:restrictions:age', 'utf-8')
309
-
310
- if restriction_pattern in html:
311
- raise AgeRestricted(
312
- 'Age restricted video. Unable to download '
313
- 'without being signed in.',
314
- )
315
-
316
- # Extract out the json data from the html response body.
317
- json_object = self._get_json_data(html)
318
-
319
- # Here we decode the stream map and bundle it into the json object. We
320
- # do this just so we just can return one object for the video data.
321
- encoded_stream_map = json_object.get('args', {}).get(
322
- 'url_encoded_fmt_stream_map',
323
- )
324
- json_object['args']['stream_map'] = self._parse_stream_map(
325
- encoded_stream_map,
326
- )
327
- return json_object
328
-
329
- def _parse_stream_map(self, blob):
330
- """A modified version of `urlparse.parse_qs` that's able to decode
331
- YouTube's stream map.
332
-
333
- :param str blob:
334
- An encoded blob of text containing the stream map data.
335
- """
336
- dct = defaultdict(list)
337
-
338
- # Split the comma separated videos.
339
- videos = blob.split(',')
340
-
341
- # Unquote the characters and split to parameters.
342
- videos = [video.split('&') for video in videos]
343
-
344
- # Split at the equals sign so we can break this key value pairs and
345
- # toss it into a dictionary.
346
- for video in videos:
347
- for kv in video:
348
- key, value = kv.split('=')
349
- dct[key].append(unquote(value))
350
- log.debug('decoded stream map: %s', dct)
351
- return dct
352
-
353
- def _get_json_data(self, html):
354
- """Extract the json out from the html.
355
-
356
- :param str html:
357
- The raw html of the page.
358
- """
359
- # 18 represents the length of "ytplayer.config = ".
360
- if isinstance(html, str):
361
- json_start_pattern = 'ytplayer.config = '
362
- else:
363
- json_start_pattern = bytes('ytplayer.config = ', 'utf-8')
364
- pattern_idx = html.find(json_start_pattern)
365
- # In case video is unable to play
366
- if(pattern_idx == -1):
367
- raise PytubeError('Unable to find start pattern.')
368
- start = pattern_idx + 18
369
- html = html[start:]
370
-
371
- offset = self._get_json_offset(html)
372
- if not offset:
373
- raise PytubeError('Unable to extract json.')
374
- if isinstance(html, str):
375
- json_content = json.loads(html[:offset])
376
- else:
377
- json_content = json.loads(html[:offset].decode('utf-8'))
378
-
379
- return json_content
380
-
381
- def _get_json_offset(self, html):
382
- """Find where the json object starts.
383
-
384
- :param str html:
385
- The raw html of the YouTube page.
386
- """
387
- unmatched_brackets_num = 0
388
- index = 1
389
- for i, ch in enumerate(html):
390
- if isinstance(ch, int):
391
- ch = chr(ch)
392
- if ch == '{':
393
- unmatched_brackets_num += 1
394
- elif ch == '}':
395
- unmatched_brackets_num -= 1
396
- if unmatched_brackets_num == 0:
397
- break
398
- else:
399
- raise PytubeError('Unable to determine json offset.')
400
- return index + i
401
-
402
- def _get_cipher(self, signature, url):
403
- """Gets the signature using the cipher.
404
-
405
- :param str signature:
406
- The url signature.
407
- :param str url:
408
- The url of the javascript file.
409
- """
410
- reg_exp = re.compile(r'"signature",\s?([a-zA-Z0-9$]+)\(')
411
- # Cache the js since `_get_cipher()` will be called for each video.
412
- if not self._js_cache:
413
- response = urlopen(url)
414
- if not response:
415
- raise PytubeError('Unable to open url: {0}'.format(self.url))
416
- self._js_cache = response.read().decode()
417
- try:
418
- matches = reg_exp.search(self._js_cache)
419
- if matches:
420
- # Return the first matching group.
421
- func = next(g for g in matches.groups() if g is not None)
422
- # Load js into JS Python interpreter.
423
- jsi = JSInterpreter(self._js_cache)
424
- initial_function = jsi.extract_function(func)
425
- return initial_function([signature])
426
- except Exception as e:
427
- raise CipherError("Couldn't cipher the signature. Maybe YouTube "
428
- 'has changed the cipher algorithm. Notify this '
429
- 'issue on GitHub: {0}'.format(e))
430
- return False
431
-
432
- def _get_quality_profile_from_url(self, video_url):
433
- """Gets the quality profile given a video url. Normally we would just
434
- use `urlparse` since itags are represented as a get parameter, but
435
- YouTube doesn't pass a properly encoded url.
436
-
437
- :param str video_url:
438
- The malformed url-encoded video_url.
439
- """
440
- reg_exp = re.compile('itag=(\d+)')
441
- itag = reg_exp.findall(video_url)
442
- if itag and len(itag) == 1:
443
- itag = int(itag[0])
444
- # Given an itag, refer to the YouTube quality profiles to get the
445
- # properties (media type, resolution, etc.) of the video.
446
- quality_profile = QUALITY_PROFILES.get(itag)
447
- if not quality_profile:
448
- return itag, None
449
- # Here we just combine the quality profile keys to the
450
- # corresponding quality profile, referenced by the itag.
451
- return itag, dict(list(zip(QUALITY_PROFILE_KEYS, quality_profile)))
452
- if not itag:
453
- raise PytubeError('Unable to get encoding profile, no itag found.')
454
- elif len(itag) > 1:
455
- log.warn('Multiple itags found: %s', itag)
456
- raise PytubeError(
457
- 'Unable to get encoding profile, multiple itags '
458
- 'found.',
459
- )
460
- return False
461
-
462
- def _add_video(self, url, filename, **kwargs):
463
- """Adds new video object to videos.
464
-
465
- :param str url:
466
- The signed url to the video.
467
- :param str filename:
468
- The filename for the video.
469
- :param kwargs:
470
- Additional properties to set for the video object.
471
- """
472
- video = Video(url, filename, **kwargs)
473
- self._videos.append(video)
474
- self._videos.sort()
475
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytube/cipher.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.cipher
4
+ ~~~~~~~~~~~~~
5
+
6
+ """
7
+ import re
8
+ from itertools import chain
9
+
10
+ from pytube.helpers import memoize
11
+
12
+
13
+ def get_initial_function_name(js):
14
+ """Extracts the name of the function responsible for computing the signature.
15
+ """
16
+ # c&&d.set("signature", EE(c));
17
+ pattern = r'"signature",\s?([a-zA-Z0-9$]+)\('
18
+ regex = re.compile(pattern)
19
+ return (
20
+ regex
21
+ .search(js)
22
+ .group(1)
23
+ )
24
+
25
+
26
+ def get_transform_plan(js):
27
+ """Extracts the "transform plan", that is, the functions the original
28
+ signature is passed through to decode the actual signature.
29
+
30
+ Sample Output:
31
+ ~~~~~~~~~~~~~~
32
+ ['DE.AJ(a,15)',
33
+ 'DE.VR(a,3)',
34
+ 'DE.AJ(a,51)',
35
+ 'DE.VR(a,3)',
36
+ 'DE.kT(a,51)',
37
+ 'DE.kT(a,8)',
38
+ 'DE.VR(a,3)',
39
+ 'DE.kT(a,21)']
40
+ """
41
+ name = re.escape(get_initial_function_name(js))
42
+ pattern = r'%s=function\(\w\){[a-z=\.\(\"\)]*;(.*);(?:.+)}' % name
43
+ regex = re.compile(pattern)
44
+ return (
45
+ regex
46
+ .search(js)
47
+ .group(1)
48
+ .split(';')
49
+ )
50
+
51
+
52
+ def get_transform_object(js, var):
53
+ """Extracts the "transform object" which contains the function definitions
54
+ referenced in the "transform plan". The ``var`` argument is the obfuscated
55
+ variable name which contains these functions, for example, given the
56
+ function call ``DE.AJ(a,15)`` returned by the transform plan, "DE" would be
57
+ the var.
58
+
59
+ Sample Output:
60
+ ~~~~~~~~~~~~~~
61
+ ['AJ:function(a){a.reverse()}',
62
+ 'VR:function(a,b){a.splice(0,b)}',
63
+ 'kT:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}']
64
+
65
+ """
66
+ pattern = r'var %s={(.*?)};' % re.escape(var)
67
+ regex = re.compile(pattern, re.DOTALL)
68
+ return (
69
+ regex
70
+ .search(js)
71
+ .group(1)
72
+ .replace('\n', ' ')
73
+ .split(', ')
74
+ )
75
+
76
+
77
+ def get_transform_map(js, var):
78
+ transform_object = get_transform_object(js, var)
79
+ mapper = {}
80
+ for obj in transform_object:
81
+ # AJ:function(a){a.reverse()} => AJ, function(a){a.reverse()}
82
+ name, function = obj.split(':', 1)
83
+ fn = map_functions(function)
84
+ mapper[name] = fn
85
+ return mapper
86
+
87
+
88
+ def reverse(arr, b):
89
+ """Immutable equivalent to function(a){a.reverse()}.
90
+
91
+ Example usage:
92
+ ~~~~~~~~~~~~~~
93
+ >>> reverse([1, 2, 3, 4])
94
+ [4, 3, 2, 1]
95
+ """
96
+ return arr[::-1]
97
+
98
+
99
+ def splice(arr, b):
100
+ """Immutable equivalent to function(a,b){a.splice(0,b)}.
101
+
102
+ Example usage:
103
+ ~~~~~~~~~~~~~~
104
+ >>> splice([1, 2, 3, 4], 2)
105
+ [1, 2]
106
+ """
107
+ return arr[:b] + arr[b * 2:]
108
+
109
+
110
+ def swap(arr, b):
111
+ """Immutable equivalent to:
112
+ function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}.
113
+
114
+ Example usage:
115
+ ~~~~~~~~~~~~~~
116
+ >>> swap([1, 2, 3, 4], 2)
117
+ [3, 2, 1, 4]
118
+ """
119
+ r = b % len(arr)
120
+ return list(chain([arr[r]], arr[1:r], [arr[0]], arr[r + 1:]))
121
+
122
+
123
+ def map_functions(js_func):
124
+ """Maps the javascript function to its Python equivalent.
125
+ """
126
+ mapper = (
127
+ # function(a){a.reverse()}
128
+ ('{\w\.reverse\(\)}', reverse),
129
+ # function(a,b){a.splice(0,b)}
130
+ ('{\w\.splice\(0,\w\)}', splice),
131
+ # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
132
+ ('{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\]=\w}', swap),
133
+ )
134
+
135
+ for pattern, fn in mapper:
136
+ if re.search(pattern, js_func):
137
+ return fn
138
+ # TODO(nficano): raise error
139
+
140
+
141
+ def parse_function(js_func):
142
+ pattern = r'\w+\.(\w+)\(\w,(\d+)\)'
143
+ regex = re.compile(pattern)
144
+ return (
145
+ regex
146
+ .search(js_func)
147
+ .groups()
148
+ )
149
+
150
+
151
+ @memoize
152
+ def get_signature(js, signature):
153
+ tplan = get_transform_plan(js)
154
+ # DE.AJ(a,15) => DE, AJ(a,15)
155
+ var, _ = tplan[0].split('.')
156
+ tmap = get_transform_map(js, var)
157
+ signature = [s for s in signature]
158
+
159
+ for js_func in tplan:
160
+ name, argument = parse_function(js_func)
161
+ signature = tmap[name](signature, int(argument))
162
+ return ''.join(signature)
pytube/cli.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.cli
4
+ ~~~~~~~~~~
5
+
6
+ A simple command line application to download youtube videos.
7
+
8
+ """
9
+ from __future__ import print_function
10
+
11
+ import argparse
12
+ import os
13
+ import sys
14
+
15
+ from youtube import YouTube
16
+
17
+
18
+ def main():
19
+ """A simple command line application to download youtube videos."""
20
+ parser = argparse.ArgumentParser(description=main.__doc__)
21
+ parser.add_argument('url', help='The YouTube /watch url', nargs='?')
22
+ parser.add_argument(
23
+ '--itag', type=int, help=(
24
+ 'The itag for the desired stream'
25
+ ),
26
+ )
27
+ parser.add_argument(
28
+ '-l', '--list', action='store_true', help=(
29
+ 'The list option causes pytube cli to return a list of streams '
30
+ 'available to download'
31
+ ),
32
+ )
33
+ args = parser.parse_args()
34
+ if not args.url:
35
+ parser.print_help()
36
+ exit(1)
37
+ if args.list:
38
+ display_streams(args.url)
39
+ elif args.itag:
40
+ download(args.url, args.itag)
41
+
42
+
43
+ def _get_terminal_size():
44
+ """Returns the terminal size in rows and columns."""
45
+ rows, columns = os.popen('stty size', 'r').read().split()
46
+ return int(rows), int(columns)
47
+
48
+
49
+ def _render_progress_bar(bar, percent):
50
+ text = ' ↳ |{bar}| {percent}%\r'.format(bar=bar, percent=percent)
51
+ sys.stdout.write(text)
52
+ sys.stdout.flush()
53
+
54
+
55
+ def display_progress_bar(count, total, ch='█', scale=0.55):
56
+ _, columns = _get_terminal_size()
57
+ max_width = int(columns * scale)
58
+
59
+ filled = int(round(max_width * count / float(total)))
60
+ remaining = max_width - filled
61
+ bar = ch * filled + ' ' * remaining
62
+ percent = round(100.0 * count / float(total), 1)
63
+ _render_progress_bar(bar, percent)
64
+
65
+
66
+ def on_progress(stream, file_handle, bytes_remaining):
67
+ total = stream.filesize
68
+ count = total - bytes_remaining
69
+ display_progress_bar(count, total)
70
+
71
+
72
+ def download(url, itag):
73
+ # TODO(nficano): allow download target to be specified
74
+ # TODO(nficano): allow dash itags to be selected
75
+ yt = YouTube(url, on_progress_callback=on_progress)
76
+ stream = yt.streams.get(itag)
77
+ print('\n{fn} | {fs} bytes'.format(
78
+ fn=stream.default_filename,
79
+ fs=stream.filesize,
80
+ ))
81
+ stream.download()
82
+ sys.stdout.write('\n')
83
+
84
+
85
+ def display_streams(url):
86
+ yt = YouTube(url)
87
+ for stream in yt.streams.all():
88
+ print(stream)
89
+
90
+
91
+ if __name__ == '__main__':
92
+ main()
pytube/compat.py DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- # flake8: noqa
4
- import sys
5
-
6
- PY2 = sys.version_info[0] == 2
7
- PY3 = sys.version_info[0] == 3
8
-
9
- if PY2:
10
- from urllib2 import urlopen
11
- from urlparse import urlparse, parse_qs, unquote
12
- if PY3:
13
- from urllib.parse import urlparse, parse_qs, unquote
14
- from urllib.request import urlopen
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytube/download.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.download
4
+ ~~~~~~~~~~~~~~~
5
+
6
+ """
7
+ from urllib.request import urlopen
8
+
9
+ import requests
10
+
11
+
12
+ def get(url):
13
+ response = requests.get(url)
14
+ return response.text
15
+
16
+
17
+ def headers(url):
18
+ response = urlopen(url)
19
+ return dict(response.info().items())
20
+
21
+
22
+ def stream(url, chunk_size=8 * 1024):
23
+ response = urlopen(url)
24
+ while True:
25
+ buf = response.read(chunk_size)
26
+ if not buf:
27
+ break
28
+ yield buf
pytube/exceptions.py DELETED
@@ -1,33 +0,0 @@
1
- class MultipleObjectsReturned(Exception):
2
- """The query returned multiple objects when only one was expected.
3
- """
4
- pass
5
-
6
-
7
- class ExtractorError(Exception):
8
- """Something specific to the js parser failed.
9
- """
10
-
11
-
12
- class PytubeError(Exception):
13
- """Something specific to the wrapper failed.
14
- """
15
- pass
16
-
17
-
18
- class CipherError(Exception):
19
- """The _cipher method returned an error.
20
- """
21
- pass
22
-
23
-
24
- class DoesNotExist(Exception):
25
- """The requested video does not exist.
26
- """
27
- pass
28
-
29
-
30
- class AgeRestricted(Exception):
31
- """The requested video has an age restriction.
32
- """
33
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytube/extract.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.extract
4
+ ~~~~~~~~~~~~~~
5
+
6
+ """
7
+ import json
8
+ import re
9
+ from urllib.parse import parse_qsl
10
+ from urllib.parse import quote
11
+ from urllib.parse import urlencode
12
+
13
+ from pytube.helpers import memoize
14
+
15
+
16
+ def video_id(url):
17
+ pattern = re.compile(r'.*(?:v=|/v/|^)(?P<id>[^&]*)')
18
+ return pattern.search(url).group(1)
19
+
20
+
21
+ def watch_url(video_id):
22
+ return (
23
+ 'http://www.youtube.com/watch?v={video_id}'
24
+ .format(video_id=video_id)
25
+ )
26
+
27
+
28
+ def video_info_url(video_id, watch_url, watch_html):
29
+ # TODO(nficano): not sure what t represents.
30
+ t = re.compile('\W[\'"]?t[\'"]?: ?[\'"](.+?)[\'"]')
31
+ params = urlencode({
32
+ 'video_id': video_id,
33
+ 'el': '$el',
34
+ 'ps': 'default',
35
+ 'eurl': quote(watch_url),
36
+ 'hl': 'en_US',
37
+ 't': quote(t.search(watch_html).group(0)),
38
+ })
39
+ return (
40
+ 'https://www.youtube.com/get_video_info?{params}'
41
+ .format(params=params)
42
+ )
43
+
44
+
45
+ def js_url(watch_html):
46
+ ytplayer_config = get_ytplayer_config(watch_html)
47
+ base_js = ytplayer_config['assets']['js']
48
+ return 'https://youtube.com{base_js}'.format(base_js=base_js)
49
+
50
+
51
+ @memoize
52
+ def get_ytplayer_config(watch_html):
53
+ pattern = re.compile(r';ytplayer\.config\s*=\s*({.*?});')
54
+ return json.loads(pattern.search(watch_html).group(1))
55
+
56
+
57
+ def decode_video_info(video_info):
58
+ return {k: v for k, v in parse_qsl(video_info)}
pytube/helpers.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.helpers
4
+ ~~~~~~~~~~~~~~
5
+
6
+ Various helper functions implemented by pytube.
7
+
8
+ """
9
+ import functools
10
+ import re
11
+
12
+
13
+ def truncate(text, max_length=200):
14
+ return text[:max_length].rsplit(' ', 0)[0]
15
+
16
+
17
+ def safe_filename(text, max_length=200):
18
+ """Sanitizes filenames for many operating systems."""
19
+ output_text = text
20
+
21
+ output_text = (
22
+ text
23
+ .replace('_', ' ')
24
+ .replace(':', ' -')
25
+ )
26
+
27
+ # NTFS forbids filenames containing characters in range 0-31 (0x00-0x1F)
28
+ ntfs_illegal_chars = [chr(i) for i in range(0, 31)]
29
+
30
+ # Removing these SHOULD make most filename safe for a wide range of
31
+ # operating systems.
32
+ misc_illegal_chars = [
33
+ '\"', '\#', '\$', '\%', '\'', '\*', '\,', '\.', '\/', '\:',
34
+ '\;', '\<', '\>', '\?', '\\', '\^', '\|', '\~', '\\\\',
35
+ ]
36
+ pattern = '|'.join(ntfs_illegal_chars + misc_illegal_chars)
37
+ forbidden_chars = re.compile(pattern, re.UNICODE)
38
+ filename = forbidden_chars.sub('', output_text)
39
+ return truncate(filename)
40
+
41
+
42
+ def memoize(func):
43
+ cache = func.cache = {}
44
+
45
+ @functools.wraps(func)
46
+ def wrapper(*args, **kwargs):
47
+ key = str(args) + str(kwargs)
48
+ if key not in cache:
49
+ cache[key] = func(*args, **kwargs)
50
+ return cache[key]
51
+ return wrapper
pytube/itags.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.itags
4
+ ~~~~~~~~~~~~
5
+
6
+ """
7
+
8
+ ITAGS = {
9
+ 5: ('240p', '64kbps', 'H.263', None, 30),
10
+ 6: ('270p', '64kbps', 'H.263', None, 30),
11
+ 13: ('144p', None, 'mp4v', None, 30),
12
+ 17: ('144p', '24kbps', 'mp4v', None, 30),
13
+ 18: ('360p', '96kbps', 'H.264', None, 30),
14
+ 22: ('720p', '192kbps', 'H.264', None, 30),
15
+ 34: ('360p', '128kbps', 'H.264', None, 30),
16
+ 35: ('480p', '128kbps', 'H.264', None, 30),
17
+ 36: ('240p', None, 'mp4v', None, 30),
18
+ 37: ('1080p', '192kbps', 'H.264', None, 30),
19
+ 38: ('3072p', '192kbps', 'H.264', None, 30),
20
+ 43: ('360p', '128kbps', 'VP8', None, 30),
21
+ 44: ('480p', '128kbps', 'VP8', None, 30),
22
+ 45: ('720p', '192kbps', 'VP8', None, 30),
23
+ 46: ('1080p', '192kbps', 'VP8', None, 30),
24
+ 59: ('480p', '128kbps', 'H.264', None, 30),
25
+ 78: ('480p', '128kbps', 'H.264', None, 30),
26
+ 82: ('360p', '128kbps', 'H.264', '3D', 30),
27
+ 83: ('480p', '128kbps', 'H.264', '3D', 30),
28
+ 84: ('720p', '192kbps', 'H.264', '3D', 30),
29
+ 85: ('1080p', '192kbps', 'H.264', '3D', 30),
30
+ 91: ('144p', '48kbps', 'H.264', 'HLS', 30),
31
+ 92: ('240p', '48kbps', 'H.264', 'HLS', 30),
32
+ 93: ('360p', '128kbps', 'H.264', 'HLS', 30),
33
+ 94: ('480p', '128kbps', 'H.264', 'HLS', 30),
34
+ 95: ('720p', '256kbps', 'H.264', 'HLS', 30),
35
+ 96: ('1080p', '256kbps', 'H.264', 'HLS', 30),
36
+ 100: ('360p', '128kbps', 'VP8', '3D', 30),
37
+ 101: ('480p', '192kbps', 'VP8', '3D', 30),
38
+ 102: ('720p', '192kbps', 'VP8', '3D', 30),
39
+ 132: ('240p', '48kbps', 'H.264', 'HLS', 30),
40
+ 151: ('720p', '24kbps', 'H.264', 'HLS', 30),
41
+
42
+ # DASH Video
43
+ 133: ('240p', None, 'H.264', 'video', 30),
44
+ 134: ('360p', None, 'H.264', 'video', 30),
45
+ 135: ('480p', None, 'H.264', 'video', 30),
46
+ 136: ('720p', None, 'H.264', 'video', 30),
47
+ 137: ('1080p', None, 'H.264', 'video', 30),
48
+ 138: ('2160p', None, 'H.264', 'video', 30),
49
+ 160: ('144p', None, 'H.264', 'video', 30),
50
+ 167: ('360p', None, 'VP8', 'video', 30),
51
+ 168: ('480p', None, 'VP8', 'video', 30),
52
+ 169: ('720p', None, 'VP8', 'video', 30),
53
+ 170: ('1080p', None, 'VP8', 'video', 30),
54
+ 212: ('480p', None, 'H.264', 'video', 30),
55
+ 218: ('480p', None, 'VP8', 'video', 30),
56
+ 219: ('480p', None, 'VP8', 'video', 30),
57
+ 242: ('240p', None, 'VP9', 'video', 30),
58
+ 243: ('360p', None, 'VP9', 'video', 30),
59
+ 244: ('480p', None, 'VP9', 'video', 30),
60
+ 245: ('480p', None, 'VP9', 'video', 30),
61
+ 246: ('480p', None, 'VP9', 'video', 30),
62
+ 247: ('720p', None, 'VP9', 'video', 30),
63
+ 248: ('1080p', None, 'VP9', 'video', 30),
64
+ 264: ('144p', None, 'H.264', 'video', 30),
65
+ 266: ('2160p', None, 'H.264', 'video', 30),
66
+ 271: ('144p', None, 'VP9', 'video', 30),
67
+ 272: ('2160p', None, 'VP9', 'video', 30),
68
+ 278: ('144p', None, 'VP9', 'video', 30),
69
+ 298: ('720p', None, 'H.264', 'video', 60),
70
+ 299: ('1080p', None, 'H.264', 'video', 60),
71
+ 302: ('720p', None, 'VP9', 'video', 60),
72
+ 303: ('1080p', None, 'VP9', 'video', 60),
73
+ 308: ('1440p', None, 'VP9', 'video', 60),
74
+ 313: ('2160p', None, 'VP9', 'video', 30),
75
+ 315: ('2160p', None, 'VP9', 'video', 60),
76
+
77
+ # DASH Audio
78
+ 139: (None, '48kbps', None, 'audio', None),
79
+ 140: (None, '128kbps', None, 'audio', None),
80
+ 141: (None, '256kbps', None, 'audio', None),
81
+ 171: (None, '128kbps', None, 'audio', None),
82
+ 172: (None, '256kbps', None, 'audio', None),
83
+ 249: (None, '50kbps', None, 'audio', None),
84
+ 250: (None, '70kbps', None, 'audio', None),
85
+ 251: (None, '160kbps', None, 'audio', None),
86
+ 256: (None, None, None, 'audio', None),
87
+ 258: (None, None, None, 'audio', None),
88
+ 325: (None, None, None, 'audio', None),
89
+ 328: (None, None, None, 'audio', None),
90
+ }
91
+
92
+
93
+ def get_format_profile(itag):
94
+ res, bitrate, vcodec, note, fps = ITAGS[int(itag)]
95
+ return {
96
+ 'resolution': res,
97
+ 'abr': bitrate,
98
+ 'fmt_note': note,
99
+ 'fps': fps,
100
+ }
pytube/jsinterp.py DELETED
@@ -1,281 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- from __future__ import unicode_literals
4
-
5
- import json
6
- import operator
7
- import re
8
-
9
- from .exceptions import ExtractorError
10
-
11
- _OPERATORS = [
12
- ('|', operator.or_),
13
- ('^', operator.xor),
14
- ('&', operator.and_),
15
- ('>>', operator.rshift),
16
- ('<<', operator.lshift),
17
- ('-', operator.sub),
18
- ('+', operator.add),
19
- ('%', operator.mod),
20
- ('/', operator.truediv),
21
- ('*', operator.mul),
22
- ]
23
- _ASSIGN_OPERATORS = [(op + '=', opfunc) for op, opfunc in _OPERATORS]
24
- _ASSIGN_OPERATORS.append(('=', lambda cur, right: right))
25
-
26
- _NAME_RE = r'[a-zA-Z_$][a-zA-Z_$0-9]*'
27
-
28
-
29
- class JSInterpreter(object):
30
- def __init__(self, code, objects=None):
31
- if objects is None:
32
- objects = {}
33
- self.code = code
34
- self._functions = {}
35
- self._objects = objects
36
-
37
- def interpret_statement(self, stmt, local_vars, allow_recursion=100):
38
- if allow_recursion < 0:
39
- raise ExtractorError('Recursion limit reached')
40
-
41
- should_abort = False
42
- stmt = stmt.lstrip()
43
- stmt_m = re.match(r'var\s', stmt)
44
- if stmt_m:
45
- expr = stmt[len(stmt_m.group(0)):]
46
- else:
47
- return_m = re.match(r'return(?:\s+|$)', stmt)
48
- if return_m:
49
- expr = stmt[len(return_m.group(0)):]
50
- should_abort = True
51
- else:
52
- # Try interpreting it as an expression
53
- expr = stmt
54
-
55
- v = self.interpret_expression(expr, local_vars, allow_recursion)
56
- return v, should_abort
57
-
58
- def interpret_expression(self, expr, local_vars, allow_recursion):
59
- expr = expr.strip()
60
-
61
- if expr == '': # Empty expression
62
- return None
63
-
64
- if expr.startswith('('):
65
- parens_count = 0
66
- for m in re.finditer(r'[()]', expr):
67
- if m.group(0) == '(':
68
- parens_count += 1
69
- else:
70
- parens_count -= 1
71
- if parens_count == 0:
72
- sub_expr = expr[1:m.start()]
73
- sub_result = self.interpret_expression(
74
- sub_expr, local_vars, allow_recursion,
75
- )
76
- remaining_expr = expr[m.end():].strip()
77
- if not remaining_expr:
78
- return sub_result
79
- else:
80
- expr = json.dumps(sub_result) + remaining_expr
81
- break
82
- else:
83
- raise ExtractorError('Premature end of parens in %r' % expr)
84
-
85
- for op, opfunc in _ASSIGN_OPERATORS:
86
- m = re.match(
87
- r'''(?x)
88
- (?P<out>%s)(?:\[(?P<index>[^\]]+?)\])?
89
- \s*%s
90
- (?P<expr>.*)$''' % (_NAME_RE, re.escape(op)), expr,
91
- )
92
- if not m:
93
- continue
94
- right_val = self.interpret_expression(
95
- m.group('expr'), local_vars, allow_recursion - 1,
96
- )
97
-
98
- if m.groupdict().get('index'):
99
- lvar = local_vars[m.group('out')]
100
- idx = self.interpret_expression(
101
- m.group('index'), local_vars, allow_recursion,
102
- )
103
- assert isinstance(idx, int)
104
- cur = lvar[idx]
105
- val = opfunc(cur, right_val)
106
- lvar[idx] = val
107
- return val
108
- else:
109
- cur = local_vars.get(m.group('out'))
110
- val = opfunc(cur, right_val)
111
- local_vars[m.group('out')] = val
112
- return val
113
-
114
- if expr.isdigit():
115
- return int(expr)
116
-
117
- var_m = re.match(
118
- r'(?!if|return|true|false)(?P<name>%s)$' % _NAME_RE,
119
- expr,
120
- )
121
- if var_m:
122
- return local_vars[var_m.group('name')]
123
-
124
- try:
125
- return json.loads(expr)
126
- except ValueError:
127
- pass
128
-
129
- m = re.match(
130
- r'(?P<var>%s)\.(?P<member>[^(]+)'
131
- '(?:\(+(?P<args>[^()]*)\))?$' % _NAME_RE,
132
- expr,
133
- )
134
- if m:
135
- variable = m.group('var')
136
- member = m.group('member')
137
- arg_str = m.group('args')
138
-
139
- if variable in local_vars:
140
- obj = local_vars[variable]
141
- else:
142
- if variable not in self._objects:
143
- self._objects[variable] = self.extract_object(variable)
144
- obj = self._objects[variable]
145
-
146
- if arg_str is None:
147
- # Member access
148
- if member == 'length':
149
- return len(obj)
150
- return obj[member]
151
-
152
- assert expr.endswith(')')
153
- # Function call
154
- if arg_str == '':
155
- argvals = tuple()
156
- else:
157
- argvals = tuple([
158
- self.interpret_expression(v, local_vars, allow_recursion)
159
- for v in arg_str.split(',')
160
- ])
161
-
162
- if member == 'split':
163
- assert argvals == ('',)
164
- return list(obj)
165
- if member == 'join':
166
- assert len(argvals) == 1
167
- return argvals[0].join(obj)
168
- if member == 'reverse':
169
- assert len(argvals) == 0
170
- obj.reverse()
171
- return obj
172
- if member == 'slice':
173
- assert len(argvals) == 1
174
- return obj[argvals[0]:]
175
- if member == 'splice':
176
- assert isinstance(obj, list)
177
- index, howMany = argvals
178
- res = []
179
- for i in range(index, min(index + howMany, len(obj))):
180
- res.append(obj.pop(index))
181
- return res
182
-
183
- return obj[member](argvals)
184
-
185
- m = re.match(
186
- r'(?P<in>%s)\[(?P<idx>.+)\]$' % _NAME_RE, expr,
187
- )
188
- if m:
189
- val = local_vars[m.group('in')]
190
- idx = self.interpret_expression(
191
- m.group('idx'), local_vars, allow_recursion - 1,
192
- )
193
- return val[idx]
194
-
195
- for op, opfunc in _OPERATORS:
196
- m = re.match(r'(?P<x>.+?)%s(?P<y>.+)' % re.escape(op), expr)
197
- if not m:
198
- continue
199
- x, abort = self.interpret_statement(
200
- m.group('x'), local_vars, allow_recursion - 1,
201
- )
202
- if abort:
203
- raise ExtractorError(
204
- 'Premature left-side return of %s in %r' % (op, expr),
205
- )
206
- y, abort = self.interpret_statement(
207
- m.group('y'), local_vars, allow_recursion - 1,
208
- )
209
- if abort:
210
- raise ExtractorError(
211
- 'Premature right-side return of %s in %r' % (op, expr),
212
- )
213
- return opfunc(x, y)
214
-
215
- m = re.match(
216
- r'^(?P<func>%s)\((?P<args>[a-zA-Z0-9_$,]+)\)$' % _NAME_RE, expr,
217
- )
218
- if m:
219
- fname = m.group('func')
220
- argvals = tuple([
221
- int(v) if v.isdigit() else local_vars[v]
222
- for v in m.group('args').split(',')
223
- ])
224
- if fname not in self._functions:
225
- self._functions[fname] = self.extract_function(fname)
226
- return self._functions[fname](argvals)
227
-
228
- raise ExtractorError('Unsupported JS expression %r' % expr)
229
-
230
- def extract_object(self, objname):
231
- obj = {}
232
- obj_m = re.search(
233
- (r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
234
- r'\s*(?P<fields>([a-zA-Z$0-9]+\s*:\s*function\(.*?\)\s*\{.*?\}'
235
- r'(?:,\s*)?)*)' + r'\}\s*;',
236
- self.code,
237
- )
238
- fields = obj_m.group('fields')
239
- # Currently, it only supports function definitions
240
- fields_m = re.finditer(
241
- r'(?P<key>[a-zA-Z$0-9]+)\s*:\s*function'
242
- r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
243
- fields,
244
- )
245
- for f in fields_m:
246
- argnames = f.group('args').split(',')
247
- obj[f.group('key')] = self.build_function(
248
- argnames, f.group('code'),
249
- )
250
-
251
- return obj
252
-
253
- def extract_function(self, funcname):
254
- func_m = re.search(
255
- r'''(?x)
256
- (?:function\s+%s|[{;,]\s*%s\s*=\s*function|var\s+%s\s*=\s*function)\s*
257
- \((?P<args>[^)]*)\)\s*
258
- \{(?P<code>[^}]+)\}''' % (
259
- re.escape(funcname), re.escape(funcname), re.escape(funcname),
260
- ),
261
- self.code,
262
- )
263
- if func_m is None:
264
- raise ExtractorError('Could not find JS function %r' % funcname)
265
- argnames = func_m.group('args').split(',')
266
-
267
- return self.build_function(argnames, func_m.group('code'))
268
-
269
- def call_function(self, funcname, *args):
270
- f = self.extract_function(funcname)
271
- return f(args)
272
-
273
- def build_function(self, argnames, code):
274
- def resf(args):
275
- local_vars = dict(zip(argnames, args))
276
- for stmt in code.split(';'):
277
- res, abort = self.interpret_statement(stmt, local_vars)
278
- if abort:
279
- break
280
- return res
281
- return resf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytube/mixins.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.mixins
4
+ ~~~~~~~~~~~~~
5
+
6
+ """
7
+ from urllib.parse import parse_qsl
8
+ from urllib.parse import unquote
9
+
10
+ from pytube import cipher
11
+
12
+
13
+ def apply(dct, key, fn):
14
+ dct[key] = fn(dct[key])
15
+
16
+
17
+ def apply_cipher(video_info, fmt, js):
18
+ stream_map = video_info[fmt]
19
+ for i, stream in enumerate(stream_map):
20
+ url = stream['url']
21
+ if 'signature=' in url:
22
+ continue
23
+ signature = cipher.get_signature(js, stream['s'])
24
+ stream_map[i]['url'] = url + '&signature=' + signature
25
+
26
+
27
+ def apply_fmt_decoder(video_info, fmt):
28
+ video_info[fmt] = [
29
+ {k: unquote(v) for k, v in parse_qsl(i)} for i
30
+ in video_info[fmt].split(',')
31
+ ]
pytube/models.py DELETED
@@ -1,146 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- from __future__ import unicode_literals
4
-
5
- import os
6
- from time import clock
7
-
8
- try:
9
- from urllib2 import urlopen
10
- except ImportError:
11
- from urllib.request import urlopen
12
-
13
-
14
- class Video(object):
15
- """Class representation of a single instance of a YouTube video.
16
- """
17
-
18
- def __init__(
19
- self, url, filename, extension, resolution=None,
20
- video_codec=None, profile=None, video_bitrate=None,
21
- audio_codec=None, audio_bitrate=None,
22
- ):
23
- """Sets-up the video object.
24
-
25
- :param str url:
26
- The url of the video. (e.g.: https://youtube.com/watch?v=...)
27
- :param str filename:
28
- The filename (minus the extention) to save the video.
29
- :param str extention:
30
- The desired file extention (e.g.: mp4, flv, webm).
31
- :param str resolution:
32
- *Optional* The broadcasting standard (e.g.: 720p, 1080p).
33
- :param str video_codec:
34
- *Optional* The codec used to encode the video.
35
- :param str profile:
36
- *Optional* The arbitrary quality profile.
37
- :param str video_bitrate:
38
- *Optional* The bitrate of the video over sampling interval.
39
- :param str audio_codec:
40
- *Optional* The codec used to encode the audio.
41
- :param str audio_bitrate:
42
- *Optional* The bitrate of the video's audio over sampling interval.
43
- """
44
- self.url = url
45
- self.filename = filename
46
- self.extension = extension
47
- self.resolution = resolution
48
- self.video_codec = video_codec
49
- self.profile = profile
50
- self.video_bitrate = video_bitrate
51
- self.audio_codec = audio_codec
52
- self.audio_bitrate = audio_bitrate
53
-
54
- def download(
55
- self, path, chunk_size=8 * 1024, on_progress=None,
56
- on_finish=None, force_overwrite=False,
57
- ):
58
- """Downloads the video.
59
-
60
- :param str path:
61
- The destination output directory.
62
- :param int chunk_size:
63
- File size (in bytes) to write to buffer at a time. By default,
64
- this is set to 8 bytes.
65
- :param func on_progress:
66
- *Optional* function to be called every time the buffer is written
67
- to. Arguments passed are the bytes recieved, file size, and start
68
- datetime.
69
- :param func on_finish:
70
- *Optional* callback function when download is complete. Arguments
71
- passed are the full path to downloaded the file.
72
- :param bool force_overwrite:
73
- *Optional* force a file overwrite if conflicting one exists.
74
- """
75
- path = os.path.normpath(path)
76
- if not os.path.isdir(path):
77
- raise OSError('Make sure path exists.')
78
-
79
- filename = '{0}.{1}'.format(self.filename, self.extension)
80
- path = os.path.join(path, filename)
81
- # TODO: If it's not a path, this should raise an ``OSError``.
82
- # TODO: Move this into cli, this kind of logic probably shouldn't be
83
- # handled by the library.
84
- if os.path.isfile(path) and not force_overwrite:
85
- raise OSError("Conflicting filename:'{0}'".format(self.filename))
86
- # TODO: Split up the downloading and OS jazz into separate functions.
87
- response = urlopen(self.url)
88
- file_size = self.file_size(response)
89
- self._bytes_received = 0
90
- start = clock()
91
- # TODO: Let's get rid of this whole try/except block, let ``OSErrors``
92
- # fail loudly.
93
- try:
94
- with open(path, 'wb') as dst_file:
95
- while True:
96
- self._buffer = response.read(chunk_size)
97
- # Check if the buffer is empty (aka no bytes remaining).
98
- if not self._buffer:
99
- if on_finish:
100
- # TODO: We possibly want to flush the
101
- # `_bytes_recieved`` buffer before we call
102
- # ``on_finish()``.
103
- on_finish(path)
104
- break
105
-
106
- self._bytes_received += len(self._buffer)
107
- dst_file.write(self._buffer)
108
- if on_progress:
109
- on_progress(self._bytes_received, file_size, start)
110
-
111
- except KeyboardInterrupt:
112
- # TODO: Move this into the cli, ``KeyboardInterrupt`` handling
113
- # should be taken care of by the client. Also you should be allowed
114
- # to disable this.
115
- os.remove(path)
116
- raise KeyboardInterrupt(
117
- 'Interrupt signal given. Deleting incomplete video.',
118
- )
119
-
120
- def file_size(self, response):
121
- """Gets the file size from the response
122
-
123
- :param response:
124
- Response of a opened url.
125
- """
126
- meta_data = dict(response.info().items())
127
- return int(meta_data.get('Content-Length') or
128
- meta_data.get('content-length'))
129
-
130
- def __repr__(self):
131
- """A clean representation of the class instance."""
132
- return '<Video: {0} (.{1}) - {2} - {3}>'.format(
133
- self.video_codec, self.extension, self.resolution, self.profile,
134
- )
135
-
136
- def __lt__(self, other):
137
- """The "less than" (lt) method is used for comparing video object to
138
- one another. This useful when sorting.
139
-
140
- :param other:
141
- The instance of the other video instance for comparison.
142
- """
143
- if isinstance(other, Video):
144
- v1 = '{0} {1}'.format(self.extension, self.resolution)
145
- v2 = '{0} {1}'.format(other.extension, other.resolution)
146
- return (v1 > v2) - (v1 < v2) < 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytube/query.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.query
4
+ ~~~~~~~~~~~~
5
+
6
+ """
7
+
8
+
9
+ class StreamQuery:
10
+ def __init__(self, fmt_streams):
11
+ self.fmt_streams = fmt_streams
12
+ self.itag_index = {int(s.itag): s for s in fmt_streams}
13
+
14
+ def filter(
15
+ self, fps=None, res=None, resolution=None, mime_type=None,
16
+ type=None, subtype=None, abr=None, bitrate=None,
17
+ video_codec=None, audio_codec=None,
18
+ custom_filter_functions=None,
19
+ ):
20
+ filters = []
21
+ if res or resolution:
22
+ filters.append(lambda s: s.resolution == (res or resolution))
23
+
24
+ if fps:
25
+ filters.append(lambda s: s.fps == fps)
26
+
27
+ if mime_type:
28
+ filters.append(lambda s: s.mime_type == mime_type)
29
+
30
+ if type:
31
+ filters.append(lambda s: s.type == type)
32
+
33
+ if subtype:
34
+ filters.append(lambda s: s.subtype == subtype)
35
+
36
+ if abr or bitrate:
37
+ filters.append(lambda s: s.abr == (abr or bitrate))
38
+
39
+ if video_codec:
40
+ filters.append(lambda s: s.video_codec == video_codec)
41
+
42
+ if audio_codec:
43
+ filters.append(lambda s: s.audio_codec == audio_codec)
44
+
45
+ if custom_filter_functions:
46
+ for fn in custom_filter_functions:
47
+ filters.append(fn)
48
+
49
+ fmt_streams = self.fmt_streams
50
+ for fn in filters:
51
+ fmt_streams = list(filter(fn, fmt_streams))
52
+ return StreamQuery(fmt_streams)
53
+
54
+ def get(self, itag):
55
+ return self.itag_index[itag]
56
+
57
+ def first(self):
58
+ return self.fmt_streams[0]
59
+
60
+ def last(self):
61
+ return self.fmt_streams[-1]
62
+
63
+ def count(self):
64
+ return len(self.fmt_streams)
65
+
66
+ def all(self):
67
+ return self.fmt_streams
pytube/streams.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ pytube.streams
4
+ ~~~~~~~~~~~~~~
5
+
6
+ """
7
+ import os
8
+ import re
9
+
10
+ from pytube import download
11
+ from pytube.helpers import memoize
12
+ from pytube.helpers import safe_filename
13
+ from pytube.itags import get_format_profile
14
+
15
+
16
+ class Stream:
17
+ def __init__(self, stream, player_config, shared_stream_state):
18
+ self.shared_stream_state = shared_stream_state
19
+ self.abr = None
20
+ self.audio_codec = None
21
+ self.codecs = None
22
+ self.fps = None
23
+ self.itag = None
24
+ self.mime_type = None
25
+ self.res = None
26
+ self.subtype = None
27
+ self.type = None
28
+ self.url = None
29
+ self.video_codec = None
30
+
31
+ self.set_attributes_from_dict(stream)
32
+ self.fmt_profile = get_format_profile(self.itag)
33
+ self.set_attributes_from_dict(self.fmt_profile)
34
+
35
+ self.player_config = player_config
36
+ self.mime_type, self.codecs = self.parse_type()
37
+ self.type, self.subtype = self.mime_type.split('/')
38
+ self.video_codec, self.audio_codec = self.parse_codecs()
39
+
40
+ def set_attributes_from_dict(self, dct):
41
+ for key, val in dct.items():
42
+ setattr(self, key, val)
43
+
44
+ @property
45
+ def is_dash(self):
46
+ return len(self.codecs) % 2
47
+
48
+ @property
49
+ def is_audio(self):
50
+ return self.type == 'audio'
51
+
52
+ @property
53
+ def is_video(self):
54
+ return self.type == 'video'
55
+
56
+ def parse_codecs(self):
57
+ video = None
58
+ audio = None
59
+ if not self.is_dash:
60
+ video, audio = self.codecs
61
+ elif self.is_video:
62
+ video = self.codecs[0]
63
+ elif self.is_audio:
64
+ audio = self.codecs[0]
65
+ return video, audio
66
+
67
+ def parse_type(self):
68
+ regex = re.compile(r'(\w+\/\w+)\;\scodecs=\"([a-zA-Z-0-9.,\s]*)\"')
69
+ mime_type, codecs = regex.search(self.type).groups()
70
+ return mime_type, [c.strip() for c in codecs.split(',')]
71
+
72
+ @property
73
+ @memoize
74
+ def filesize(self):
75
+ headers = download.headers(self.url)
76
+ return int(headers['Content-Length'])
77
+
78
+ @property
79
+ def default_filename(self):
80
+ title = self.player_config['args']['title']
81
+ filename = safe_filename(title)
82
+ return '{filename}.{s.subtype}'.format(filename=filename, s=self)
83
+
84
+ def on_progress(self, chunk, file_handler, bytes_remaining):
85
+ file_handler.write(chunk)
86
+ on_progress = self.shared_stream_state['on_progress']
87
+ if on_progress:
88
+ on_progress(self, chunk, bytes_remaining)
89
+
90
+ def on_complete(self, fh):
91
+ on_complete = self.shared_stream_state['on_complete']
92
+ if on_complete:
93
+ on_complete(self, fh)
94
+
95
+ def download(self, output_path=None):
96
+ output_path = output_path or os.getcwd()
97
+ fp = os.path.join(output_path, self.default_filename)
98
+ bytes_remaining = self.filesize
99
+ with open(fp, 'wb') as fh:
100
+ for chunk in download.stream(self.url):
101
+ bytes_remaining -= len(chunk)
102
+ self.on_progress(chunk, fh, bytes_remaining)
103
+
104
+ def __repr__(self):
105
+ parts = [
106
+ 'itag="{self.itag}"',
107
+ 'mime_type="{self.mime_type}"',
108
+ ]
109
+ if self.is_video:
110
+ parts.extend([
111
+ 'res="{self.resolution}"',
112
+ 'fps="{self.fps}fps"',
113
+ ])
114
+ if not self.is_dash:
115
+ parts.extend([
116
+ 'vcodec="{self.video_codec}"',
117
+ 'acodec="{self.audio_codec}"',
118
+ ])
119
+ else:
120
+ parts.extend([
121
+ 'vcodec="{self.video_codec}"',
122
+ ])
123
+ else:
124
+ parts.extend([
125
+ 'abr="{self.abr}"',
126
+ 'acodec="{self.audio_codec}"',
127
+ ])
128
+ parts = ' '.join(parts)
129
+ return '<Stream: {parts}>'.format(self=self)
pytube/utils.py DELETED
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- import argparse
4
- import math
5
- import re
6
- from os import path
7
- from sys import stdout
8
- from time import clock
9
-
10
-
11
- class FullPaths(argparse.Action):
12
- """Expand user- and relative-paths"""
13
-
14
- def __call__(self, parser, namespace, values, option_string=None):
15
- setattr(namespace, self.dest, path.abspath(path.expanduser(values)))
16
-
17
-
18
- def truncate(text, max_length=200):
19
- return text[:max_length].rsplit(' ', 0)[0]
20
-
21
-
22
- def safe_filename(text, max_length=200):
23
- """Sanitizes filenames for many operating systems.
24
-
25
- :params text: The unsanitized pending filename.
26
- """
27
-
28
- # Tidy up ugly formatted filenames.
29
- text = text.replace('_', ' ')
30
- text = text.replace(':', ' -')
31
-
32
- # NTFS forbids filenames containing characters in range 0-31 (0x00-0x1F)
33
- ntfs = [chr(i) for i in range(0, 31)]
34
-
35
- # Removing these SHOULD make most filename safe for a wide range of
36
- # operating systems.
37
- paranoid = [
38
- '\"', '\#', '\$', '\%', '\'', '\*', '\,', '\.', '\/', '\:',
39
- '\;', '\<', '\>', '\?', '\\', '\^', '\|', '\~', '\\\\',
40
- ]
41
-
42
- blacklist = re.compile('|'.join(ntfs + paranoid), re.UNICODE)
43
- filename = blacklist.sub('', text)
44
- return truncate(filename)
45
-
46
-
47
- def sizeof(byts):
48
- """Takes the size of file or folder in bytes and returns size formatted in
49
- KB, MB, GB, TB or PB.
50
- :params byts:
51
- Size of the file in bytes
52
- """
53
- sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
54
- power = int(math.floor(math.log(byts, 1024)))
55
- value = int(byts / float(1024**power))
56
- suffix = sizes[power] if byts != 1 else 'byte'
57
- return '{0} {1}'.format(value, suffix)
58
-
59
-
60
- def print_status(progress, file_size, start):
61
- """
62
- This function - when passed as `on_progress` to `Video.download` - prints
63
- out the current download progress.
64
-
65
- :params progress:
66
- The lenght of the currently downloaded bytes.
67
- :params file_size:
68
- The total size of the video.
69
- :params start:
70
- The time when started
71
- """
72
-
73
- percent_done = int(progress) * 100. / file_size
74
- done = int(50 * progress / int(file_size))
75
- dt = (clock() - start)
76
- if dt > 0:
77
- stdout.write('\r [%s%s][%3.2f%%] %s at %s/s ' %
78
- (
79
- '=' * done, ' ' * (50 - done), percent_done,
80
- sizeof(file_size), sizeof(progress // dt),
81
- ))
82
- stdout.flush()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
setup.py CHANGED
@@ -21,7 +21,7 @@ setup(
21
  license=license,
22
  entry_points={
23
  'console_scripts': [
24
- 'pytube = pytube.__main__:main',
25
  ],
26
  },
27
  classifiers=[
 
21
  license=license,
22
  entry_points={
23
  'console_scripts': [
24
+ 'pytube = pytube.cli:main',
25
  ],
26
  },
27
  classifiers=[
tests/conftest.py DELETED
@@ -1,24 +0,0 @@
1
- import mock
2
- import pytest
3
-
4
- from pytube import api
5
-
6
-
7
- @pytest.fixture
8
- def yt_video():
9
- url = 'http://www.youtube.com/watch?v=9bZkp7q19f0'
10
- mock_html = None
11
- mock_js = None
12
-
13
- with open('tests/mock_data/youtube_gangnam_style.html') as fh:
14
- mock_html = fh.read()
15
-
16
- with open('tests/mock_data/youtube_gangnam_style.js') as fh:
17
- mock_js = fh.read()
18
-
19
- with mock.patch('pytube.api.urlopen') as urlopen:
20
- urlopen.return_value.read.return_value = mock_html
21
- yt = api.YouTube()
22
- yt._js_cache = mock_js
23
- yt.from_url(url)
24
- return yt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/mock_data/youtube_age_restricted.html DELETED
@@ -1,930 +0,0 @@
1
- <!DOCTYPE html><html lang="en" data-cast-api-enabled="true"><head><style name="www-roboto">@font-face{font-family:'Roboto';font-style:normal;font-weight:400;src:local('Roboto Regular'),local('Roboto-Regular'),url(//fonts.gstatic.com/s/roboto/v15/zN7GBFwfMP4uA6AR0HCoLQ.ttf)format('truetype');}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;src:local('Roboto Medium'),local('Roboto-Medium'),url(//fonts.gstatic.com/s/roboto/v15/RxZJdnzeo3R5zSexge8UUaCWcynf_cDxXwCLxiixG1c.ttf)format('truetype');}@font-face{font-family:'Roboto';font-style:italic;font-weight:400;src:local('Roboto Italic'),local('Roboto-Italic'),url(//fonts.gstatic.com/s/roboto/v15/W4wDsBUluyw0tK3tykhXEfesZW2xOQ-xsNqO47m55DA.ttf)format('truetype');}@font-face{font-family:'Roboto';font-style:italic;font-weight:500;src:local('Roboto Medium Italic'),local('Roboto-MediumItalic'),url(//fonts.gstatic.com/s/roboto/v15/OLffGBTaF0XFOW1gnuHF0Z0EAVxt0G0biEntp43Qt6E.ttf)format('truetype');}</style><script name="www-roboto">if (document.fonts && document.fonts.load) {document.fonts.load("400 10pt Roboto", "E");document.fonts.load("500 10pt Roboto", "E");}</script><script>var ytcsi = {gt: function(n) {n = (n || '') + 'data_';return ytcsi[n] || (ytcsi[n] = {tick: {},span: {},info: {}});},tick: function(l, t, n) {ytcsi.gt(n).tick[l] = t || +new Date();},span: function(l, s, e, n) {ytcsi.gt(n).span[l] = (e ? e : +new Date()) - ytcsi.gt(n).tick[s];},setSpan: function(l, s, n) {ytcsi.gt(n).span[l] = s;},info: function(k, v, n) {ytcsi.gt(n).info[k] = v;},setStart: function(s, t, n) {ytcsi.info('yt_sts', s, n);ytcsi.tick('_start', t, n);}};(function(w, d) {ytcsi.perf = w.performance || w.mozPerformance ||w.msPerformance || w.webkitPerformance;ytcsi.setStart('dhs', ytcsi.perf ? ytcsi.perf.timing.responseStart : null);var isPrerender = (d.visibilityState || d.webkitVisibilityState) == 'prerender';var vName = d.webkitVisibilityState ? 'webkitvisibilitychange' : 'visibilitychange';if (isPrerender) {ytcsi.info('prerender', 1);var startTick = function() {ytcsi.setStart('dhs');d.removeEventListener(vName, startTick);};d.addEventListener(vName, startTick, false);}if (d.addEventListener) {d.addEventListener(vName, function() {ytcsi.tick('vc');}, false);}})(window, document);</script><script>var ytcfg = {d: function() {return (window.yt && yt.config_) || ytcfg.data_ || (ytcfg.data_ = {});},get: function(k, o) {return (k in ytcfg.d()) ? ytcfg.d()[k] : o;},set: function() {var a = arguments;if (a.length > 1) {ytcfg.d()[a[0]] = a[1];} else {for (var k in a[0]) {ytcfg.d()[k] = a[0][k];}}}};</script> <script>ytcfg.set("LACT", null);</script>
2
-
3
-
4
-
5
-
6
-
7
- <script>
8
- (function(){var b={f:"content-snap-width-1",h:"content-snap-width-2",j:"content-snap-width-3"};function f(){var a=[],c;for(c in b)a.push(b[c]);return a}function g(a){var c=f().concat(["guide-pinned","show-guide"]),d=c.length,e=[];a.replace(/\S+/g,function(a){for(var k=0;k<d;k++)if(a==c[k])return;e.push(a)});return e};function l(a,c,d){var e=document.getElementsByTagName("html")[0],h=g(e.className);a&&1251<=(window.innerWidth||document.documentElement.clientWidth)&&(h.push("guide-pinned"),c&&h.push("show-guide"));d&&(d=(window.innerWidth||document.documentElement.clientWidth)-21-50,1251<=(window.innerWidth||document.documentElement.clientWidth)&&a&&c&&(d-=230),h.push(1262<=d?"content-snap-width-3":1056<=d?"content-snap-width-2":"content-snap-width-1"));e.className=h.join(" ")}
9
- var m=["yt","www","masthead","sizing","runBeforeBodyIsReady"],n=this;m[0]in n||!n.execScript||n.execScript("var "+m[0]);for(var p;m.length&&(p=m.shift());)m.length||void 0===l?n[p]?n=n[p]:n=n[p]={}:n[p]=l;})();
10
-
11
- try {window.ytbuffer = {};ytbuffer.handleClick = function(e) {var element = e.target || e.srcElement;while (element.parentElement) {if (/(^| )yt-can-buffer( |$)/.test(element.className)) {window.ytbuffer = {bufferedClick: e};element.className += ' yt-is-buffered';break;}element = element.parentElement;}};if (document.addEventListener) {document.addEventListener('click', ytbuffer.handleClick);} else {document.attachEvent('onclick', ytbuffer.handleClick);}} catch(e) {}
12
-
13
- yt.www.masthead.sizing.runBeforeBodyIsReady(false,false,true);
14
- </script>
15
-
16
- <script src="//s.ytimg.com/yts/jsbin/scheduler-vflg2uLA4/scheduler.js" type="text/javascript" name="scheduler/scheduler"></script>
17
-
18
-
19
- <script>var ytimg = {};ytimg.count = 1;ytimg.preload = function(src) {var img = new Image();var count = ++ytimg.count;ytimg[count] = img;img.onload = img.onerror = function() {delete ytimg[count];};img.src = src;};</script>
20
-
21
- <script src="//s.ytimg.com/yts/jsbin/www-spacecast-early-vflYMfQ_6/www-spacecast-early.js" type="text/javascript" name="www-spacecast-early/www-spacecast-early"></script>
22
-
23
- <script>ytbin.www.spacecast.early.prefetch('nzNgkc6t260');</script>
24
-
25
- <script src="//s.ytimg.com/yts/jsbin/html5player-new-en_US-vflIUNjzZ/html5player-new.js" type="text/javascript" name="html5player-legacy/html5player-legacy"></script>
26
-
27
-
28
-
29
- <link rel="stylesheet" href="//s.ytimg.com/yts/cssbin/www-core-vflrZizp3.css" name="www-core">
30
- <link rel="stylesheet" href="//s.ytimg.com/yts/cssbin/www-player-new-vfl1mGUtZ.css" name="www-player">
31
-
32
- <link rel="stylesheet" href="//s.ytimg.com/yts/cssbin/www-pageframe-vflAoSuzf.css" name="www-pageframe">
33
-
34
-
35
-
36
- <title>Kara Tointon (Age-restricted video) - YouTube</title><link rel="search" type="application/opensearchdescription+xml" href="https://www.youtube.com/opensearch?locale=en_US" title="YouTube Video Search"><link rel="shortcut icon" href="https://s.ytimg.com/yts/img/favicon-vflz7uhzw.ico" type="image/x-icon"> <link rel="icon" href="//s.ytimg.com/yts/img/favicon_32-vfl8NGn4k.png" sizes="32x32"><link rel="icon" href="//s.ytimg.com/yts/img/favicon_48-vfl1s0rGh.png" sizes="48x48"><link rel="icon" href="//s.ytimg.com/yts/img/favicon_96-vfldSA3ca.png" sizes="96x96"><link rel="icon" href="//s.ytimg.com/yts/img/favicon_144-vflWmzoXw.png" sizes="144x144"><link rel="canonical" href="http://www.youtube.com/watch?v=nzNgkc6t260"><link rel="alternate" media="handheld" href="http://m.youtube.com/watch?v=nzNgkc6t260"><link rel="alternate" media="only screen and (max-width: 640px)" href="http://m.youtube.com/watch?v=nzNgkc6t260"><link rel="shortlink" href="https://youtu.be/nzNgkc6t260"> <meta name="title" content="Kara Tointon (Age-restricted video)">
37
-
38
- <meta name="description" content="Kara Tointon [EASTENDERS] - Dawn Swann tries Modelling.: https://www.youtube.com/watch?v=keX0Fh_x7J8 Sunday Brunch Kara Tointon Interview: https://www.youtub...">
39
-
40
- <meta name="keywords" content="People, Person, Public, Kara Tointon, Kara Louise Tointon, Dawn Swann, EastEnders, Strictly Come Dancing, Actress, British, BBC, Sport Relief, Don&#39;t Call Me ...">
41
-
42
- <link rel="alternate" href="android-app://com.google.android.youtube/http/www.youtube.com/watch?v=nzNgkc6t260">
43
- <link rel="alternate" href="ios-app://544007664/vnd.youtube/www.youtube.com/watch?v=nzNgkc6t260">
44
-
45
- <link rel="alternate" type="application/json+oembed" href="http://www.youtube.com/oembed?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DnzNgkc6t260&amp;format=json" title="Kara Tointon (Age-restricted video)">
46
- <link rel="alternate" type="text/xml+oembed" href="http://www.youtube.com/oembed?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DnzNgkc6t260&amp;format=xml" title="Kara Tointon (Age-restricted video)">
47
-
48
- <meta property="og:site_name" content="YouTube">
49
- <meta property="og:url" content="http://www.youtube.com/watch?v=nzNgkc6t260">
50
- <meta property="og:title" content="Kara Tointon (Age-restricted video)">
51
- <meta property="og:image" content="https://i.ytimg.com/vi/nzNgkc6t260/maxresdefault.jpg">
52
-
53
- <meta property="og:description" content="Kara Tointon [EASTENDERS] - Dawn Swann tries Modelling.: https://www.youtube.com/watch?v=keX0Fh_x7J8 Sunday Brunch Kara Tointon Interview: https://www.youtub...">
54
-
55
- <meta property="al:ios:app_store_id" content="544007664">
56
- <meta property="al:ios:app_name" content="YouTube">
57
- <meta property="al:ios:url" content="vnd.youtube://www.youtube.com/watch?v=nzNgkc6t260&amp;feature=applinks">
58
-
59
- <meta property="al:android:url" content="vnd.youtube://www.youtube.com/watch?v=nzNgkc6t260&amp;feature=applinks">
60
- <meta property="al:android:app_name" content="YouTube">
61
- <meta property="al:android:package" content="com.google.android.youtube">
62
- <meta property="al:web:url" content="http://www.youtube.com/watch?v=nzNgkc6t260&amp;feature=applinks">
63
-
64
- <meta property="og:type" content="video">
65
- <meta property="og:video:url" content="https://www.youtube.com/embed/nzNgkc6t260">
66
- <meta property="og:video:secure_url" content="https://www.youtube.com/embed/nzNgkc6t260">
67
- <meta property="og:video:type" content="text/html">
68
- <meta property="og:video:width" content="1280">
69
- <meta property="og:video:height" content="720">
70
- <meta property="og:video:url" content="http://www.youtube.com/v/nzNgkc6t260?version=3&amp;autohide=1">
71
- <meta property="og:video:secure_url" content="https://www.youtube.com/v/nzNgkc6t260?version=3&amp;autohide=1">
72
- <meta property="og:video:type" content="application/x-shockwave-flash">
73
- <meta property="og:video:width" content="1280">
74
- <meta property="og:video:height" content="720">
75
-
76
- <meta property="og:video:tag" content="People">
77
- <meta property="og:video:tag" content="Person">
78
- <meta property="og:video:tag" content="Public">
79
- <meta property="og:video:tag" content="Kara Tointon">
80
- <meta property="og:video:tag" content="Kara Louise Tointon">
81
- <meta property="og:video:tag" content="Dawn Swann">
82
- <meta property="og:video:tag" content="EastEnders">
83
- <meta property="og:video:tag" content="Strictly Come Dancing">
84
- <meta property="og:video:tag" content="Actress">
85
- <meta property="og:video:tag" content="British">
86
- <meta property="og:video:tag" content="BBC">
87
- <meta property="og:video:tag" content="Sport Relief">
88
- <meta property="og:video:tag" content="Don&#39;t Call Me Stupid">
89
- <meta property="og:video:tag" content="BBC Three">
90
-
91
- <meta property="fb:app_id" content="87741124305">
92
- <meta property="og:restrictions:age" content="18+">
93
-
94
- <meta name="twitter:card" content="player">
95
- <meta name="twitter:site" content="@youtube">
96
- <meta name="twitter:url" content="http://www.youtube.com/watch?v=nzNgkc6t260">
97
- <meta name="twitter:title" content="Kara Tointon (Age-restricted video)">
98
- <meta name="twitter:description" content="Kara Tointon [EASTENDERS] - Dawn Swann tries Modelling.: https://www.youtube.com/watch?v=keX0Fh_x7J8 Sunday Brunch Kara Tointon Interview: https://www.youtub...">
99
- <meta name="twitter:image" content="https://i.ytimg.com/vi/nzNgkc6t260/maxresdefault.jpg">
100
- <meta name="twitter:app:name:iphone" content="YouTube">
101
- <meta name="twitter:app:id:iphone" content="544007664">
102
- <meta name="twitter:app:name:ipad" content="YouTube">
103
- <meta name="twitter:app:id:ipad" content="544007664">
104
- <meta name="twitter:app:url:iphone" content="vnd.youtube://www.youtube.com/watch?v=nzNgkc6t260&amp;feature=applinks">
105
- <meta name="twitter:app:url:ipad" content="vnd.youtube://www.youtube.com/watch?v=nzNgkc6t260&amp;feature=applinks">
106
- <meta name="twitter:app:name:googleplay" content="YouTube">
107
- <meta name="twitter:app:id:googleplay" content="com.google.android.youtube">
108
- <meta name="twitter:app:url:googleplay" content="http://www.youtube.com/watch?v=nzNgkc6t260">
109
- <meta name="twitter:player" content="https://www.youtube.com/embed/nzNgkc6t260">
110
- <meta name="twitter:player:width" content="1280">
111
- <meta name="twitter:player:height" content="720">
112
-
113
-
114
- <style>.exp-responsive #content .yt-uix-button-subscription-container .yt-short-subscriber-count {display: inline-block;}.exp-responsive #content .yt-uix-button-subscription-container .yt-subscriber-count {display: none;}@media only screen and (min-width: 850px) {.exp-responsive #content .yt-uix-button-subscription-container .yt-short-subscriber-count {display: none;}.exp-responsive #content .yt-uix-button-subscription-container .yt-subscriber-count {display: inline-block;}}</style></head> <body dir="ltr" id="body" class=" ltr exp-hamburglar exp-responsive exp-scrollable-guide exp-watch-controls-overlay site-center-aligned site-as-giant-card appbar-hidden not-nirvana-dogfood not-yt-legacy-css flex-width-enabled flex-width-enabled-snap delayed-frame-styles-not-in " data-spf-name="watch">
115
- <div id="early-body"></div><div id="body-container"><div id="a11y-announcements-container" role="alert"><div id="a11y-announcements-message"></div></div><form name="logoutForm" method="POST" action="/logout"><input type="hidden" name="action_logout" value="1"></form><div id="masthead-positioner">
116
- <div id="yt-masthead-container" class="clearfix yt-base-gutter"> <button id="a11y-skip-nav" class="skip-nav" data-target-id="main" tabindex="3">
117
- Skip navigation
118
- </button>
119
- <div id="yt-masthead"><div class="yt-masthead-logo-container "> <div id="appbar-guide-button-container">
120
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-text yt-uix-button-empty yt-uix-button-has-icon appbar-guide-toggle appbar-guide-clickable-ancestor" type="button" onclick=";return false;" aria-label="Guide" aria-controls="appbar-guide-menu" id="appbar-guide-button"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-appbar-guide yt-sprite"></span></span></button>
121
- <div id="appbar-guide-button-notification-check" class="yt-valign">
122
- <span class="appbar-guide-notification-icon yt-valign-content yt-sprite"></span>
123
- </div>
124
- </div>
125
- <div id="appbar-main-guide-notification-container"></div>
126
- <a id="logo-container" href="/" title="YouTube home" class=" spf-link masthead-logo-renderer yt-uix-sessionlink" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw&amp;ved=CAEQsV4"><span class="logo masthead-logo-renderer-logo yt-sprite"></span></a>
127
- </div><div id="yt-masthead-signin"><a href="//www.youtube.com/upload" class="yt-uix-button yt-uix-sessionlink yt-uix-button-default yt-uix-button-size-default" data-sessionlink="feature=mhsb&amp;ei=ig0bVurxA9qp-QWVhLDIDw" id="upload-btn"><span class="yt-uix-button-content">Upload</span></a><div class="signin-container "><button class="yt-uix-button yt-uix-button-size-default yt-uix-button-primary" type="button" onclick=";window.location.href=this.getAttribute(&#39;href&#39;);return false;" href="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252Fwatch%253Fv%253DnzNgkc6t260%26feature%3Dsign_in_button%26action_handle_signin%3Dtrue&amp;hl=en&amp;service=youtube&amp;passive=true&amp;uilel=3" role="link"><span class="yt-uix-button-content">Sign in</span></button></div></div><div id="yt-masthead-content"><form id="masthead-search" class="search-form consolidated-form" action="/results" onsubmit="if (document.getElementById(&#39;masthead-search-term&#39;).value == &#39;&#39;) return false;" data-clicktracking="CAMQ7VA"><button class="yt-uix-button yt-uix-button-size-default yt-uix-button-default search-btn-component search-button" type="submit" onclick="if (document.getElementById(&#39;masthead-search-term&#39;).value == &#39;&#39;) return false; document.getElementById(&#39;masthead-search&#39;).submit(); return false;;return true;" tabindex="2" id="search-btn" dir="ltr"><span class="yt-uix-button-content">Search</span></button><div id="masthead-search-terms" class="masthead-search-terms-border" dir="ltr"><input id="masthead-search-term" autocomplete="off" onkeydown="if (!this.value &amp;&amp; (event.keyCode == 40 || event.keyCode == 32 || event.keyCode == 34)) {this.onkeydown = null; this.blur();}" class="search-term masthead-search-renderer-input yt-uix-form-input-bidi" name="search_query" value="" type="text" tabindex="1" placeholder="" title="Search"></div></form></div></div></div>
128
- <div id="masthead-appbar-container" class="clearfix"><div id="masthead-appbar"><div id="appbar-content" class=""></div></div></div>
129
-
130
- </div><div id="masthead-positioner-height-offset"></div><div id="page-container"><div id="page" class=" watch watch-non-stage-mode clearfix"><div id="guide" class="yt-scrollbar"> <div id="appbar-guide-menu" class="appbar-menu appbar-guide-menu-layout appbar-guide-clickable-ancestor">
131
- <div id="guide-container">
132
- <div class="guide-module-content guide-module-loading">
133
- <p class="yt-spinner ">
134
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
135
-
136
- <span class="yt-spinner-message">
137
- Loading...
138
- </span>
139
- </p>
140
-
141
- </div>
142
- </div>
143
- </div>
144
-
145
- </div><div class="alerts-wrapper"><div id="alerts" class="content-alignment">
146
- <div id="editor-progress-alert-container"></div>
147
- <div class="yt-alert yt-alert-default yt-alert-warn hid " id="editor-progress-alert-template"> <div class="yt-alert-icon">
148
- <span class="icon master-sprite yt-sprite"></span>
149
- </div>
150
- <div class="yt-alert-content" role="alert"></div><div class="yt-alert-buttons"><button class="yt-uix-button yt-uix-button-size-default yt-uix-button-close close yt-uix-close" type="button" onclick=";return false;" aria-label="Close" data-close-parent-class="yt-alert"><span class="yt-uix-button-content">Close</span></button></div></div>
151
-
152
- <div id="edit-confirmation-alert"></div>
153
- <div class="yt-alert yt-alert-actionable yt-alert-info hid " id="edit-confirmation-alert-template"> <div class="yt-alert-icon">
154
- <span class="icon master-sprite yt-sprite"></span>
155
- </div>
156
- <div class="yt-alert-content" role="alert"> <div class="yt-alert-message">
157
- </div>
158
- </div><div class="yt-alert-buttons"> <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-alert-info yt-uix-button-has-icon edit-confirmation-yes" type="button" onclick=";return false;"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-watch-like-invert yt-sprite"></span></span><span class="yt-uix-button-content">Yeah, keep it</span></button>
159
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-alert-info yt-uix-button-has-icon edit-confirmation-no" type="button" onclick=";return false;"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-watch-unlike-invert yt-sprite"></span></span><span class="yt-uix-button-content">Undo</span></button>
160
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-close close yt-uix-close" type="button" onclick=";return false;" aria-label="Close" data-close-parent-class="yt-alert"><span class="yt-uix-button-content">Close</span></button></div></div>
161
-
162
-
163
-
164
- </div></div><div id="header"></div><div id="player" class=" content-alignment watch-small off-screen-trigger " role="complementary"><div id="theater-background" class="player-height"></div> <div id="player-mole-container">
165
- <div id="player-unavailable" class=" player-width player-height player-unavailable ">
166
- <div class="icon meh"></div>
167
- <div class="content">
168
- <h1 id="unavailable-message" class="message">
169
- Content Warning
170
-
171
-
172
- </h1>
173
- <div id="unavailable-submessage" class="submessage">
174
- <div id="watch7-player-age-gate-content">
175
- <p>This video may be inappropriate for some users.</p>
176
- <p></p>
177
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-primary" type="button" onclick=";window.location.href=this.getAttribute(&#39;href&#39;);return false;" href="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252Fwatch%253Fv%253DnzNgkc6t260%26feature%3Dverify_age_streamlined%26action_handle_signin%3Dtrue&amp;hl=en&amp;shdf=ClgLEgZyZWFzb24aATMMCxIHdmlkZW9JZBoLbnpOZ2tjNnQyNjAMCxIKdmlkZW9UaXRsZRojS2FyYSBUb2ludG9uIChBZ2UtcmVzdHJpY3RlZCB2aWRlbykMEgd5b3V0dWJlGgRTSEExIhSz3OyysAa2Tj-lLeoCjzZL09lDBigBMhSVe8wz43Gvgjdc-OYYy6R-_EyFsQ%3D%3D&amp;service=youtube&amp;ltmpl=verifyage&amp;passive=true" role="link"><span class="yt-uix-button-content">Sign in to confirm your age</span></button>
178
- </div>
179
-
180
-
181
- </div>
182
- </div>
183
-
184
-
185
- </div>
186
-
187
- <div id="player-api" class="player-width player-height off-screen-target player-api" tabIndex="-1"></div>
188
-
189
-
190
- <div id="watch-queue-mole" class="video-mole mole-collapsed hid"><div id="watch-queue" class="watch-playlist player-height"><div class="main-content"><div class="watch-queue-header"><div class="watch-queue-info"><div class="watch-queue-info-icon"><span class="tv-queue-list-icon yt-sprite"></span></div><h3 class="watch-queue-title">Watch Queue</h3><h3 class="tv-queue-title">TV Queue</h3><span class="tv-queue-details"></span></div><div class="watch-queue-control-bar control-bar-button"><div class="watch-queue-mole-info"><div class="watch-queue-control-bar-icon"><span class="watch-queue-icon yt-sprite"></span></div><div class="watch-queue-title-container"><span class="watch-queue-count"></span><span class="watch-queue-title">Watch Queue</span><span class="tv-queue-title">TV Queue</span></div></div> <span class="dark-overflow-action-menu">
191
-
192
-
193
- <button onclick=";return false;" class="flip control-bar-button yt-uix-button yt-uix-button-dark-overflow-action-menu yt-uix-button-size-default yt-uix-button-has-icon no-icon-markup yt-uix-button-empty" type="button" aria-label="Actions for the queue" aria-expanded="false" aria-haspopup="true" ><span class="yt-uix-button-arrow yt-sprite"></span><ul class="watch-queue-menu yt-uix-button-menu yt-uix-button-menu-dark-overflow-action-menu hid" role="menu" aria-haspopup="true"><li role="menuitem"><span onclick=";return false;" class="watch-queue-menu-choice overflow-menu-choice yt-uix-button-menu-item" data-action="remove-all" >Remove all</span></li><li role="menuitem"><span onclick=";return false;" class="watch-queue-menu-choice overflow-menu-choice yt-uix-button-menu-item" data-action="disconnect" >Disconnect</span></li></ul></button>
194
- </span>
195
- <div class="watch-queue-controls">
196
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-empty yt-uix-button-has-icon control-bar-button prev-watch-queue-button yt-uix-button-opacity yt-uix-tooltip yt-uix-tooltip" type="button" onclick=";return false;" title="Previous video"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-watch-queue-prev yt-sprite"></span></span></button>
197
-
198
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-empty yt-uix-button-has-icon control-bar-button play-watch-queue-button yt-uix-button-opacity yt-uix-tooltip yt-uix-tooltip" type="button" onclick=";return false;" title="Play"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-watch-queue-play yt-sprite"></span></span></button>
199
-
200
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-empty yt-uix-button-has-icon control-bar-button pause-watch-queue-button yt-uix-button-opacity yt-uix-tooltip hid yt-uix-tooltip" type="button" onclick=";return false;" title="Pause"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-watch-queue-pause yt-sprite"></span></span></button>
201
-
202
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-empty yt-uix-button-has-icon control-bar-button next-watch-queue-button yt-uix-button-opacity yt-uix-tooltip yt-uix-tooltip" type="button" onclick=";return false;" title="Next video"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-watch-queue-next yt-sprite"></span></span></button>
203
- </div>
204
- </div></div><div class="watch-queue-items-container yt-scrollbar-dark yt-scrollbar"><ol class="watch-queue-items-list playlist-videos-list yt-uix-scroller" data-scroll-action="yt.www.watchqueue.loadThumbnails"> <p class="yt-spinner ">
205
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
206
-
207
- <span class="yt-spinner-message">
208
- Loading...
209
- </span>
210
- </p>
211
- </ol></div></div> <div class="hid">
212
- <div id="watch-queue-title-msg">
213
- Watch Queue
214
- </div>
215
-
216
- <div id="tv-queue-title-msg">Queue</div>
217
-
218
- <div id="watch-queue-count-msg">
219
- __count__/__total__
220
- </div>
221
-
222
- <div id="watch-queue-loading-template">
223
- <!--
224
- <p class="yt-spinner ">
225
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
226
-
227
- <span class="yt-spinner-message">
228
- Loading...
229
- </span>
230
- </p>
231
-
232
- -->
233
- </div>
234
- </div>
235
- </div></div>
236
- <div id="player-playlist" class=" content-alignment watch-player-playlist ">
237
-
238
-
239
- </div>
240
-
241
- </div>
242
-
243
- <div class="clear"></div>
244
- </div><div id="content" class=" content-alignment" role="main"> <div id="placeholder-player" >
245
- <div class="player-api player-width player-height"></div>
246
- </div>
247
-
248
- <div id="watch7-container" class="">
249
- <div id="watch7-main-container">
250
- <div id="watch7-main" class="clearfix">
251
- <div id="watch7-preview" class="player-width player-height hid">
252
- </div>
253
- <div id="watch7-content" class="watch-main-col " itemscope itemid="" itemtype="http://schema.org/VideoObject"
254
- >
255
- <link itemprop="url" href="http://www.youtube.com/watch?v=nzNgkc6t260">
256
- <meta itemprop="name" content="Kara Tointon (Age-restricted video)">
257
- <meta itemprop="description" content="Kara Tointon [EASTENDERS] - Dawn Swann tries Modelling.: https://www.youtube.com/watch?v=keX0Fh_x7J8 Sunday Brunch Kara Tointon Interview: https://www.youtub...">
258
- <meta itemprop="paid" content="False">
259
-
260
- <meta itemprop="channelId" content="UCAX1W4xcZRiDUVjqwBr5x-g">
261
- <meta itemprop="videoId" content="nzNgkc6t260">
262
-
263
- <meta itemprop="duration" content="PT4M27S">
264
- <meta itemprop="unlisted" content="False">
265
-
266
- <span itemprop="author" itemscope itemtype="http://schema.org/Person">
267
- <link itemprop="url" href="http://www.youtube.com/user/MrGreglaw">
268
- </span>
269
- <span itemprop="author" itemscope itemtype="http://schema.org/Person">
270
- <link itemprop="url" href="https://plus.google.com/105238201384119333175">
271
- </span>
272
-
273
- <link itemprop="thumbnailUrl" href="https://i.ytimg.com/vi/nzNgkc6t260/maxresdefault.jpg">
274
- <span itemprop="thumbnail" itemscope itemtype="http://schema.org/ImageObject">
275
- <link itemprop="url" href="https://i.ytimg.com/vi/nzNgkc6t260/maxresdefault.jpg">
276
- <meta itemprop="width" content="1280">
277
- <meta itemprop="height" content="720">
278
- </span>
279
-
280
- <link itemprop="embedURL" href="https://www.youtube.com/embed/nzNgkc6t260">
281
- <meta itemprop="playerType" content="HTML5 Flash">
282
- <meta itemprop="width" content="1280">
283
- <meta itemprop="height" content="720">
284
-
285
- <meta itemprop="isFamilyFriendly" content="False">
286
- <meta itemprop="regionsAllowed" content="AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GS,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TK,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW">
287
- <meta itemprop="interactionCount" content="9040">
288
- <meta itemprop="datePublished" content="2014-12-19">
289
- <meta itemprop="genre" content="Entertainment">
290
-
291
- <div id="watch7-speedyg-area">
292
- <div class="yt-alert yt-alert-actionable yt-alert-info hid " id="speedyg-template"> <div class="yt-alert-icon">
293
- <span class="icon master-sprite yt-sprite"></span>
294
- </div>
295
- <div class="yt-alert-content" role="alert"> <div class="yt-alert-message">
296
- </div>
297
- </div><div class="yt-alert-buttons"><a href="https://www.google.com/get/videoqualityreport/?v=nzNgkc6t260" class="yt-uix-button yt-uix-sessionlink yt-uix-button-alert-info yt-uix-button-size-small" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw" id="speedyg-link" target="_blank"><span class="yt-uix-button-content">Find out why</span></a><button class="yt-uix-button yt-uix-button-size-default yt-uix-button-close close yt-uix-close" type="button" onclick=";return false;" aria-label="Close" data-close-parent-class="yt-alert"><span class="yt-uix-button-content">Close</span></button></div></div>
298
- </div>
299
-
300
- <div id="watch-header" class=" yt-card yt-card-has-padding">
301
- <div id="watch7-headline" class="clearfix">
302
- <div id="watch-headline-title">
303
- <h1 class="yt watch-title-container" >
304
-
305
-
306
- <span id="eow-title" class="watch-title " dir="ltr" title="Kara Tointon (Age-restricted video)">
307
- Kara Tointon (Age-restricted video)
308
- </span>
309
-
310
- </h1>
311
- </div>
312
- </div>
313
-
314
- <div id="watch7-user-header" class=" spf-link "> <a href="/user/MrGreglaw" class="yt-uix-sessionlink yt-user-photo g-hovercard spf-link " data-ytid="UCAX1W4xcZRiDUVjqwBr5x-g" data-sessionlink="itct=CAwQ4TkiEwjq3t_a5LvIAhXaVL4KHRUCDPko-B0">
315
- <span class="video-thumb yt-thumb yt-thumb-48 g-hovercard"
316
- data-ytid="UCAX1W4xcZRiDUVjqwBr5x-g"
317
- >
318
- <span class="yt-thumb-square">
319
- <span class="yt-thumb-clip">
320
- <img src="https://s.ytimg.com/yts/img/pixel-vfl3z5WfW.gif" width="48" data-thumb="https://yt3.ggpht.com/-XfDpwfZgH9g/AAAAAAAAAAI/AAAAAAAAAAA/_M82-OpTwbw/s88-c-k-no/photo.jpg" height="48" alt="Buzz Rock @MrGreglaw" >
321
- <span class="vertical-align"></span>
322
- </span>
323
- </span>
324
- </span>
325
-
326
- </a>
327
- <div class="yt-user-info">
328
- <a href="/channel/UCAX1W4xcZRiDUVjqwBr5x-g" class="yt-uix-sessionlink g-hovercard spf-link " data-ytid="UCAX1W4xcZRiDUVjqwBr5x-g" data-sessionlink="itct=CAwQ4TkiEwjq3t_a5LvIAhXaVL4KHRUCDPko-B0" >Buzz Rock @MrGreglaw</a>
329
- </div>
330
- <span id="watch7-subscription-container"><span class=" yt-uix-button-subscription-container"><button class="yt-uix-button yt-uix-button-size-default yt-uix-button-subscribe-branded yt-uix-button-has-icon no-icon-markup yt-uix-subscription-button yt-can-buffer" type="button" onclick=";return false;" aria-live="polite" aria-busy="false" data-style-type="branded" data-href="https://accounts.google.com/ServiceLogin?continue=http%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26action_handle_signin%3Dtrue%26feature%3Dsubscribe%26next%3D%252Fchannel%252FUCAX1W4xcZRiDUVjqwBr5x-g%26continue_action%3DQUFFLUhqbDRuRDZfV1N1MzViWl9GTHFFT0I0SXZiMEFpd3xBQ3Jtc0ttTk5tQ19ubENRcUNYV0psSGNOS21wQldrM0NTbUtkR2tOa2ZBR3lnZ0wxdGNjcS02ajVnRmN1bDJ3QkdaX29JcG1IM3RpVG9lQW52WjFzM3ZCMEVaMzVqT09OVElhanI3cVNzc3BxbGpINl9JUmVGQnp0NThVdWtuRmdQby1ZMTU1UnY1d0tpdThneFdKaWp6dHJyeFkzZGJSaEZBMlBNaXVzVnd6aUZoWnVndlJ3T2hPaVhpU3h5SHJBcFNGTTFIQVR5YUdZXzlObWRFX2x6Y2otTWR4Q19VOWN3%26app%3Ddesktop&amp;hl=en&amp;service=youtube&amp;passive=true&amp;uilel=3" data-channel-external-id="UCAX1W4xcZRiDUVjqwBr5x-g" data-clicktracking="itct=CA0QmysiEwjq3t_a5LvIAhXaVL4KHRUCDPko-B0yBXdhdGNo"><span class="yt-uix-button-content"><span class="subscribe-label" aria-label="Subscribe">Subscribe</span><span class="subscribed-label" aria-label="Unsubscribe">Subscribed</span><span class="unsubscribe-label" aria-label="Unsubscribe">Unsubscribe</span></span></button><button class="yt-uix-button yt-uix-button-size-default yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon yt-uix-subscription-preferences-button" type="button" onclick=";return false;" aria-role="button" aria-live="polite" aria-label="Subscription preferences" aria-busy="false" data-channel-external-id="UCAX1W4xcZRiDUVjqwBr5x-g"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-subscription-preferences yt-sprite"></span></span></button><span class="yt-subscription-button-subscriber-count-branded-horizontal yt-subscriber-count" title="2,598" aria-label="2,598" tabindex="0">2,598</span><span class="yt-subscription-button-subscriber-count-branded-horizontal yt-short-subscriber-count" title="2K" aria-label="2K" tabindex="0">2K</span>
331
- <div class="yt-uix-overlay " data-overlay-style="primary" data-overlay-shape="tiny">
332
-
333
- <div class="yt-dialog hid ">
334
- <div class="yt-dialog-base">
335
- <span class="yt-dialog-align"></span>
336
- <div class="yt-dialog-fg" role="dialog">
337
- <div class="yt-dialog-fg-content">
338
- <div class="yt-dialog-loading">
339
- <div class="yt-dialog-waiting-content">
340
- <p class="yt-spinner ">
341
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
342
-
343
- <span class="yt-spinner-message">
344
- Loading...
345
- </span>
346
- </p>
347
-
348
- </div>
349
-
350
- </div>
351
- <div class="yt-dialog-content">
352
- <div class="subscription-preferences-overlay-content-container">
353
- <div class="subscription-preferences-overlay-loading ">
354
- <p class="yt-spinner ">
355
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
356
-
357
- <span class="yt-spinner-message">
358
- Loading...
359
- </span>
360
- </p>
361
-
362
- </div>
363
- <div class="subscription-preferences-overlay-content">
364
- </div>
365
- </div>
366
-
367
- </div>
368
- <div class="yt-dialog-working">
369
- <div class="yt-dialog-working-overlay"></div>
370
- <div class="yt-dialog-working-bubble">
371
- <div class="yt-dialog-waiting-content">
372
- <p class="yt-spinner ">
373
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
374
-
375
- <span class="yt-spinner-message">
376
- Working...
377
- </span>
378
- </p>
379
-
380
- </div>
381
- </div>
382
-
383
- </div>
384
- </div>
385
- <div class="yt-dialog-focus-trap" tabindex="0"></div>
386
- </div>
387
- </div>
388
- </div>
389
-
390
-
391
- </div>
392
-
393
- </span></span></div>
394
- <div id="watch8-action-buttons" class="watch-action-buttons clearfix"><div id="watch8-secondary-actions" class="watch-secondary-actions yt-uix-button-group" data-button-toggle-group="optional"> <span class="yt-uix-clickcard">
395
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity yt-uix-button-has-icon no-icon-markup yt-uix-clickcard-target addto-button pause-resume-autoplay yt-uix-tooltip" type="button" onclick=";return false;" title="Add to" data-orientation="vertical" data-position="bottomleft"><span class="yt-uix-button-content">Add to</span></button>
396
- <div class="signin-clickcard yt-uix-clickcard-content">
397
- <h3 class="signin-clickcard-header">Want to watch this again later?</h3>
398
- <div class="signin-clickcard-message">
399
- Sign in to add this video to a playlist.
400
- </div>
401
- <a href="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252Fwatch%253Fv%253DnzNgkc6t260%26feature%3D__FEATURE__%26action_handle_signin%3Dtrue&amp;hl=en&amp;service=youtube&amp;passive=true&amp;uilel=3" class="yt-uix-button signin-button yt-uix-sessionlink yt-uix-button-primary yt-uix-button-size-default" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw"><span class="yt-uix-button-content">Sign in</span></a>
402
- </div>
403
-
404
- </span>
405
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity yt-uix-button-has-icon no-icon-markup pause-resume-autoplay action-panel-trigger action-panel-trigger-share yt-uix-tooltip" type="button" onclick=";return false;" title="Share
406
- " data-trigger-for="action-panel-share" data-button-toggle="true"><span class="yt-uix-button-content">Share
407
- </span></button>
408
- <div class="yt-uix-menu " > <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity yt-uix-button-has-icon no-icon-markup pause-resume-autoplay yt-uix-menu-trigger yt-uix-tooltip" type="button" onclick=";return false;" title="More actions" aria-pressed="false" id="action-panel-overflow-button" role="button" aria-haspopup="true"><span class="yt-uix-button-content">More</span></button>
409
- <div class="yt-uix-menu-content yt-ui-menu-content yt-uix-menu-content-hidden" role="menu"><ul id="action-panel-overflow-menu"> <li>
410
- <span class="yt-uix-clickcard" data-card-class=report-card>
411
- <button type="button" class="yt-ui-menu-item has-icon action-panel-trigger action-panel-trigger-report report-button yt-uix-clickcard-target"
412
- data-orientation="horizontal" data-position="topright">
413
- <span class="yt-ui-menu-item-label">Report</span>
414
- </button>
415
-
416
- <div class="signin-clickcard yt-uix-clickcard-content">
417
- <h3 class="signin-clickcard-header">Need to report the video?</h3>
418
- <div class="signin-clickcard-message">
419
- Sign in to report inappropriate content.
420
- </div>
421
- <a href="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252Fwatch%253Fv%253DnzNgkc6t260%26feature%3D__FEATURE__%26action_handle_signin%3Dtrue&amp;hl=en&amp;service=youtube&amp;passive=true&amp;uilel=3" class="yt-uix-button signin-button yt-uix-sessionlink yt-uix-button-primary yt-uix-button-size-default" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw"><span class="yt-uix-button-content">Sign in</span></a>
422
- </div>
423
-
424
- </span>
425
- </li>
426
- <li>
427
- <button type="button" class="yt-ui-menu-item has-icon yt-uix-menu-close-on-select action-panel-trigger action-panel-trigger-stats"
428
- data-trigger-for="action-panel-stats">
429
- <span class="yt-ui-menu-item-label">Statistics</span>
430
- </button>
431
-
432
- </li>
433
- </ul></div></div></div><div id="watch8-sentiment-actions"><div id="watch7-views-info"><div class="watch-view-count">9,040</div>
434
- <div class="video-extras-sparkbars">
435
- <div class="video-extras-sparkbar-likes" style="width: 37.037037037%"></div>
436
- <div class="video-extras-sparkbar-dislikes" style="width: 62.962962963%"></div>
437
- </div>
438
- </div>
439
-
440
-
441
-
442
-
443
-
444
- <span class="like-button-renderer " data-button-toggle-group="optional" >
445
- <span class="yt-uix-clickcard">
446
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity yt-uix-button-has-icon no-icon-markup like-button-renderer-like-button like-button-renderer-like-button-unclicked yt-uix-clickcard-target yt-uix-tooltip" type="button" onclick=";return false;" title="I like this" aria-label="like this video along with 10 other people" data-orientation="vertical" data-force-position="true" data-position="bottomright"><span class="yt-uix-button-content">10</span></button>
447
- <div class="signin-clickcard yt-uix-clickcard-content">
448
- <h3 class="signin-clickcard-header">Like this video?</h3>
449
- <div class="signin-clickcard-message">
450
- Sign in to make your opinion count.
451
- </div>
452
- <a href="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252Fwatch%253Fv%253DnzNgkc6t260%26feature%3D__FEATURE__%26action_handle_signin%3Dtrue&amp;hl=en&amp;service=youtube&amp;passive=true&amp;uilel=3" class="yt-uix-button signin-button yt-uix-sessionlink yt-uix-button-primary yt-uix-button-size-default" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw"><span class="yt-uix-button-content">Sign in</span></a>
453
- </div>
454
-
455
- </span>
456
- <span class="yt-uix-clickcard">
457
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity yt-uix-button-has-icon no-icon-markup like-button-renderer-like-button like-button-renderer-like-button-clicked yt-uix-button-toggled hid yt-uix-tooltip" type="button" onclick=";return false;" title="Unlike" aria-label="like this video along with 10 other people" data-orientation="vertical" data-force-position="true" data-position="bottomright"><span class="yt-uix-button-content">11</span></button>
458
- </span>
459
- <span class="yt-uix-clickcard">
460
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity yt-uix-button-has-icon no-icon-markup like-button-renderer-dislike-button like-button-renderer-dislike-button-unclicked yt-uix-clickcard-target yt-uix-tooltip" type="button" onclick=";return false;" title="I dislike this" aria-label="dislike this video along with 17 other people" data-orientation="vertical" data-force-position="true" data-position="bottomright"><span class="yt-uix-button-content">17</span></button>
461
- <div class="signin-clickcard yt-uix-clickcard-content">
462
- <h3 class="signin-clickcard-header">Don't like this video?</h3>
463
- <div class="signin-clickcard-message">
464
- Sign in to make your opinion count.
465
- </div>
466
- <a href="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252Fwatch%253Fv%253DnzNgkc6t260%26feature%3D__FEATURE__%26action_handle_signin%3Dtrue&amp;hl=en&amp;service=youtube&amp;passive=true&amp;uilel=3" class="yt-uix-button signin-button yt-uix-sessionlink yt-uix-button-primary yt-uix-button-size-default" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw"><span class="yt-uix-button-content">Sign in</span></a>
467
- </div>
468
-
469
- </span>
470
- <span class="yt-uix-clickcard">
471
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity yt-uix-button-has-icon no-icon-markup like-button-renderer-dislike-button like-button-renderer-dislike-button-clicked yt-uix-button-toggled hid yt-uix-tooltip" type="button" onclick=";return false;" title="I dislike this" aria-label="dislike this video along with 17 other people" data-orientation="vertical" data-force-position="true" data-position="bottomright"><span class="yt-uix-button-content">18</span></button>
472
- </span>
473
- </span>
474
- </div></div>
475
- </div>
476
-
477
-
478
-
479
- <div id="watch-action-panels" class="watch-action-panels yt-uix-button-panel hid yt-card yt-card-has-padding">
480
- <div id="action-panel-share" class="action-panel-content hid">
481
- <div id="watch-actions-share-loading">
482
- <div class="action-panel-loading">
483
- <p class="yt-spinner ">
484
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
485
-
486
- <span class="yt-spinner-message">
487
- Loading...
488
- </span>
489
- </p>
490
-
491
- </div>
492
- </div>
493
- <div id="watch-actions-share-panel"></div>
494
-
495
- </div>
496
-
497
- <div id="action-panel-stats" class="action-panel-content hid">
498
- <div class="action-panel-loading">
499
- <p class="yt-spinner ">
500
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
501
-
502
- <span class="yt-spinner-message">
503
- Loading...
504
- </span>
505
- </p>
506
-
507
- </div>
508
- </div>
509
-
510
- <div id="action-panel-report" class="action-panel-content hid" data-auth-required="true" tabIndex="0">
511
- <div class="action-panel-loading">
512
- <p class="yt-spinner ">
513
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
514
-
515
- <span class="yt-spinner-message">
516
- Loading...
517
- </span>
518
- </p>
519
-
520
- </div>
521
- </div>
522
-
523
-
524
- <div id="action-panel-rental-required" class="action-panel-content hid">
525
- <div id="watch-actions-rental-required">
526
- <strong>Rating is available when the video has been rented.</strong>
527
- </div>
528
-
529
- </div>
530
-
531
- <div id="action-panel-error" class="action-panel-content hid">
532
- <div class="action-panel-error">
533
- This feature is not available right now. Please try again later.
534
- </div>
535
- </div>
536
-
537
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon no-icon-markup yt-uix-button-opacity yt-uix-close" type="button" onclick=";return false;" aria-label="Close" id="action-panel-dismiss" data-close-parent-id="watch8-action-panels"></button>
538
- </div>
539
-
540
- <div id="action-panel-details" class="action-panel-content yt-uix-expander yt-uix-expander-collapsed yt-card yt-card-has-padding"><div id="watch-description" class="yt-uix-button-panel"><div id="watch-description-content"><div id="watch-description-clip"><div id="watch-uploader-info"><strong class="watch-time-text">Published on Dec 19, 2014</strong></div><div id="watch-description-text" class=""><p id="eow-description" >Kara Tointon [EASTENDERS] - Dawn Swann tries Modelling.: <a href="https://www.youtube.com/watch?v=keX0Fh_x7J8" target="_blank" title="https://www.youtube.com/watch?v=keX0Fh_x7J8" rel="nofollow" dir="ltr" class="yt-uix-redirect-link">https://www.youtube.com/watch?v=keX0F...</a><br /><br />Sunday Brunch Kara Tointon Interview: <a href="https://www.youtube.com/watch?v=raX720xeBMo" target="_blank" title="https://www.youtube.com/watch?v=raX720xeBMo" rel="nofollow" dir="ltr" class="yt-uix-redirect-link">https://www.youtube.com/watch?v=raX72...</a><br /><br />Kara and Artem Strictly Champions 2010 - Winning Moment: <a href="https://www.youtube.com/watch?v=63fQaC9jE6s" target="_blank" title="https://www.youtube.com/watch?v=63fQaC9jE6s" rel="nofollow" dir="ltr" class="yt-uix-redirect-link">https://www.youtube.com/watch?v=63fQa...</a><br /><br />Playlist of her: <a href="https://www.youtube.com/playlist?list=PL0E044246B98CD281" target="_blank" title="https://www.youtube.com/playlist?list=PL0E044246B98CD281" rel="nofollow" dir="ltr" class="yt-uix-redirect-link">https://www.youtube.com/playlist?list...</a><br /><br />Her sister Hannah Tointon (Age-restricted video): <a href="https://www.youtube.com/watch?v=rmy8rmfGJzg" target="_blank" title="https://www.youtube.com/watch?v=rmy8rmfGJzg" rel="nofollow" dir="ltr" class="yt-uix-redirect-link">https://www.youtube.com/watch?v=rmy8r...</a><br /><br />Soap Opera Hot Females playlist: <a href="https://www.youtube.com/playlist?list=PL409EB2667F6A30C6" target="_blank" title="https://www.youtube.com/playlist?list=PL409EB2667F6A30C6" rel="nofollow" dir="ltr" class="yt-uix-redirect-link">https://www.youtube.com/playlist?list...</a><br /><br />My Youtube Editer Uploads playlist: <a href="https://www.youtube.com/playlist?list=PLqvXvT1NkiVTpyHWzlNDfiV3XAMo3lUXg" target="_blank" title="https://www.youtube.com/playlist?list=PLqvXvT1NkiVTpyHWzlNDfiV3XAMo3lUXg" rel="nofollow" dir="ltr" class="yt-uix-redirect-link">https://www.youtube.com/playlist?list...</a><br /><br />Music by: Silent Partner - Don&#39;t Look<br /><br />Kara Louise Tointon (born 5 August 1983) is an English actress, best known for playing Dawn Swann in BBC soap opera EastEnders. Tointon was the 2010 winner of BBC television series Strictly Come Dancing.<br /><br />Early Life<br /><br />Tointon was born to Ken and Carol in Basildon, Essex. Together with her actress sister, Hannah (born 1987), Tointon was brought up in Leigh-on-Sea. Both sisters attended St Michael&#39;s School, Leigh, and St. Hilda&#39;s School, Westcliff-on-Sea, Essex. Tointon was diagnosed with dyslexia at age seven. She had speech and drama lessons at school.</p></div> <div id="watch-description-extras" >
541
- <ul class="watch-extras-section">
542
- <li class="watch-meta-item ">
543
- <h4 class="title">
544
- Notice
545
- </h4>
546
- <ul class="content watch-info-tag-list">
547
- <li><a href="//support.google.com/youtube/?p=age_restrictions&amp;hl=en" class="yt-uix-sessionlink " data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw" >Age-restricted video (requested by uploader).</a></li>
548
- </ul>
549
- </li>
550
-
551
- <li class="watch-meta-item yt-uix-expander-body">
552
- <h4 class="title">
553
- Category
554
- </h4>
555
- <ul class="content watch-info-tag-list">
556
- <li><a href="/channel/UCi-g4cjqGV7jvU8aeSuj0jQ" class="yt-uix-sessionlink g-hovercard spf-link " data-ytid="UCi-g4cjqGV7jvU8aeSuj0jQ" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw" >Entertainment</a></li>
557
- </ul>
558
- </li>
559
-
560
- <li class="watch-meta-item yt-uix-expander-body">
561
- <h4 class="title">
562
- License
563
- </h4>
564
- <ul class="content watch-info-tag-list">
565
- <li>Standard YouTube License</li>
566
- </ul>
567
- </li>
568
-
569
- <li class="watch-meta-item yt-uix-expander-body">
570
- <h4 class="title">
571
- Created using
572
- </h4>
573
- <ul class="content watch-info-tag-list">
574
- <li><a href="http://www.youtube.com/editor" class="yt-uix-sessionlink " data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw" >YouTube Video Editor</a></li>
575
- </ul>
576
- </li>
577
-
578
- </ul>
579
- </div>
580
- </div></div></div> <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-expander yt-uix-expander-head yt-uix-expander-collapsed-body yt-uix-gen204" type="button" onclick=";return false;" data-gen204="feature=watch-show-more-metadata"><span class="yt-uix-button-content">Show more</span></button>
581
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-expander yt-uix-expander-head yt-uix-expander-body" type="button" onclick=";return false;"><span class="yt-uix-button-content">Show less</span></button>
582
- </div>
583
-
584
- <div class="cmt_iframe_holder" data-href="http://www.youtube.com/watch?v=nzNgkc6t260" data-viewtype="FILTERED" style="display: none;"></div>
585
-
586
- <div id="watch-discussion" class="branded-page-box yt-card">
587
-
588
-
589
- <div class="comments-iframe-container">
590
- <div id="comments-test-iframe"></div>
591
- <div id="distiller-spinner" class="action-panel-loading">
592
- <p class="yt-spinner ">
593
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
594
-
595
- <span class="yt-spinner-message">
596
- Loading...
597
- </span>
598
- </p>
599
-
600
- </div>
601
- </div>
602
-
603
-
604
- </div>
605
-
606
-
607
- </div>
608
- <div id="watch-sidebar-spacer"></div>
609
- <div id="watch7-sidebar" class="watch-sidebar">
610
- <div id="placeholder-playlist" class="watch-playlist player-height hid"></div>
611
-
612
-
613
- <div id="watch7-sidebar-contents" class="watch-sidebar-gutter yt-card yt-card-has-padding yt-uix-expander yt-uix-expander-collapsed">
614
-
615
- <div id="watch7-sidebar-ads">
616
-
617
- </div>
618
- <div id="watch7-sidebar-modules">
619
- </div>
620
- </div>
621
-
622
- </div>
623
- </div>
624
- </div>
625
-
626
- <div id="watch7-hidden-extras">
627
- <div style="visibility: hidden; height: 0px; padding: 0px; overflow: hidden;">
628
- </div>
629
-
630
- </div>
631
-
632
-
633
- </div>
634
-
635
- </div></div></div></div> <div id="footer-container" class="yt-base-gutter force-layer"><div id="footer"><div id="footer-main"><div id="footer-logo"><a href="/" id="footer-logo-link" title="YouTube home" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw&amp;ved=CAIQpmE" class="yt-uix-sessionlink"><span class="footer-logo-icon yt-sprite"></span></a></div> <ul class="pickers yt-uix-button-group" data-button-toggle-group="optional">
636
- <li>
637
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-default yt-uix-button-has-icon" type="button" onclick=";return false;" id="yt-picker-language-button" data-button-action="yt.www.picker.load" data-picker-position="footer" data-picker-key="language" data-button-menu-id="arrow-display" data-button-toggle="true"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-footer-language yt-sprite"></span></span><span class="yt-uix-button-content"> <span class="yt-picker-button-label">
638
- Language:
639
- </span>
640
- English
641
- </span><span class="yt-uix-button-arrow yt-sprite"></span></button>
642
-
643
-
644
- </li>
645
- <li>
646
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-default" type="button" onclick=";return false;" id="yt-picker-country-button" data-button-action="yt.www.picker.load" data-picker-position="footer" data-picker-key="country" data-button-menu-id="arrow-display" data-button-toggle="true"><span class="yt-uix-button-content"> <span class="yt-picker-button-label">
647
- Country:
648
- </span>
649
- Worldwide
650
- </span><span class="yt-uix-button-arrow yt-sprite"></span></button>
651
-
652
-
653
- </li>
654
- <li>
655
- <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-default" type="button" onclick=";return false;" id="yt-picker-safetymode-button" data-button-action="yt.www.picker.load" data-picker-position="footer" data-picker-key="safetymode" data-button-menu-id="arrow-display" data-button-toggle="true"><span class="yt-uix-button-content"> <span class="yt-picker-button-label">
656
- Restricted Mode:
657
- </span>
658
- Off
659
- </span><span class="yt-uix-button-arrow yt-sprite"></span></button>
660
-
661
-
662
- </li>
663
- </ul>
664
- <a href="/feed/history" class="yt-uix-button footer-history yt-uix-sessionlink yt-uix-button-default yt-uix-button-size-default yt-uix-button-has-icon" data-sessionlink="ei=ig0bVurxA9qp-QWVhLDIDw"><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-footer-history yt-sprite"></span></span><span class="yt-uix-button-content">History</span></a> <button class="yt-uix-button yt-uix-button-size-default yt-uix-button-default yt-uix-button-has-icon yt-uix-button-reverse yt-google-help-link inq-no-click " type="button" onclick=";return false;" data-ghelp-anchor="google-help" data-ghelp-tracking-param="" id="google-help" data-load-chat-support=""><span class="yt-uix-button-icon-wrapper"><span class="yt-uix-button-icon yt-uix-button-icon-questionmark yt-sprite"></span></span><span class="yt-uix-button-content">Help
665
- </span></button>
666
- <div id="yt-picker-language-footer" class="yt-picker" style="display: none">
667
- <p class="yt-spinner ">
668
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
669
-
670
- <span class="yt-spinner-message">
671
- Loading...
672
- </span>
673
- </p>
674
-
675
- </div>
676
-
677
- <div id="yt-picker-country-footer" class="yt-picker" style="display: none">
678
- <p class="yt-spinner ">
679
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
680
-
681
- <span class="yt-spinner-message">
682
- Loading...
683
- </span>
684
- </p>
685
-
686
- </div>
687
-
688
- <div id="yt-picker-safetymode-footer" class="yt-picker" style="display: none">
689
- <p class="yt-spinner ">
690
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
691
-
692
- <span class="yt-spinner-message">
693
- Loading...
694
- </span>
695
- </p>
696
-
697
- </div>
698
-
699
- </div><div id="footer-links"><ul id="footer-links-primary"> <li><a href="//www.youtube.com/yt/about/">About</a></li>
700
- <li><a href="//www.youtube.com/yt/press/">
701
- Press
702
- </a></li>
703
- <li><a href="//www.youtube.com/yt/copyright/">Copyright</a></li>
704
- <li><a href="//www.youtube.com/yt/creators/">
705
- Creators
706
- </a></li>
707
- <li><a href="//www.youtube.com/yt/advertise/">
708
- Advertise
709
- </a></li>
710
- <li><a href="//www.youtube.com/yt/dev/">Developers</a></li>
711
- <li><a href="https://plus.google.com/+youtube" dir="ltr">+YouTube</a></li>
712
- </ul><ul id="footer-links-secondary"> <li><a href="/t/terms">Terms</a></li>
713
- <li><a href="https://www.google.com/intl/en/policies/privacy/">Privacy</a></li>
714
- <li><a href="//www.youtube.com/yt/policyandsafety/">
715
- Policy &amp; Safety
716
- </a></li>
717
- <li><a href="//support.google.com/youtube/?hl=en" onclick="return yt.www.feedback.start(59);" class="reportbug">Send feedback</a></li>
718
- <li><a href="/testtube">Try something new!</a></li>
719
- <li></li>
720
- </ul></div></div></div>
721
-
722
-
723
- <div class="yt-dialog hid " id="feed-privacy-lb">
724
- <div class="yt-dialog-base">
725
- <span class="yt-dialog-align"></span>
726
- <div class="yt-dialog-fg" role="dialog">
727
- <div class="yt-dialog-fg-content">
728
- <div class="yt-dialog-loading">
729
- <div class="yt-dialog-waiting-content">
730
- <p class="yt-spinner ">
731
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
732
-
733
- <span class="yt-spinner-message">
734
- Loading...
735
- </span>
736
- </p>
737
-
738
- </div>
739
-
740
- </div>
741
- <div class="yt-dialog-content">
742
- <div id="feed-privacy-dialog">
743
- </div>
744
-
745
- </div>
746
- <div class="yt-dialog-working">
747
- <div class="yt-dialog-working-overlay"></div>
748
- <div class="yt-dialog-working-bubble">
749
- <div class="yt-dialog-waiting-content">
750
- <p class="yt-spinner ">
751
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
752
-
753
- <span class="yt-spinner-message">
754
- Working...
755
- </span>
756
- </p>
757
-
758
- </div>
759
- </div>
760
-
761
- </div>
762
- </div>
763
- <div class="yt-dialog-focus-trap" tabindex="0"></div>
764
- </div>
765
- </div>
766
- </div>
767
-
768
-
769
- <div id="hidden-component-template-wrapper" class="hid"> <div id="shared-addto-watch-later-login" class="hid">
770
- <a href="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252Fwatch%253Fv%253DnzNgkc6t260%26feature%3Dplaylist%26action_handle_signin%3Dtrue&amp;hl=en&amp;service=youtube&amp;passive=true&amp;uilel=3" class="sign-in-link">Sign in</a> to add this to Watch Later
771
-
772
- </div>
773
- <div id="yt-uix-videoactionmenu-menu" class="yt-ui-menu-content">
774
- <div class="hide-on-create-pl-panel">
775
- <h3>
776
- Add to
777
- </h3>
778
- </div>
779
- <div class="add-to-widget">
780
- <p class="yt-spinner ">
781
- <span title="Loading icon" class="yt-spinner-img yt-sprite"></span>
782
-
783
- <span class="yt-spinner-message">
784
- Loading playlists...
785
- </span>
786
- </p>
787
-
788
- </div>
789
-
790
- </div>
791
- </div> <script>var ytspf = ytspf || {};ytspf.enabled = true;ytspf.config = {'reload-identifier': 'spfreload'};ytspf.config['cache-max'] = 50;ytspf.config['navigate-limit'] = 50;ytspf.config['navigate-lifetime'] = 64800000;</script>
792
- <script src="//s.ytimg.com/yts/jsbin/spf-vflXxZ96-/spf.js" type="text/javascript" name="spf/spf"></script>
793
- <script src="//s.ytimg.com/yts/jsbin/www-en_US-vflJjxTUB/base.js" name="www/base"></script>
794
- <script>spf.script.path({'www/': '//s.ytimg.com/yts/jsbin/www-en_US-vflJjxTUB/'});var ytdepmap = {"www/base": null, "www/common": "www/base", "www/angular_base": "www/common", "www/channels_accountupload": "www/common", "www/channels": "www/common", "www/dashboard": "www/common", "www/debugpolymer": "www/common", "www/downloadreports": "www/common", "www/experiments": "www/common", "www/feed": "www/common", "www/innertube": "www/common", "www/instant": "www/common", "www/legomap": "www/common", "www/live_chat": "www/common", "www/live_chat_moderation": "www/common", "www/promo_join_network": "www/common", "www/results_harlemshake": "www/common", "www/results": "www/common", "www/results_star_trek": "www/common", "www/results_starwars": "www/common", "www/subscriptionmanager": "www/common", "www/unlimited": "www/common", "www/watch": "www/common", "www/ypc_bootstrap": "www/common", "www/ypc_core": "www/common", "www/ytstyles": "www/common", "www/channels_edit": "www/channels", "www/innertube_watchnext": "www/innertube", "www/live_broadcasts": "www/angular_base", "www/live_dashboard": "www/angular_base", "www/videomanager": "www/angular_base", "www/watch_autoplayrenderer": "www/watch", "www/watch_edit": "www/watch", "www/watch_editor": "www/watch", "www/watch_live": "www/watch", "www/watch_missilecommand": "www/watch", "www/watch_promos": "www/watch", "www/watch_speedyg": "www/watch", "www/watch_transcript": "www/watch", "www/watch_videoshelf": "www/watch", "www/ct_advancedsearch": "www/videomanager", "www/my_videos": "www/videomanager", "www/vm_coverrevshare": "www/videomanager"};spf.script.declare(ytdepmap);</script><script>if (window.ytcsi) {window.ytcsi.tick("je", null, '');}</script> <script>
795
- yt.setConfig({
796
- 'VIDEO_ID': "nzNgkc6t260",
797
- 'THUMB_LOADER_PAUSE_MS': 0,
798
- 'THUMB_LOADER_GROUP_PX': 400,
799
- 'THUMB_LOADER_IGNORE_FOLD': false,
800
- 'WAIT_TO_DELAYLOAD_FRAME_CSS': true,
801
- 'IS_UNAVAILABLE_PAGE': false,
802
- 'DROPDOWN_ARROW_URL': "https:\/\/s.ytimg.com\/yts\/img\/pixel-vfl3z5WfW.gif",
803
- 'AUTONAV_EXTRA_CHECK': false,
804
-
805
- 'JS_PAGE_MODULES': [
806
- 'www/watch',
807
- 'www/ypc_bootstrap',
808
- 'www/watch_speedyg',
809
- 'www/watch_autoplayrenderer',
810
- '' ],
811
-
812
-
813
- 'REPORTVIDEO_JS': "\/\/s.ytimg.com\/yts\/jsbin\/www-reportvideo-vflpvhzkD\/www-reportvideo.js",
814
- 'REPORTVIDEO_CSS': "\/\/s.ytimg.com\/yts\/cssbin\/www-watch-reportvideo-vflkl6hpI.css",
815
-
816
-
817
- 'TIMING_AFT_KEYS': ['pbr', 'pbs'],
818
- 'YPC_CAN_RATE_VIDEO': true,
819
-
820
-
821
- 'RELATED_PLAYER_ARGS': {"rvs":""},
822
-
823
-
824
-
825
-
826
-
827
-
828
-
829
- 'PYV_DISABLE_MUTE': true,
830
-
831
-
832
-
833
-
834
-
835
- 'HL_LOCALE': "en_US",
836
- 'TTS_URL': "",
837
- 'JS_DELAY_LOAD': 0,
838
- 'LIST_AUTO_PLAY_VALUE': 1,
839
- 'SHUFFLE_VALUE': 0,
840
- 'SKIP_RELATED_ADS': false,
841
- 'SKIP_TO_NEXT_VIDEO': false,
842
- 'RESUME_COOKIE_NAME': null,
843
- 'CONVERSION_CONFIG_DICT': {},
844
- 'RESOLUTION_TRACKING_ENABLED': false,
845
- 'MEMORY_TRACKING_ENABLED': false,
846
- 'NAVIGATION_TRACKING_ENABLED': false,
847
- 'WATCH_LEGAL_TEXT_ENABLE_AUTOSCROLL': false,
848
- 'SHARE_ON_VIDEO_END': true,
849
- 'SHARE_ON_VIDEO_START': false,
850
- 'ADS_DATA': {"check_status":false},
851
- 'PLAYBACK_ID': null,
852
- 'IS_DISTILLER': true,
853
- 'SHARE_CAPTION': null,
854
- 'SHARE_REFERER': "",
855
- 'PLAYLIST_INDEX': null
856
- });
857
-
858
- yt.setMsg({
859
- 'EDITOR_AJAX_REQUEST_FAILED': "Something went wrong trying to get data from the server. Try again, or reload the page.",
860
- 'EDITOR_AJAX_REQUEST_503': "This functionality is not available right now. Please try again later.",
861
- 'LOADING': "Loading..." });
862
-
863
- yt.setMsg('SPEEDYG_INFO', "Experiencing interruptions?");
864
-
865
- yt.setMsg({
866
- 'UNBLOCK_USER': "Are you sure you want to unblock this user?",
867
- 'BLOCK_USER': "Are you sure you want to block this user?"
868
- });
869
- yt.setConfig('BLOCK_USER_AJAX_XSRF', 'QUFFLUhqbUVRNm9RcEJoTjBLRmduZE9RRi1fX25pdE4yQXxBQ3Jtc0tub1ZOMUJueXhodlJjVFdrMkU5aEdGa2JjVC1SaTVUZjBEWXRNUWxSdkdoV2ZTcXNCVmt3MnFTclljTExLNU02RmdnaU1Cd0szNlZ2MGlMSHNLUTB0YWVjWjdVWTFSczA1UnNJX3FQYW1GajRyc3J2T2Jpb1VFR28yZ0o0dzZCYVBVWkNuLTlaa0dmSFZKQ3BFaGoyMXpYOTgyUGc=');
870
-
871
-
872
-
873
-
874
-
875
-
876
-
877
-
878
-
879
-
880
-
881
-
882
- yt.setConfig({
883
- 'GUIDED_HELP_LOCALE': "en_US",
884
- 'GUIDED_HELP_ENVIRONMENT': "prod"
885
- });
886
-
887
-
888
- </script>
889
-
890
-
891
- <script>yt.setConfig({APIARY_HOST: "",GAPI_HINT_PARAMS: "m;\/_\/scs\/abc-static\/_\/js\/k=gapi.gapi.en.od-BQUk2OLg.O\/m=__features__\/am=AAI\/rt=j\/d=1\/rs=AItRSTPBQlhop3BvMITyen1x2FEmN3Mcfw",INNERTUBE_CONTEXT_CLIENT_VERSION: "1.20151006",INNERTUBE_API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",INNERTUBE_API_VERSION: "v1",APIARY_HOST_FIRSTPARTY: "",'VISITOR_DATA': "CgtRNmo3NThETllyTQ%3D%3D",'GAPI_HOST': "https:\/\/apis.google.com",'GAPI_LOCALE': "en_US",'INNERTUBE_CONTEXT_HL': "en",'INNERTUBE_CONTEXT_GL': "US"});yt.setConfig({"MENDEL_FLAG_GAME_METADATA_ON_WATCH":false,"MENDEL_FLAG_PLAYER_SWFCFG_CLEANUP":true});yt.setConfig({'EVENT_ID': "ig0bVurxA9qp-QWVhLDIDw",'PAGE_NAME': "watch",'LOGGED_IN': false,'SESSION_INDEX': null,'PARENT_TRACKING_PARAMS': "",'FORMATS_FILE_SIZE_JS': ["%s B","%s KB","%s MB","%s GB","%s TB"],'DELEGATED_SESSION_ID': null,'ONE_PICK_URL': "",'UNIVERSAL_HOVERCARDS': true,'GOOGLEPLUS_HOST': "https:\/\/plus.google.com",'PAGEFRAME_JS': "\/\/s.ytimg.com\/yts\/jsbin\/www-pageframe-vflnlTmDC\/www-pageframe.js",'JS_COMMON_MODULE': "\/\/s.ytimg.com\/yts\/jsbin\/www-en_US-vflJjxTUB\/common.js",'PAGE_FRAME_DELAYLOADED_CSS': "\/\/s.ytimg.com\/yts\/cssbin\/www-pageframedelayloaded-vflvaIXFr.css",'GUIDE_DELAY_LOAD': true,'GUIDE_DELAYLOADED_CSS': "\/\/s.ytimg.com\/yts\/cssbin\/www-guide-vflU12X3p.css",'HIGH_CONTRAST_MODE_CSS': "\/\/s.ytimg.com\/yts\/cssbin\/www-highcontrastmode-vfl1NXB8s.css",'PREFETCH_CSS_RESOURCES' : ["\/\/s.ytimg.com\/yts\/cssbin\/www-player-new-vfl1mGUtZ.css"],'PREFETCH_JS_RESOURCES': ["\/\/s.ytimg.com\/yts\/jsbin\/html5player-new-en_US-vflIUNjzZ\/html5player-new.js",'' ],'PREFETCH_LINKS': false,'PREFETCH_LINKS_MAX': 1,'PREFETCH_AUTOPLAY': false,'PREFETCH_AUTOPLAY_TIME': 0,'PREFETCH_AUTONAV': false,'PREBUFFER_MAX': 1,'PREBUFFER_LINKS': false,'PREBUFFER_AUTOPLAY': false,'PREBUFFER_AUTONAV': false,'WATCH_LATER_BUTTON': "\n\n \u003cbutton class=\"yt-uix-button yt-uix-button-size-small yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon no-icon-markup addto-button video-actions spf-nolink hide-until-delayloaded addto-watch-later-button-sign-in yt-uix-tooltip\" type=\"button\" onclick=\";return false;\" title=\"Watch Later\" role=\"button\" data-video-ids=\"__VIDEO_ID__\" data-button-menu-id=\"shared-addto-watch-later-login\"\u003e\u003cspan class=\"yt-uix-button-arrow yt-sprite\"\u003e\u003c\/span\u003e\u003c\/button\u003e\n",'WATCH_QUEUE_BUTTON': " \u003cbutton class=\"yt-uix-button yt-uix-button-size-small yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon no-icon-markup addto-button addto-queue-button video-actions spf-nolink hide-until-delayloaded addto-tv-queue-button yt-uix-tooltip\" type=\"button\" onclick=\";return false;\" title=\"Queue\" data-video-ids=\"__VIDEO_ID__\" data-style=\"tv-queue\"\u003e\u003c\/button\u003e\n",'WATCH_QUEUE_MENU': " \u003cspan class=\"thumb-menu dark-overflow-action-menu video-actions\"\u003e\n \u003cbutton onclick=\";return false;\" type=\"button\" class=\"yt-uix-button-reverse flip addto-watch-queue-menu spf-nolink hide-until-delayloaded yt-uix-button yt-uix-button-dark-overflow-action-menu yt-uix-button-size-default yt-uix-button-has-icon no-icon-markup yt-uix-button-empty\" aria-expanded=\"false\" aria-haspopup=\"true\" \u003e\u003cspan class=\"yt-uix-button-arrow yt-sprite\"\u003e\u003c\/span\u003e\u003cul class=\"watch-queue-thumb-menu yt-uix-button-menu yt-uix-button-menu-dark-overflow-action-menu hid\"\u003e\u003cli role=\"menuitem\" class=\"overflow-menu-choice addto-watch-queue-menu-choice addto-watch-queue-play-next yt-uix-button-menu-item\" data-action=\"play-next\" onclick=\";return false;\" data-video-ids=\"__VIDEO_ID__\"\u003e\u003cspan class=\"addto-watch-queue-menu-text\"\u003ePlay next\u003c\/span\u003e\u003c\/li\u003e\u003cli role=\"menuitem\" class=\"overflow-menu-choice addto-watch-queue-menu-choice addto-watch-queue-play-now yt-uix-button-menu-item\" data-action=\"play-now\" onclick=\";return false;\" data-video-ids=\"__VIDEO_ID__\"\u003e\u003cspan class=\"addto-watch-queue-menu-text\"\u003ePlay now\u003c\/span\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/button\u003e\n \u003c\/span\u003e\n",'SAFETY_MODE_PENDING': false,'LOCAL_DATE_TIME_CONFIG': {"formatShortTime":"h:mm a","shortWeekdays":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"shortMonths":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"formatLongDate":"MMMM d, yyyy h:mm a","weekendRange":[6,5],"formatShortDate":"MMM d, yyyy","firstDayOfWeek":0,"amPms":["AM","PM"],"firstWeekCutoffDay":3,"weekdays":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"months":["January","February","March","April","May","June","July","August","September","October","November","December"],"dateFormats":["MMMM d, yyyy h:mm a","MMMM d, yyyy","MMM d, yyyy","MMM d, yyyy"],"formatWeekdayShortTime":"EE h:mm a","formatLongDateOnly":"MMMM d, yyyy"},'PAGE_CL': 104915008,'PAGE_BUILD_LABEL': "youtube_20151006_RC5",'VARIANTS_CHECKSUM': "9caf74b6696958bed49b9f3270582578",'CLIENT_PROTOCOL': "HTTP\/1.1",'CLIENT_TRANSPORT': "tcp",'MDX_ENABLE_CASTV2': true,'MDX_ENABLE_QUEUE': true,'FEEDBACK_BUCKET_ID': "Watch",'FEEDBACK_LOCALE_LANGUAGE': "en",'FEEDBACK_LOCALE_EXTRAS': {"is_branded":"","is_partner":"","accept_language":null,"guide_subs":"NA","logged_in":false,"experiments":"9407016,9407448,9407484,9408066,9408214,9408710,9408721,9408814,9409069,9409142,9409205,9412845,9412901,9412991,9413142,9413432,9414667,9414764,9415018,9415049,9415117,9415364,9415575,9415802,9415890,9415938,9415995,9416038,9416051,9416089,9416091,9416093,9416126,9416239,9416427,9416475,9416527,9416715,9416717,9416837,9416851,9416863,9416864,9417035,9417513,9417664,9417704,9417707,9417755,9417796,9417946,9418124,9418141,9418159,9418212,9418228,9418394,9418414,9418441,9418443,9418448,9418495,9418543,9418546,9418675,9419359,9419437,9419525,9419785,9419793,9419979,9420060,9420098,9420189,9420195,9420289,9420335,9420348,9420405,9420407,9420444,9420535,9420632,9420634,9420790,9420827,9420950,9420951,9421000,9421013,9421036,9421139,9421162,9421226,9421265,9421318,9421332,9421372,9421381,9421382,9421392,9421403,9421708,9421847,9422251,9422376,9422438"}}); yt.setConfig({
892
- 'GUIDED_HELP_LOCALE': "en_US",
893
- 'GUIDED_HELP_ENVIRONMENT': "prod"
894
- });
895
- yt.setConfig('SPF_SEARCH_BOX', true);yt.setMsg({'ADDTO_CREATE_NEW_PLAYLIST': "Create new playlist\n",'ADDTO_CREATE_PLAYLIST_DYNAMIC_TITLE': " $dynamic_title_placeholder (create new)\n",'ADDTO_WATCH_LATER': "Watch Later",'ADDTO_WATCH_LATER_ADDED': "Added",'ADDTO_WATCH_LATER_ERROR': "Error",'ADDTO_WATCH_QUEUE': "Watch Queue",'ADDTO_WATCH_QUEUE_ADDED': "Added",'ADDTO_WATCH_QUEUE_ERROR': "Error",'ADDTO_TV_QUEUE': "Queue",'ADS_INSTREAM_FIRST_PLAY': "A video ad is playing.",'ADS_INSTREAM_SKIPPABLE': "Video ad can be skipped.",'ADS_OVERLAY_IMPRESSION': "Ad displayed.",'MASTHEAD_NOTIFICATIONS_LABEL': {"case0": "0 unread notifications.", "case1": "1 unread notification.", "other": "# unread notifications."},'MASTHEAD_NOTIFICATIONS_COUNT_99PLUS': "99+"}); yt.setConfig({
896
- 'XSRF_TOKEN': "QUFFLUhqbUVRNm9RcEJoTjBLRmduZE9RRi1fX25pdE4yQXxBQ3Jtc0tub1ZOMUJueXhodlJjVFdrMkU5aEdGa2JjVC1SaTVUZjBEWXRNUWxSdkdoV2ZTcXNCVmt3MnFTclljTExLNU02RmdnaU1Cd0szNlZ2MGlMSHNLUTB0YWVjWjdVWTFSczA1UnNJX3FQYW1GajRyc3J2T2Jpb1VFR28yZ0o0dzZCYVBVWkNuLTlaa0dmSFZKQ3BFaGoyMXpYOTgyUGc=",
897
- 'XSRF_REDIRECT_TOKEN': "iAphLiLP--KzvK_jYbh_vmll1-d8MTQ0NDY5OTkxNEAxNDQ0NjEzNTE0",
898
- 'XSRF_FIELD_NAME': "session_token"
899
- });
900
-
901
- yt.setConfig('FEED_PRIVACY_CSS_URL', "\/\/s.ytimg.com\/yts\/cssbin\/www-feedprivacydialog-vflkxdOgv.css");
902
-
903
- yt.setConfig('FEED_PRIVACY_LIGHTBOX_ENABLED', true);
904
- yt.setConfig({'SBOX_JS_URL': "\/\/s.ytimg.com\/yts\/jsbin\/www-searchbox-vflJyjVvT\/www-searchbox.js",'SBOX_SETTINGS': {"EXPERIMENT_ID":-1,"EXPERIMENT_STR":"","IS_FUSION":false,"PQ":"","PSUGGEST_TOKEN":null,"REQUEST_DOMAIN":"us","HAS_ON_SCREEN_KEYBOARD":false,"REQUEST_LANGUAGE":"en","SESSION_INDEX":null},'SBOX_LABELS': {"SUGGESTION_DISMISS_LABEL":"Remove","SUGGESTION_DISMISSED_LABEL":"Suggestion dismissed"}}); yt.setConfig({
905
- 'YPC_LOADER_JS': "\/\/s.ytimg.com\/yts\/jsbin\/www-ypc-vflvqGLjW\/www-ypc.js",
906
- 'YPC_LOADER_CSS': "\/\/s.ytimg.com\/yts\/cssbin\/www-ypc-vflQnp6NN.css",
907
- 'YPC_SIGNIN_URL': "https:\/\/accounts.google.com\/ServiceLogin?continue=http%3A%2F%2Fwww.youtube.com%2Fsignin%3Fhl%3Den%26app%3Ddesktop%26next%3D%252F%26action_handle_signin%3Dtrue\u0026hl=en\u0026service=youtube\u0026passive=true\u0026uilel=3",
908
- 'DBLCLK_ADVERTISER_ID': "2542116",
909
- 'DBLCLK_YPC_ACTIVITY_GROUP': "youtu444",
910
- 'SUBSCRIPTION_URL': "\/subscription_ajax",
911
- 'YPC_SWITCH_URL': "\/signin?next=%2F\u0026feature=purchases\u0026skip_identity_prompt=True\u0026action_handle_signin=true",
912
- 'YPC_GB_LANGUAGE': "en_US",
913
- 'YPC_GB_URL': "https:\/\/checkout.google.com\/inapp\/lib\/buy.js",
914
- 'YPC_TRANSACTION_URL': "\/transaction_handler",
915
- 'YPC_SUBSCRIPTION_URL': "\/ypc_subscription_ajax",
916
- 'YPC_POST_PURCHASE_URL': "\/ypc_post_purchase_ajax"
917
- });
918
- yt.setMsg({
919
- 'YPC_OFFER_OVERLAY': " \n",
920
- 'YPC_UNSUBSCRIBE_OVERLAY': " \n"
921
- });
922
- yt.setConfig('GOOGLE_HELP_CONTEXT', "watch");
923
- ytcsi.setSpan('st', 113);yt.setConfig({'CSI_SERVICE_NAME': "youtube",'TIMING_ACTION': "watch,watch7_html5",'TIMING_INFO': {"e":"9407016,9408214,9408710,9409069,9409205,9412845,9413142,9414764,9416051,9416126,9416837,9416851,9417707,9418394,9418448,9418675,9419785,9420348,9420535,9421013,9421381,9421708","yt_err":0,"yt_pl":0,"yt_lt":"cold","ei":"ig0bVurxA9qp-QWVhLDIDw","yt_li":0,"yt_ad":0,"yt_spf":0}}); yt.setConfig({
924
- 'XSRF_TOKEN': "QUFFLUhqbUVRNm9RcEJoTjBLRmduZE9RRi1fX25pdE4yQXxBQ3Jtc0tub1ZOMUJueXhodlJjVFdrMkU5aEdGa2JjVC1SaTVUZjBEWXRNUWxSdkdoV2ZTcXNCVmt3MnFTclljTExLNU02RmdnaU1Cd0szNlZ2MGlMSHNLUTB0YWVjWjdVWTFSczA1UnNJX3FQYW1GajRyc3J2T2Jpb1VFR28yZ0o0dzZCYVBVWkNuLTlaa0dmSFZKQ3BFaGoyMXpYOTgyUGc=",
925
- 'XSRF_REDIRECT_TOKEN': "iAphLiLP--KzvK_jYbh_vmll1-d8MTQ0NDY5OTkxNEAxNDQ0NjEzNTE0",
926
- 'XSRF_FIELD_NAME': "session_token"
927
- });
928
- yt.setConfig('THUMB_DELAY_LOAD_BUFFER', 0);
929
- if (window.ytcsi) {window.ytcsi.tick("jl", null, '');}</script>
930
- </body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/mock_data/youtube_gangnam_style.html DELETED
The diff for this file is too large to render. See raw diff
 
tests/mock_data/youtube_gangnam_style.js DELETED
The diff for this file is too large to render. See raw diff
 
tests/requirements.txt DELETED
@@ -1,7 +0,0 @@
1
- bumpversion==0.5.3
2
- coveralls==1.0
3
- flake8==3.3.0
4
- mock==1.3.0
5
- pre-commit==0.15.0
6
- pytest==3.1.3
7
- pytest-cov==2.4.0
 
 
 
 
 
 
 
 
tests/test_pytube.py DELETED
@@ -1,130 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- import warnings
4
-
5
- import mock
6
- import pytest
7
-
8
- from pytube import api
9
- from pytube.exceptions import AgeRestricted
10
- from pytube.exceptions import MultipleObjectsReturned
11
- from pytube.exceptions import PytubeError
12
-
13
-
14
- def test_video_id(yt_video):
15
- """Resolve the video id from url"""
16
- assert yt_video.video_id == '9bZkp7q19f0'
17
-
18
-
19
- def test_auto_filename(yt_video):
20
- """Generate safe filename based on video title"""
21
- expected = u'PSY - GANGNAM STYLE(강남스타일) MV'
22
- assert yt_video.filename == expected
23
-
24
-
25
- def test_manual_filename(yt_video):
26
- """Manually set a filename"""
27
- expected = 'PSY - Gangnam Style'
28
- yt_video.set_filename(expected)
29
- assert yt_video.filename == expected
30
-
31
-
32
- def test_get_all_videos(yt_video):
33
- """Get all videos"""
34
- assert len(yt_video.get_videos()) == 6
35
-
36
-
37
- def test_filter_video_by_extension(yt_video):
38
- """Filter videos by filetype"""
39
- assert len(yt_video.filter('mp4')) == 2
40
- assert len(yt_video.filter('3gp')) == 2
41
- assert len(yt_video.filter('webm')) == 1
42
- assert len(yt_video.filter('flv')) == 1
43
-
44
-
45
- def test_filter_video_by_extension_and_resolution(yt_video):
46
- """Filter videos by file extension and resolution"""
47
- assert len(yt_video.filter('mp4', '720p')) == 1
48
- assert len(yt_video.filter('mp4', '1080p')) == 0
49
-
50
-
51
- def test_filter_video_by_extension_resolution_profile(yt_video):
52
- """Filter videos by file extension, resolution, and profile"""
53
- assert len(yt_video.filter('mp4', '360p', 'Baseline')) == 1
54
-
55
-
56
- def test_filter_video_by_profile(yt_video):
57
- """Filter videos by file profile"""
58
- assert len(yt_video.filter(profile='Simple')) == 2
59
-
60
-
61
- def test_filter_video_by_resolution(yt_video):
62
- """Filter videos by resolution"""
63
- assert len(yt_video.filter(resolution='360p')) == 2
64
-
65
-
66
- def test_get_multiple_items(yt_video):
67
- """get(...) cannot return more than one video"""
68
- with pytest.raises(MultipleObjectsReturned):
69
- yt_video.get(profile='Simple')
70
- yt_video.get('mp4')
71
- yt_video.get(resolution='240p')
72
-
73
-
74
- def test_age_restricted_video():
75
- """Raise exception on age restricted video"""
76
- mock_html = None
77
-
78
- with open('tests/mock_data/youtube_age_restricted.html') as fh:
79
- mock_html = fh.read()
80
-
81
- with mock.patch('pytube.api.urlopen') as urlopen:
82
- urlopen.return_value.read.return_value = mock_html
83
- yt = api.YouTube()
84
-
85
- with pytest.raises(AgeRestricted):
86
- yt.from_url('http://www.youtube.com/watch?v=nzNgkc6t260')
87
-
88
-
89
- def test_deprecation_warnings_on_url_set(yt_video):
90
- """Deprecation warnings get triggered on url set"""
91
- with warnings.catch_warnings(record=True) as w:
92
- # Cause all warnings to always be triggered.
93
- warnings.simplefilter('always')
94
- yt_video.url = 'http://www.youtube.com/watch?v=9bZkp7q19f0'
95
- assert len(w) == 1
96
-
97
-
98
- def test_deprecation_warnings_on_filename_set(yt_video):
99
- """Deprecation warnings get triggered on filename set"""
100
- with warnings.catch_warnings(record=True) as w:
101
- # Cause all warnings to always be triggered.
102
- warnings.simplefilter('always')
103
- yt_video.filename = 'Gangnam Style'
104
- assert len(w) == 1
105
-
106
-
107
- def test_deprecation_warnings_on_videos_get(yt_video):
108
- """Deprecation warnings get triggered on video getter"""
109
- with warnings.catch_warnings(record=True) as w:
110
- # Cause all warnings to always be triggered.
111
- warnings.simplefilter('always')
112
- yt_video.videos
113
- assert len(w) == 1
114
-
115
-
116
- def test_get_json_offset(yt_video):
117
- """Find the offset in the html for where the js starts"""
118
- mock_html = None
119
-
120
- with open('tests/mock_data/youtube_gangnam_style.html') as fh:
121
- mock_html = fh.read()
122
-
123
- offset = yt_video._get_json_offset(mock_html)
124
- assert offset == 312
125
-
126
-
127
- def test_get_json_offset_with_bad_html(yt_video):
128
- """Raise exception if json offset cannot be found"""
129
- with pytest.raises(PytubeError):
130
- yt_video._get_json_offset('asdfasdf')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_utils.py DELETED
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- from __future__ import unicode_literals
4
-
5
- from pytube import utils
6
-
7
-
8
- blob = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
9
-
10
-
11
- def test_truncate():
12
- """Truncate string works as expected"""
13
- truncated = utils.truncate(blob, 11)
14
- assert truncated == 'Lorem ipsum'
15
-
16
-
17
- def test_safe_filename():
18
- """Unsafe characters get stripped from generated filename"""
19
- assert utils.safe_filename('abc1245$$') == 'abc1245'
20
- assert utils.safe_filename('abc##') == 'abc'
21
- assert utils.safe_filename('abc:foo') == 'abc -foo'
22
- assert utils.safe_filename('abc_foo') == 'abc foo'
23
-
24
-
25
- def test_sizeof():
26
- """Accurately converts the bytes to its humanized equivalent"""
27
- assert utils.sizeof(1) == '1 byte'
28
- assert utils.sizeof(2) == '2 bytes'
29
- assert utils.sizeof(2400) == '2 KB'
30
- assert utils.sizeof(2400000) == '2 MB'
31
- assert utils.sizeof(2400000000) == '2 GB'
32
- assert utils.sizeof(2400000000000) == '2 TB'
33
- assert utils.sizeof(2400000000000000) == '2 PB'