nficano commited on
Commit
6ba0ea5
·
1 Parent(s): 11c44df
.pre-commit-config.yaml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ - repo: https://github.com/pre-commit/pre-commit-hooks
2
+ sha: v0.8.0
3
+ hooks:
4
+ - id: autopep8-wrapper
5
+ - id: check-ast
6
+ - id: check-case-conflict
7
+ - id: check-merge-conflict
8
+ - id: detect-private-key
9
+ - id: double-quote-string-fixer
10
+ - id: end-of-file-fixer
11
+ - id: flake8
12
+ - id: requirements-txt-fixer
13
+ - id: trailing-whitespace
14
+ - repo: https://github.com/asottile/reorder_python_imports
15
+ sha: v0.3.4
16
+ hooks:
17
+ - id: reorder-python-imports
18
+ language_version: python3.6
19
+ - repo: https://github.com/Lucas-C/pre-commit-hooks-safety
20
+ sha: v1.1.0
21
+ hooks:
22
+ - id: python-safety-dependencies-check
pytube/__main__.py CHANGED
@@ -1,30 +1,57 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import print_function
4
- import sys
5
- import os
6
  import argparse
 
 
 
7
 
8
  from . import YouTube
9
- from .utils import print_status, FullPaths
10
  from .exceptions import PytubeError
11
- from pprint import pprint
 
12
 
13
 
14
  def main():
15
  parser = argparse.ArgumentParser(description='YouTube video downloader')
16
- parser.add_argument("url", help=(
17
- "The URL of the Video to be downloaded"))
18
- parser.add_argument("--extension", "-e", dest="ext", help=(
19
- "The requested format of the video"))
20
- parser.add_argument("--resolution", "-r", dest="res", help=(
21
- "The requested resolution"))
22
- parser.add_argument("--path", "-p", action=FullPaths, default=os.getcwd(),
23
- dest="path", help=("The path to save the video to."))
24
- parser.add_argument("--filename", "-f", dest="filename", help=(
25
- "The filename, without extension, to save the video in."))
26
- parser.add_argument("--show_available", "-s", action='store_true',
27
- dest='show_available', help=("Prints a list of available formats for download."))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  args = parser.parse_args()
30
 
@@ -36,7 +63,7 @@ def main():
36
  res = video.resolution
37
  videos.append((ext, res))
38
  except PytubeError:
39
- print("Incorrect video URL.")
40
  sys.exit(1)
41
 
42
  if args.show_available:
@@ -48,8 +75,8 @@ def main():
48
 
49
  if args.ext or args.res:
50
  if not all([args.ext, args.res]):
51
- print("Make sure you give either of the below specified "
52
- "format/resolution combination.")
53
  print_available_vids(videos)
54
  sys.exit(1)
55
 
@@ -59,7 +86,7 @@ def main():
59
  # Check if there's a video returned
60
  if not vid:
61
  print("There's no video with the specified format/resolution "
62
- "combination.")
63
  pprint(videos)
64
  sys.exit(1)
65
 
@@ -68,7 +95,7 @@ def main():
68
  videos = yt.filter(extension=args.ext)
69
  # Check if we have a video
70
  if not videos:
71
- print("There are no videos in the specified format.")
72
  sys.exit(1)
73
  # Select the highest resolution one
74
  vid = max(videos)
@@ -77,8 +104,8 @@ def main():
77
  videos = yt.filter(resolution=args.res)
78
  # Check if we have a video
79
  if not videos:
80
- print("There are no videos in the specified in the specified "
81
- "resolution.")
82
  sys.exit(1)
83
  # Select the highest resolution one
84
  vid = max(videos)
@@ -87,25 +114,29 @@ def main():
87
  print_available_vids(videos)
88
  while True:
89
  try:
90
- choice = int(input("Enter choice: "))
91
  vid = yt.get(*videos[choice])
92
  break
93
  except (ValueError, IndexError):
94
- print("Requires an integer in range 0-{}".format(len(videos) - 1))
 
95
  except KeyboardInterrupt:
96
  sys.exit(2)
97
 
98
  try:
99
  vid.download(path=args.path, on_progress=print_status)
100
  except KeyboardInterrupt:
101
- print("Download interrupted.")
102
  sys.exit(1)
103
 
 
104
  def print_available_vids(videos):
105
- formatString = "{:<2} {:<15} {:<15}"
106
- print(formatString.format("", "Resolution", "Extension"))
107
- print("-"*28)
108
- print("\n".join([formatString.format(index, *formatTuple) for index, formatTuple in enumerate(videos)]))
 
 
109
 
110
  if __name__ == '__main__':
111
  main()
 
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
 
 
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:
 
75
 
76
  if args.ext or args.res:
77
  if not all([args.ext, args.res]):
78
+ print('Make sure you give either of the below specified '
79
+ 'format/resolution combination.')
80
  print_available_vids(videos)
81
  sys.exit(1)
82
 
 
86
  # Check if there's a video returned
87
  if not vid:
88
  print("There's no video with the specified format/resolution "
89
+ 'combination.')
90
  pprint(videos)
91
  sys.exit(1)
92
 
 
95
  videos = yt.filter(extension=args.ext)
96
  # Check if we have a video
97
  if not videos:
98
+ print('There are no videos in the specified format.')
99
  sys.exit(1)
100
  # Select the highest resolution one
101
  vid = max(videos)
 
104
  videos = yt.filter(resolution=args.res)
105
  # Check if we have a video
106
  if not videos:
107
+ print('There are no videos in the specified in the specified '
108
+ 'resolution.')
109
  sys.exit(1)
110
  # Select the highest resolution one
111
  vid = max(videos)
 
114
  print_available_vids(videos)
115
  while True:
116
  try:
117
+ choice = int(input('Enter choice: '))
118
  vid = yt.get(*videos[choice])
119
  break
120
  except (ValueError, IndexError):
121
+ print('Requires an integer in range 0-{}'
122
+ .format(len(videos) - 1))
123
  except KeyboardInterrupt:
124
  sys.exit(2)
125
 
126
  try:
127
  vid.download(path=args.path, on_progress=print_status)
128
  except KeyboardInterrupt:
129
+ print('Download interrupted.')
130
  sys.exit(1)
131
 
132
+
133
  def print_available_vids(videos):
134
+ formatString = '{:<2} {:<15} {:<15}'
135
+ print(formatString.format('', 'Resolution', 'Extension'))
136
+ print('-' * 28)
137
+ print('\n'.join([formatString.format(index, *formatTuple)
138
+ for index, formatTuple in enumerate(videos)]))
139
+
140
 
141
  if __name__ == '__main__':
142
  main()
pytube/api.py CHANGED
@@ -1,14 +1,22 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import absolute_import
4
- from collections import defaultdict
5
  import json
6
  import logging
7
  import re
8
  import warnings
9
- from .compat import urlopen, urlparse, parse_qs, unquote
10
- from .exceptions import MultipleObjectsReturned, PytubeError, CipherError, \
11
- DoesNotExist, AgeRestricted
 
 
 
 
 
 
 
 
12
  from .jsinterp import JSInterpreter
13
  from .models import Video
14
  from .utils import safe_filename
@@ -18,65 +26,66 @@ log = logging.getLogger(__name__)
18
  # YouTube quality and codecs id map.
19
  QUALITY_PROFILES = {
20
  # flash
21
- 5: ("flv", "240p", "Sorenson H.263", "N/A", "0.25", "MP3", "64"),
22
 
23
  # 3gp
24
- 17: ("3gp", "144p", "MPEG-4 Visual", "Simple", "0.05", "AAC", "24"),
25
- 36: ("3gp", "240p", "MPEG-4 Visual", "Simple", "0.17", "AAC", "38"),
26
 
27
  # webm
28
- 43: ("webm", "360p", "VP8", "N/A", "0.5", "Vorbis", "128"),
29
- 100: ("webm", "360p", "VP8", "3D", "N/A", "Vorbis", "128"),
30
 
31
  # mpeg4
32
- 18: ("mp4", "360p", "H.264", "Baseline", "0.5", "AAC", "96"),
33
- 22: ("mp4", "720p", "H.264", "High", "2-2.9", "AAC", "192"),
34
- 82: ("mp4", "360p", "H.264", "3D", "0.5", "AAC", "96"),
35
- 83: ("mp4", "240p", "H.264", "3D", "0.5", "AAC", "96"),
36
- 84: ("mp4", "720p", "H.264", "3D", "2-2.9", "AAC", "152"),
37
- 85: ("mp4", "1080p", "H.264", "3D", "2-2.9", "AAC", "152"),
38
-
39
- 160: ("mp4", "144p", "H.264", "Main", "0.1", "", ""),
40
- 133: ("mp4", "240p", "H.264", "Main", "0.2-0.3", "", ""),
41
- 134: ("mp4", "360p", "H.264", "Main", "0.3-0.4", "", ""),
42
- 135: ("mp4", "480p", "H.264", "Main", "0.5-1", "", ""),
43
- 136: ("mp4", "720p", "H.264", "Main", "1-1.5", "", ""),
44
- 298: ("mp4", "720p HFR", "H.264", "Main", "3-3.5", "", ""),
45
-
46
- 137: ("mp4", "1080p", "H.264", "High", "2.5-3", "", ""),
47
- 299: ("mp4", "1080p HFR", "H.264", "High", "5.5", "", ""),
48
- 264: ("mp4", "2160p-2304p", "H.264", "High", "12.5-16", "", ""),
49
- 266: ("mp4", "2160p-4320p", "H.264", "High", "13.5-25", "", ""),
50
-
51
- 242: ("webm", "240p", "vp9", "n/a", "0.1-0.2", "", ""),
52
- 243: ("webm", "360p", "vp9", "n/a", "0.25", "", ""),
53
- 244: ("webm", "480p", "vp9", "n/a", "0.5", "", ""),
54
- 247: ("webm", "720p", "vp9", "n/a", "0.7-0.8", "", ""),
55
- 248: ("webm", "1080p", "vp9", "n/a", "1.5", "", ""),
56
- 271: ("webm", "1440p", "vp9", "n/a", "9", "", ""),
57
- 278: ("webm", "144p 15 fps", "vp9", "n/a", "0.08", "", ""),
58
- 302: ("webm", "720p HFR", "vp9", "n/a", "2.5", "", ""),
59
- 303: ("webm", "1080p HFR", "vp9", "n/a", "5", "", ""),
60
- 308: ("webm", "1440p HFR", "vp9", "n/a", "10", "", ""),
61
- 313: ("webm", "2160p", "vp9", "n/a", "13-15", "", ""),
62
- 315: ("webm", "2160p HFR", "vp9", "n/a", "20-25", "", "")
63
  }
64
 
65
  # The keys corresponding to the quality/codec map above.
66
  QUALITY_PROFILE_KEYS = (
67
- "extension",
68
- "resolution",
69
- "video_codec",
70
- "profile",
71
- "video_bitrate",
72
- "audio_codec",
73
- "audio_bitrate"
74
  )
75
 
76
 
77
  class YouTube(object):
78
  """Class representation of a single instance of a YouTube session.
79
  """
 
80
  def __init__(self, url=None):
81
  """Initializes YouTube API wrapper.
82
 
@@ -103,17 +112,17 @@ class YouTube(object):
103
  :param str url:
104
  The url to the YouTube video.
105
  """
106
- warnings.warn("url setter deprecated, use `from_url()` "
107
- "instead.", DeprecationWarning)
108
  self.from_url(url)
109
 
110
  @property
111
  def video_id(self):
112
  """Gets the video id by parsing and extracting it from the url."""
113
  parts = urlparse(self._video_url)
114
- qs = getattr(parts, "query")
115
  if qs:
116
- video_id = parse_qs(qs).get("v")
117
  if video_id:
118
  return video_id.pop()
119
 
@@ -135,8 +144,8 @@ class YouTube(object):
135
  :param str filename:
136
  The filename of the video.
137
  """
138
- warnings.warn("filename setter deprecated. Use `set_filename()` "
139
- "instead.", DeprecationWarning)
140
  self.set_filename(filename)
141
 
142
  def set_filename(self, filename):
@@ -162,8 +171,8 @@ class YouTube(object):
162
  """Gets all videos. (This method is deprecated. Use `get_videos()`
163
  instead.
164
  """
165
- warnings.warn("videos property deprecated. Use `get_videos()` "
166
- "instead.", DeprecationWarning)
167
  return self._videos
168
 
169
  def from_url(self, url):
@@ -183,40 +192,40 @@ class YouTube(object):
183
  video_data = self.get_video_data()
184
 
185
  # Set the title from the title.
186
- self.title = video_data.get("args", {}).get("title")
187
 
188
  # Rewrite and add the url to the javascript file, we'll need to fetch
189
  # this if YouTube doesn't provide us with the signature.
190
- js_partial_url = video_data.get("assets", {}).get("js")
191
  if js_partial_url.startswith('//'):
192
  js_url = 'http:' + js_partial_url
193
  elif js_partial_url.startswith('/'):
194
  js_url = 'https://youtube.com' + js_partial_url
195
 
196
  # Just make these easily accessible as variables.
197
- stream_map = video_data.get("args", {}).get("stream_map")
198
- video_urls = stream_map.get("url")
199
 
200
  # For each video url, identify the quality profile and add it to list
201
  # of available videos.
202
  for i, url in enumerate(video_urls):
203
- log.debug("attempting to get quality profile from url: %s", url)
204
  try:
205
  itag, quality_profile = self._get_quality_profile_from_url(url)
206
  if not quality_profile:
207
- log.warn("unable to identify profile for itag=%s", itag)
208
  continue
209
  except (TypeError, KeyError) as e:
210
- log.exception("passing on exception %s", e)
211
  continue
212
 
213
  # Check if we have the signature, otherwise we'll need to get the
214
  # cipher from the js.
215
- if "signature=" not in url:
216
- log.debug("signature not in url, attempting to resolve the "
217
- "cipher.")
218
- signature = self._get_cipher(stream_map["s"][i], js_url)
219
- url = "{0}&signature={1}".format(url, signature)
220
  self._add_video(url, self.filename, **quality_profile)
221
  # Clear the cached js. Make sure to keep this at the end of
222
  # `from_url()` so we can mock inject the js in unit tests.
@@ -246,11 +255,11 @@ class YouTube(object):
246
  result.append(v)
247
  matches = len(result)
248
  if matches <= 0:
249
- raise DoesNotExist("No videos met this criteria.")
250
  elif matches == 1:
251
  return result[0]
252
  else:
253
- raise MultipleObjectsReturned("Multiple videos met this criteria.")
254
 
255
  def filter(self, extension=None, resolution=None, profile=None):
256
  """Gets a filtered list of videos given a file extention and/or
@@ -282,26 +291,26 @@ class YouTube(object):
282
  self.title = None
283
  response = urlopen(self.url)
284
  if not response:
285
- raise PytubeError("Unable to open url: {0}".format(self.url))
286
 
287
  html = response.read()
288
  if isinstance(html, str):
289
- restriction_pattern = "og:restrictions:age"
290
  else:
291
- restriction_pattern = bytes("og:restrictions:age", "utf-8")
292
 
293
  if restriction_pattern in html:
294
- raise AgeRestricted("Age restricted video. Unable to download "
295
- "without being signed in.")
296
 
297
  # Extract out the json data from the html response body.
298
  json_object = self._get_json_data(html)
299
 
300
  # Here we decode the stream map and bundle it into the json object. We
301
  # do this just so we just can return one object for the video data.
302
- encoded_stream_map = json_object.get("args", {}).get(
303
- "url_encoded_fmt_stream_map")
304
- json_object["args"]["stream_map"] = self._parse_stream_map(
305
  encoded_stream_map)
306
  return json_object
307
 
@@ -315,18 +324,18 @@ class YouTube(object):
315
  dct = defaultdict(list)
316
 
317
  # Split the comma separated videos.
318
- videos = blob.split(",")
319
 
320
  # Unquote the characters and split to parameters.
321
- videos = [video.split("&") for video in videos]
322
 
323
  # Split at the equals sign so we can break this key value pairs and
324
  # toss it into a dictionary.
325
  for video in videos:
326
  for kv in video:
327
- key, value = kv.split("=")
328
  dct[key].append(unquote(value))
329
- log.debug("decoded stream map: %s", dct)
330
  return dct
331
 
332
  def _get_json_data(self, html):
@@ -337,23 +346,23 @@ class YouTube(object):
337
  """
338
  # 18 represents the length of "ytplayer.config = ".
339
  if isinstance(html, str):
340
- json_start_pattern = "ytplayer.config = "
341
  else:
342
- json_start_pattern = bytes("ytplayer.config = ", "utf-8")
343
  pattern_idx = html.find(json_start_pattern)
344
  # In case video is unable to play
345
  if(pattern_idx == -1):
346
- raise PytubeError("Unable to find start pattern.")
347
  start = pattern_idx + 18
348
  html = html[start:]
349
 
350
  offset = self._get_json_offset(html)
351
  if not offset:
352
- raise PytubeError("Unable to extract json.")
353
  if isinstance(html, str):
354
  json_content = json.loads(html[:offset])
355
  else:
356
- json_content = json.loads(html[:offset].decode("utf-8"))
357
 
358
  return json_content
359
 
@@ -368,14 +377,14 @@ class YouTube(object):
368
  for i, ch in enumerate(html):
369
  if isinstance(ch, int):
370
  ch = chr(ch)
371
- if ch == "{":
372
  unmatched_brackets_num += 1
373
- elif ch == "}":
374
  unmatched_brackets_num -= 1
375
  if unmatched_brackets_num == 0:
376
  break
377
  else:
378
- raise PytubeError("Unable to determine json offset.")
379
  return index + i
380
 
381
  def _get_cipher(self, signature, url):
@@ -391,7 +400,7 @@ class YouTube(object):
391
  if not self._js_cache:
392
  response = urlopen(url)
393
  if not response:
394
- raise PytubeError("Unable to open url: {0}".format(self.url))
395
  self._js_cache = response.read().decode()
396
  try:
397
  matches = reg_exp.search(self._js_cache)
@@ -404,8 +413,8 @@ class YouTube(object):
404
  return initial_function([signature])
405
  except Exception as e:
406
  raise CipherError("Couldn't cipher the signature. Maybe YouTube "
407
- "has changed the cipher algorithm. Notify this "
408
- "issue on GitHub: {0}".format(e))
409
  return False
410
 
411
  def _get_quality_profile_from_url(self, video_url):
@@ -429,11 +438,11 @@ class YouTube(object):
429
  # corresponding quality profile, referenced by the itag.
430
  return itag, dict(list(zip(QUALITY_PROFILE_KEYS, quality_profile)))
431
  if not itag:
432
- raise PytubeError("Unable to get encoding profile, no itag found.")
433
  elif len(itag) > 1:
434
- log.warn("Multiple itags found: %s", itag)
435
- raise PytubeError("Unable to get encoding profile, multiple itags "
436
- "found.")
437
  return False
438
 
439
  def _add_video(self, url, filename, **kwargs):
 
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
 
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
 
 
112
  :param str url:
113
  The url to the YouTube video.
114
  """
115
+ warnings.warn('url setter deprecated, use `from_url()` '
116
+ 'instead.', DeprecationWarning)
117
  self.from_url(url)
118
 
119
  @property
120
  def video_id(self):
121
  """Gets the video id by parsing and extracting it from the url."""
122
  parts = urlparse(self._video_url)
123
+ qs = getattr(parts, 'query')
124
  if qs:
125
+ video_id = parse_qs(qs).get('v')
126
  if video_id:
127
  return video_id.pop()
128
 
 
144
  :param str filename:
145
  The filename of the video.
146
  """
147
+ warnings.warn('filename setter deprecated. Use `set_filename()` '
148
+ 'instead.', DeprecationWarning)
149
  self.set_filename(filename)
150
 
151
  def set_filename(self, filename):
 
171
  """Gets all videos. (This method is deprecated. Use `get_videos()`
172
  instead.
173
  """
174
+ warnings.warn('videos property deprecated. Use `get_videos()` '
175
+ 'instead.', DeprecationWarning)
176
  return self._videos
177
 
178
  def from_url(self, url):
 
192
  video_data = self.get_video_data()
193
 
194
  # Set the title from the title.
195
+ self.title = video_data.get('args', {}).get('title')
196
 
197
  # Rewrite and add the url to the javascript file, we'll need to fetch
198
  # this if YouTube doesn't provide us with the signature.
199
+ js_partial_url = video_data.get('assets', {}).get('js')
200
  if js_partial_url.startswith('//'):
201
  js_url = 'http:' + js_partial_url
202
  elif js_partial_url.startswith('/'):
203
  js_url = 'https://youtube.com' + js_partial_url
204
 
205
  # Just make these easily accessible as variables.
206
+ stream_map = video_data.get('args', {}).get('stream_map')
207
+ video_urls = stream_map.get('url')
208
 
209
  # For each video url, identify the quality profile and add it to list
210
  # of available videos.
211
  for i, url in enumerate(video_urls):
212
+ log.debug('attempting to get quality profile from url: %s', url)
213
  try:
214
  itag, quality_profile = self._get_quality_profile_from_url(url)
215
  if not quality_profile:
216
+ log.warn('unable to identify profile for itag=%s', itag)
217
  continue
218
  except (TypeError, KeyError) as e:
219
+ log.exception('passing on exception %s', e)
220
  continue
221
 
222
  # Check if we have the signature, otherwise we'll need to get the
223
  # cipher from the js.
224
+ if 'signature=' not in url:
225
+ log.debug('signature not in url, attempting to resolve the '
226
+ 'cipher.')
227
+ signature = self._get_cipher(stream_map['s'][i], js_url)
228
+ url = '{0}&signature={1}'.format(url, signature)
229
  self._add_video(url, self.filename, **quality_profile)
230
  # Clear the cached js. Make sure to keep this at the end of
231
  # `from_url()` so we can mock inject the js in unit tests.
 
255
  result.append(v)
256
  matches = len(result)
257
  if matches <= 0:
258
+ raise DoesNotExist('No videos met this criteria.')
259
  elif matches == 1:
260
  return result[0]
261
  else:
262
+ raise MultipleObjectsReturned('Multiple videos met this criteria.')
263
 
264
  def filter(self, extension=None, resolution=None, profile=None):
265
  """Gets a filtered list of videos given a file extention and/or
 
291
  self.title = None
292
  response = urlopen(self.url)
293
  if not response:
294
+ raise PytubeError('Unable to open url: {0}'.format(self.url))
295
 
296
  html = response.read()
297
  if isinstance(html, str):
298
+ restriction_pattern = 'og:restrictions:age'
299
  else:
300
+ restriction_pattern = bytes('og:restrictions:age', 'utf-8')
301
 
302
  if restriction_pattern in html:
303
+ raise AgeRestricted('Age restricted video. Unable to download '
304
+ 'without being signed in.')
305
 
306
  # Extract out the json data from the html response body.
307
  json_object = self._get_json_data(html)
308
 
309
  # Here we decode the stream map and bundle it into the json object. We
310
  # do this just so we just can return one object for the video data.
311
+ encoded_stream_map = json_object.get('args', {}).get(
312
+ 'url_encoded_fmt_stream_map')
313
+ json_object['args']['stream_map'] = self._parse_stream_map(
314
  encoded_stream_map)
315
  return json_object
316
 
 
324
  dct = defaultdict(list)
325
 
326
  # Split the comma separated videos.
327
+ videos = blob.split(',')
328
 
329
  # Unquote the characters and split to parameters.
330
+ videos = [video.split('&') for video in videos]
331
 
332
  # Split at the equals sign so we can break this key value pairs and
333
  # toss it into a dictionary.
334
  for video in videos:
335
  for kv in video:
336
+ key, value = kv.split('=')
337
  dct[key].append(unquote(value))
338
+ log.debug('decoded stream map: %s', dct)
339
  return dct
340
 
341
  def _get_json_data(self, html):
 
346
  """
347
  # 18 represents the length of "ytplayer.config = ".
348
  if isinstance(html, str):
349
+ json_start_pattern = 'ytplayer.config = '
350
  else:
351
+ json_start_pattern = bytes('ytplayer.config = ', 'utf-8')
352
  pattern_idx = html.find(json_start_pattern)
353
  # In case video is unable to play
354
  if(pattern_idx == -1):
355
+ raise PytubeError('Unable to find start pattern.')
356
  start = pattern_idx + 18
357
  html = html[start:]
358
 
359
  offset = self._get_json_offset(html)
360
  if not offset:
361
+ raise PytubeError('Unable to extract json.')
362
  if isinstance(html, str):
363
  json_content = json.loads(html[:offset])
364
  else:
365
+ json_content = json.loads(html[:offset].decode('utf-8'))
366
 
367
  return json_content
368
 
 
377
  for i, ch in enumerate(html):
378
  if isinstance(ch, int):
379
  ch = chr(ch)
380
+ if ch == '{':
381
  unmatched_brackets_num += 1
382
+ elif ch == '}':
383
  unmatched_brackets_num -= 1
384
  if unmatched_brackets_num == 0:
385
  break
386
  else:
387
+ raise PytubeError('Unable to determine json offset.')
388
  return index + i
389
 
390
  def _get_cipher(self, signature, url):
 
400
  if not self._js_cache:
401
  response = urlopen(url)
402
  if not response:
403
+ raise PytubeError('Unable to open url: {0}'.format(self.url))
404
  self._js_cache = response.read().decode()
405
  try:
406
  matches = reg_exp.search(self._js_cache)
 
413
  return initial_function([signature])
414
  except Exception as e:
415
  raise CipherError("Couldn't cipher the signature. Maybe YouTube "
416
+ 'has changed the cipher algorithm. Notify this '
417
+ 'issue on GitHub: {0}'.format(e))
418
  return False
419
 
420
  def _get_quality_profile_from_url(self, video_url):
 
438
  # corresponding quality profile, referenced by the itag.
439
  return itag, dict(list(zip(QUALITY_PROFILE_KEYS, quality_profile)))
440
  if not itag:
441
+ raise PytubeError('Unable to get encoding profile, no itag found.')
442
  elif len(itag) > 1:
443
+ log.warn('Multiple itags found: %s', itag)
444
+ raise PytubeError('Unable to get encoding profile, multiple itags '
445
+ 'found.')
446
  return False
447
 
448
  def _add_video(self, url, filename, **kwargs):
pytube/jsinterp.py CHANGED
@@ -1,9 +1,11 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import unicode_literals
 
4
  import json
5
  import operator
6
  import re
 
7
  from .exceptions import ExtractorError
8
 
9
  _OPERATORS = [
@@ -118,9 +120,8 @@ class JSInterpreter(object):
118
  except ValueError:
119
  pass
120
 
121
- m = re.match(
122
- r'(?P<var>%s)\.(?P<member>[^(]+)(?:\(+(?P<args>[^()]*)\))?$' % _NAME_RE,
123
- expr)
124
  if m:
125
  variable = m.group('var')
126
  member = m.group('member')
@@ -212,7 +213,8 @@ class JSInterpreter(object):
212
  obj = {}
213
  obj_m = re.search(
214
  (r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
215
- r'\s*(?P<fields>([a-zA-Z$0-9]+\s*:\s*function\(.*?\)\s*\{.*?\}(?:,\s*)?)*)' +
 
216
  r'\}\s*;',
217
  self.code)
218
  fields = obj_m.group('fields')
@@ -223,7 +225,8 @@ class JSInterpreter(object):
223
  fields)
224
  for f in fields_m:
225
  argnames = f.group('args').split(',')
226
- obj[f.group('key')] = self.build_function(argnames, f.group('code'))
 
227
 
228
  return obj
229
 
 
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 = [
 
120
  except ValueError:
121
  pass
122
 
123
+ m = re.match(r'(?P<var>%s)\.(?P<member>[^(]+)'
124
+ r'(?:\(+(?P<args>[^()]*)\))?$' % _NAME_RE, expr)
 
125
  if m:
126
  variable = m.group('var')
127
  member = m.group('member')
 
213
  obj = {}
214
  obj_m = re.search(
215
  (r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
216
+ r'\s*(?P<fields>([a-zA-Z$0-9]+\s*:\s*function\(.*?\)\s*\'' +
217
+ r'{.*?\}(?:,\s*)?)*)' +
218
  r'\}\s*;',
219
  self.code)
220
  fields = obj_m.group('fields')
 
225
  fields)
226
  for f in fields_m:
227
  argnames = f.group('args').split(',')
228
+ obj[f.group('key')] = self.build_function(
229
+ argnames, f.group('code'))
230
 
231
  return obj
232
 
pytube/models.py CHANGED
@@ -1,6 +1,7 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import unicode_literals
 
4
  import os
5
  from time import clock
6
 
@@ -13,6 +14,7 @@ except ImportError:
13
  class Video(object):
14
  """Class representation of a single instance of a YouTube video.
15
  """
 
16
  def __init__(self, url, filename, extension, resolution=None,
17
  video_codec=None, profile=None, video_bitrate=None,
18
  audio_codec=None, audio_bitrate=None):
@@ -70,7 +72,7 @@ class Video(object):
70
  if not os.path.isdir(path):
71
  raise OSError('Make sure path exists.')
72
 
73
- filename = "{0}.{1}".format(self.filename, self.extension)
74
  path = os.path.join(path, filename)
75
  # TODO: If it's not a path, this should raise an ``OSError``.
76
  # TODO: Move this into cli, this kind of logic probably shouldn't be
@@ -108,7 +110,7 @@ class Video(object):
108
  # to disable this.
109
  os.remove(path)
110
  raise KeyboardInterrupt(
111
- "Interrupt signal given. Deleting incomplete video.")
112
 
113
  def file_size(self, response):
114
  """Gets the file size from the response
@@ -117,12 +119,12 @@ class Video(object):
117
  Response of a opened url.
118
  """
119
  meta_data = dict(response.info().items())
120
- return int(meta_data.get("Content-Length") or
121
- meta_data.get("content-length"))
122
 
123
  def __repr__(self):
124
  """A clean representation of the class instance."""
125
- return "<Video: {0} (.{1}) - {2} - {3}>".format(
126
  self.video_codec, self.extension, self.resolution, self.profile)
127
 
128
  def __lt__(self, other):
@@ -133,6 +135,6 @@ class Video(object):
133
  The instance of the other video instance for comparison.
134
  """
135
  if isinstance(other, Video):
136
- v1 = "{0} {1}".format(self.extension, self.resolution)
137
- v2 = "{0} {1}".format(other.extension, other.resolution)
138
  return (v1 > v2) - (v1 < v2) < 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
 
 
14
  class Video(object):
15
  """Class representation of a single instance of a YouTube video.
16
  """
17
+
18
  def __init__(self, url, filename, extension, resolution=None,
19
  video_codec=None, profile=None, video_bitrate=None,
20
  audio_codec=None, audio_bitrate=None):
 
72
  if not os.path.isdir(path):
73
  raise OSError('Make sure path exists.')
74
 
75
+ filename = '{0}.{1}'.format(self.filename, self.extension)
76
  path = os.path.join(path, filename)
77
  # TODO: If it's not a path, this should raise an ``OSError``.
78
  # TODO: Move this into cli, this kind of logic probably shouldn't be
 
110
  # to disable this.
111
  os.remove(path)
112
  raise KeyboardInterrupt(
113
+ 'Interrupt signal given. Deleting incomplete video.')
114
 
115
  def file_size(self, response):
116
  """Gets the file size from the response
 
119
  Response of a opened url.
120
  """
121
  meta_data = dict(response.info().items())
122
+ return int(meta_data.get('Content-Length') or
123
+ meta_data.get('content-length'))
124
 
125
  def __repr__(self):
126
  """A clean representation of the class instance."""
127
+ return '<Video: {0} (.{1}) - {2} - {3}>'.format(
128
  self.video_codec, self.extension, self.resolution, self.profile)
129
 
130
  def __lt__(self, other):
 
135
  The instance of the other video instance for comparison.
136
  """
137
  if isinstance(other, Video):
138
+ v1 = '{0} {1}'.format(self.extension, self.resolution)
139
+ v2 = '{0} {1}'.format(other.extension, other.resolution)
140
  return (v1 > v2) - (v1 < v2) < 0
pytube/utils.py CHANGED
@@ -1,9 +1,8 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  import argparse
4
- import re
5
  import math
6
-
7
  from os import path
8
  from sys import stdout
9
  from time import clock
@@ -11,6 +10,7 @@ from time import clock
11
 
12
  class FullPaths(argparse.Action):
13
  """Expand user- and relative-paths"""
 
14
  def __call__(self, parser, namespace, values, option_string=None):
15
  setattr(namespace, self.dest, path.abspath(path.expanduser(values)))
16
 
@@ -50,10 +50,11 @@ def sizeof(byts):
50
  """
51
  sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
52
  power = int(math.floor(math.log(byts, 1024)))
53
- value = int(byts/float(1024**power))
54
  suffix = sizes[power] if byts != 1 else 'byte'
55
  return '{0} {1}'.format(value, suffix)
56
 
 
57
  def print_status(progress, file_size, start):
58
  """
59
  This function - when passed as `on_progress` to `Video.download` - prints
@@ -71,7 +72,7 @@ def print_status(progress, file_size, start):
71
  done = int(50 * progress / int(file_size))
72
  dt = (clock() - start)
73
  if dt > 0:
74
- stdout.write("\r [%s%s][%3.2f%%] %s at %s/s " %
75
  ('=' * done, ' ' * (50 - done), percent_done,
76
  sizeof(file_size), sizeof(progress // dt)))
77
  stdout.flush()
 
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
 
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
 
 
50
  """
51
  sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
52
  power = int(math.floor(math.log(byts, 1024)))
53
+ value = int(byts / float(1024**power))
54
  suffix = sizes[power] if byts != 1 else 'byte'
55
  return '{0} {1}'.format(value, suffix)
56
 
57
+
58
  def print_status(progress, file_size, start):
59
  """
60
  This function - when passed as `on_progress` to `Video.download` - prints
 
72
  done = int(50 * progress / int(file_size))
73
  dt = (clock() - start)
74
  if dt > 0:
75
+ stdout.write('\r [%s%s][%3.2f%%] %s at %s/s ' %
76
  ('=' * done, ' ' * (50 - done), percent_done,
77
  sizeof(file_size), sizeof(progress // dt)))
78
  stdout.flush()
setup.py CHANGED
@@ -12,12 +12,12 @@ with open('LICENSE.txt') as readme_file:
12
  license = readme_file.read()
13
 
14
  setup(
15
- name="pytube",
16
- version="6.3.1",
17
- author="Nick Ficano",
18
- author_email="nficano@gmail.com",
19
  packages=['pytube'],
20
- url="https://github.com/nficano/pytube",
21
  license=license,
22
  entry_points={
23
  'console_scripts': [
@@ -25,29 +25,29 @@ setup(
25
  ],
26
  },
27
  classifiers=[
28
- "Development Status :: 5 - Production/Stable",
29
- "Environment :: Console",
30
- "Intended Audience :: Developers",
31
- "License :: OSI Approved :: MIT License",
32
- "Natural Language :: English",
33
- "Operating System :: MacOS",
34
- "Operating System :: Microsoft",
35
- "Operating System :: POSIX",
36
- "Operating System :: Unix",
37
- "Programming Language :: Python :: 2.6",
38
- "Programming Language :: Python :: 2.7",
39
- "Programming Language :: Python :: 3.3",
40
- "Programming Language :: Python :: 3.4",
41
- "Programming Language :: Python :: 3.5",
42
- "Programming Language :: Python :: 3.6",
43
- "Programming Language :: Python",
44
- "Topic :: Internet",
45
- "Topic :: Multimedia :: Video",
46
- "Topic :: Software Development :: Libraries :: Python Modules",
47
- "Topic :: Terminals",
48
- "Topic :: Utilities",
49
  ],
50
- description=("A Python library for downloading YouTube videos."),
51
  long_description=readme,
52
  zip_safe=True,
53
 
 
12
  license = readme_file.read()
13
 
14
  setup(
15
+ name='pytube',
16
+ version='6.3.1',
17
+ author='Nick Ficano',
18
+ author_email='nficano@gmail.com',
19
  packages=['pytube'],
20
+ url='https://github.com/nficano/pytube',
21
  license=license,
22
  entry_points={
23
  'console_scripts': [
 
25
  ],
26
  },
27
  classifiers=[
28
+ 'Development Status :: 5 - Production/Stable',
29
+ 'Environment :: Console',
30
+ 'Intended Audience :: Developers',
31
+ 'License :: OSI Approved :: MIT License',
32
+ 'Natural Language :: English',
33
+ 'Operating System :: MacOS',
34
+ 'Operating System :: Microsoft',
35
+ 'Operating System :: POSIX',
36
+ 'Operating System :: Unix',
37
+ 'Programming Language :: Python :: 2.6',
38
+ 'Programming Language :: Python :: 2.7',
39
+ 'Programming Language :: Python :: 3.3',
40
+ 'Programming Language :: Python :: 3.4',
41
+ 'Programming Language :: Python :: 3.5',
42
+ 'Programming Language :: Python :: 3.6',
43
+ 'Programming Language :: Python',
44
+ 'Topic :: Internet',
45
+ 'Topic :: Multimedia :: Video',
46
+ 'Topic :: Software Development :: Libraries :: Python Modules',
47
+ 'Topic :: Terminals',
48
+ 'Topic :: Utilities',
49
  ],
50
+ description=('A Python library for downloading YouTube videos.'),
51
  long_description=readme,
52
  zip_safe=True,
53
 
tests/mock_data/youtube_age_restricted.html CHANGED
@@ -1,5 +1,5 @@
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
 
@@ -30,8 +30,8 @@ var m=["yt","www","masthead","sizing","runBeforeBodyIsReady"],n=this;m[0]in n||!
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
 
@@ -110,9 +110,9 @@ var m=["yt","www","masthead","sizing","runBeforeBodyIsReady"],n=this;m[0]in n||!
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>
@@ -142,7 +142,7 @@ Loading...
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>
@@ -185,11 +185,11 @@ Loading...
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">
@@ -234,7 +234,7 @@ Loading...
234
  </div>
235
  </div></div>
236
  <div id="player-playlist" class=" content-alignment watch-player-playlist ">
237
-
238
 
239
  </div>
240
 
@@ -301,7 +301,7 @@ Loading...
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)
@@ -327,9 +327,9 @@ Loading...
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>
@@ -520,7 +520,7 @@ Loading...
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>
@@ -584,8 +584,8 @@ Loading...
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">
@@ -613,7 +613,7 @@ Loading...
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>
@@ -869,7 +869,6 @@ Add to
869
  yt.setConfig('BLOCK_USER_AJAX_XSRF', 'QUFFLUhqbUVRNm9RcEJoTjBLRmduZE9RRi1fX25pdE4yQXxBQ3Jtc0tub1ZOMUJueXhodlJjVFdrMkU5aEdGa2JjVC1SaTVUZjBEWXRNUWxSdkdoV2ZTcXNCVmt3MnFTclljTExLNU02RmdnaU1Cd0szNlZ2MGlMSHNLUTB0YWVjWjdVWTFSczA1UnNJX3FQYW1GajRyc3J2T2Jpb1VFR28yZ0o0dzZCYVBVWkNuLTlaa0dmSFZKQ3BFaGoyMXpYOTgyUGc=');
870
 
871
 
872
-
873
 
874
 
875
 
@@ -877,7 +876,8 @@ Add to
877
 
878
 
879
 
880
-
 
881
 
882
  yt.setConfig({
883
  'GUIDED_HELP_LOCALE': "en_US",
@@ -927,4 +927,4 @@ ytcsi.setSpan('st', 113);yt.setConfig({'CSI_SERVICE_NAME': "youtube",'TIMING_ACT
927
  });
928
  yt.setConfig('THUMB_DELAY_LOAD_BUFFER', 0);
929
  if (window.ytcsi) {window.ytcsi.tick("jl", null, '');}</script>
930
- </body></html>
 
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
 
 
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
 
 
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>
 
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>
 
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">
 
234
  </div>
235
  </div></div>
236
  <div id="player-playlist" class=" content-alignment watch-player-playlist ">
237
+
238
 
239
  </div>
240
 
 
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)
 
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>
 
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>
 
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">
 
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>
 
869
  yt.setConfig('BLOCK_USER_AJAX_XSRF', 'QUFFLUhqbUVRNm9RcEJoTjBLRmduZE9RRi1fX25pdE4yQXxBQ3Jtc0tub1ZOMUJueXhodlJjVFdrMkU5aEdGa2JjVC1SaTVUZjBEWXRNUWxSdkdoV2ZTcXNCVmt3MnFTclljTExLNU02RmdnaU1Cd0szNlZ2MGlMSHNLUTB0YWVjWjdVWTFSczA1UnNJX3FQYW1GajRyc3J2T2Jpb1VFR28yZ0o0dzZCYVBVWkNuLTlaa0dmSFZKQ3BFaGoyMXpYOTgyUGc=');
870
 
871
 
 
872
 
873
 
874
 
 
876
 
877
 
878
 
879
+
880
+
881
 
882
  yt.setConfig({
883
  'GUIDED_HELP_LOCALE': "en_US",
 
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 CHANGED
@@ -1,5 +1,5 @@
1
  <!DOCTYPE html><html lang="en" data-cast-api-enabled="true"><head><style name="www-roboto">@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');}@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');}</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
 
@@ -27,7 +27,7 @@ var m=["yt","www","masthead","sizing","runBeforeBodyIsReady"],n=this;m[0]in n||!
27
  <link rel="stylesheet" href="//s.ytimg.com/yts/cssbin/www-player-new-vfliB0u8F.css" name="www-player">
28
 
29
  <link rel="stylesheet" href="//s.ytimg.com/yts/cssbin/www-pageframe-vfly1fQ8j.css" name="www-pageframe">
30
-
31
  <script>ytimg.preload("https:\/\/r5---sn-ab5l6nle.googlevideo.com\/crossdomain.xml");ytimg.preload("https:\/\/r5---sn-ab5l6nle.googlevideo.com\/generate_204");</script>
32
 
33
 
@@ -109,9 +109,9 @@ var m=["yt","www","masthead","sizing","runBeforeBodyIsReady"],n=this;m[0]in n||!
109
  <meta name="twitter:player:width" content="1280">
110
  <meta name="twitter:player:height" content="720">
111
 
112
- <meta name=attribution content=ygent/>
113
  <style>li.mc-channel-permission-present { width: 100%; } .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">
114
- <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">
115
  <div id="yt-masthead-container" class="clearfix yt-base-gutter"> <button id="a11y-skip-nav" class="skip-nav" data-target-id="main" tabindex="3">
116
  Skip navigation
117
  </button>
@@ -141,7 +141,7 @@ Loading...
141
  </div>
142
  </div>
143
 
144
- </div><div class="alerts-wrapper"><div id="alerts" class="content-alignment">
145
  <div id="editor-progress-alert-container"></div>
146
  <div class="yt-alert yt-alert-default yt-alert-warn hid " id="editor-progress-alert-template"> <div class="yt-alert-icon">
147
  <span class="icon master-sprite yt-sprite"></span>
@@ -181,8 +181,8 @@ Loading...
181
 
182
 
183
  <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">
184
-
185
-
186
  <button 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-expanded="false" aria-label="Actions for the queue" aria-haspopup="true" onclick=";return false;" ><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 class="watch-queue-menu-choice overflow-menu-choice yt-uix-button-menu-item" data-action="remove-all" onclick=";return false;" >Remove all</span></li><li role="menuitem"><span class="watch-queue-menu-choice overflow-menu-choice yt-uix-button-menu-item" data-action="disconnect" onclick=";return false;" >Disconnect</span></li></ul></button>
187
  </span>
188
  <div class="watch-queue-controls">
@@ -227,7 +227,7 @@ Loading...
227
  </div>
228
  </div></div>
229
  <div id="player-playlist" class=" content-alignment watch-player-playlist ">
230
-
231
 
232
  <div id="watch-appbar-playlist" class="watch-playlist player-height">
233
  <div class="main-content">
@@ -235,7 +235,7 @@ Loading...
235
  <div class="playlist-header-content" data-loop-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-initial-loop-state="false" data-list-author="" data-full-list-id="PLirAqAtl_h2r5g8xGajEwdXd3x1sZh8hC" data-shuffle-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-normal-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-loop_shuffle-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-shareable="True" data-list-title="Most Viewed Videos of All Time・(Over 100 million views)">
236
  <div class="appbar-playlist-controls clearfix">
237
 
238
-
239
 
240
  <span class="yt-uix-clickcard">
241
  <span class="yt-uix-clickcard-target" data-position="bottomright" data-orientation="vertical">
@@ -244,7 +244,7 @@ Loading...
244
  <div class="signin-clickcard yt-uix-clickcard-content">
245
  <h3 class="signin-clickcard-header">Sign in to YouTube</h3>
246
  <div class="signin-clickcard-message">
247
-
248
  </div>
249
  <a href="https://accounts.google.com/ServiceLogin?passive=true&amp;continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26feature%3D__FEATURE__%26next%3D%252Fwatch%253Fv%253D9bZkp7q19f0%2526list%253DPLirAqAtl_h2r5g8xGajEwdXd3x1sZh8hC%2526index%253D1%26hl%3Den&amp;hl=en&amp;uilel=3&amp;service=youtube" class="yt-uix-button signin-button yt-uix-sessionlink yt-uix-button-primary yt-uix-button-size-default" data-sessionlink="ei=4NURVo25LIbK-AX7mK_oDA"><span class="yt-uix-button-content">Sign in</span></a>
250
  </div>
@@ -5762,7 +5762,7 @@ Loading...
5762
  <div id="watch7-headline" class="clearfix">
5763
  <div id="watch-headline-title">
5764
  <h1 class="yt watch-title-container" >
5765
-
5766
 
5767
  <span id="eow-title" class="watch-title " dir="ltr" title="PSY - GANGNAM STYLE(강남스타일) M/V">
5768
  PSY - GANGNAM STYLE(강남스타일) M/V
@@ -5787,12 +5787,12 @@ Loading...
5787
  </a>
5788
  <div class="yt-user-info">
5789
  <a href="/channel/UCrDkAvwZum-UTjHmzDI2iIw" class="yt-uix-sessionlink g-hovercard spf-link " data-ytid="UCrDkAvwZum-UTjHmzDI2iIw" data-sessionlink="itct=CPsBEOE5IhMIjamRrpqqyAIVBiW-Ch17zAvNKPgd" >officialpsy</a>
5790
-
5791
  <span aria-label="Verified" class="yt-channel-title-icon-verified yt-uix-tooltip yt-sprite" data-tooltip-text="Verified"></span>
5792
  </div>
5793
- <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-channel-external-id="UCrDkAvwZum-UTjHmzDI2iIw" data-href="https://accounts.google.com/ServiceLogin?passive=true&amp;continue=http%3A%2F%2Fwww.youtube.com%2Fsignin%3Fapp%3Ddesktop%26feature%3Dsubscribe%26action_handle_signin%3Dtrue%26next%3D%252Fchannel%252FUCrDkAvwZum-UTjHmzDI2iIw%26continue_action%3DQUFFLUhqbTVxeGwyWktCZGRuQlNjczRTTW9OT3Y4aE85QXxBQ3Jtc0ttZGJCOW1URzZoLUQ1YTR1R2NIR09NcTN3NmJKeXU4SnAwS3FSeU9VWkNkSVpTc185cU91dklGejNoU0hHbWVwOEJEQzlsYnBDMDRiZFcxTTV4SGVxQnByY2xXV2lUWjV4SjBPNFhPNjBZMV85OFlRSHl6aU1IRkxUSkxwdGF2NElDa3ktNVhELVZqZmU5dTFOaFBDT1hCLU9OanFXV2JLWkFvcjhWT09TU05kYm9MNnhNZ3BnUmlJd1JfUDVCU09jUG5CdTh0RFR1TG1JeGFDdWFxcEJPQ0JudEJR%26hl%3Den&amp;hl=en&amp;uilel=3&amp;service=youtube" data-style-type="branded" data-clicktracking="itct=CPwBEJsrIhMIjamRrpqqyAIVBiW-Ch17zAvNKPgdMgV3YXRjaA"><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-label="Subscription preferences" aria-live="polite" aria-busy="false" data-channel-external-id="UCrDkAvwZum-UTjHmzDI2iIw"><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="8,185,162" aria-label="8,185,162" tabindex="0">8,185,162</span><span class="yt-subscription-button-subscriber-count-branded-horizontal yt-short-subscriber-count" title="8M" aria-label="8M" tabindex="0">8M</span>
5794
  <div class="yt-uix-overlay " data-overlay-style="primary" data-overlay-shape="tiny">
5795
-
5796
  <div class="yt-dialog hid ">
5797
  <div class="yt-dialog-base">
5798
  <span class="yt-dialog-align"></span>
@@ -5983,7 +5983,7 @@ Loading...
5983
  </div>
5984
  </div>
5985
 
5986
-
5987
  <div id="action-panel-rental-required" class="action-panel-content hid">
5988
  <div id="watch-actions-rental-required">
5989
  <strong>Rating is available when the video has been rented.</strong>
@@ -6038,8 +6038,8 @@ Loading...
6038
  <div class="cmt_iframe_holder" data-href="http://www.youtube.com/watch?v=9bZkp7q19f0&amp;list=PLirAqAtl_h2r5g8xGajEwdXd3x1sZh8hC&amp;index=1" data-viewtype="FILTERED" style="display: none;"></div>
6039
 
6040
  <div id="watch-discussion" class="branded-page-box yt-card">
6041
-
6042
-
6043
  <div class="comments-iframe-container">
6044
  <div id="comments-test-iframe"></div>
6045
  <div id="distiller-spinner" class="action-panel-loading">
@@ -6066,7 +6066,7 @@ Loading...
6066
 
6067
  <div id="watch7-sidebar-contents" class="watch-sidebar-gutter yt-card yt-card-has-padding yt-uix-expander yt-uix-expander-collapsed">
6068
  <div id="watch7-sidebar-offer">
6069
-
6070
  </div>
6071
 
6072
  <div id="watch7-sidebar-ads">
@@ -6081,13 +6081,13 @@ Advertisement
6081
 
6082
  </div>
6083
  <div id="watch7-sidebar-modules">
6084
-
6085
  <div class="watch-sidebar-section">
6086
  <div class="watch-sidebar-body">
6087
  <ul id="watch-related" class="video-list">
6088
  <li class="video-list-item related-list-item show-video-time related-list-item-compact-radio"><a href="/watch?v=9bZkp7q19f0&amp;list=RD9bZkp7q19f0" class="yt-uix-sessionlink related-playlist yt-pl-thumb-link spf-link mix-playlist resumable-list spf-link " data-sessionlink="itct=CPQBEKMwGAAiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCmxpc3Rfb3RoZXJI_evX1fuUmdv1AQ" data-secondary-video-url="/watch?v=ASO_zypdnsQ&amp;list=RD9bZkp7q19f0" rel="spf-prefetch">
6089
  <span class="yt-pl-thumb is-small yt-mix-thumb">
6090
-
6091
  <span class="video-thumb yt-thumb yt-thumb-120"
6092
  >
6093
  <span class="yt-thumb-default">
@@ -6878,7 +6878,6 @@ Add to
6878
  yt.setConfig('BLOCK_USER_AJAX_XSRF', 'QUFFLUhqbUdSQlg1RlhVR25uSkotc21PX2xva2trNEx0Z3xBQ3Jtc0trdGgyeFFGUUR2Vzhua0xNVW9sMWNUOGVFTXBXRGxrTFVHOFktYzV4T2xUcDNzU2RkUngxSWl6cEwyN0lNM1pacXVYdzBWMXVEWXNlRjFQRVd0QkJFaFliV0J0OHMzOXJtWFFnWmR3cGpNdUtFWEFybmpFNVZvNUdMaktQNjc3U2hkbmhVMWg1QjlCNmJSem5jRWYySTRIY2tldFE=');
6879
 
6880
 
6881
-
6882
 
6883
 
6884
 
@@ -6886,7 +6885,8 @@ Add to
6886
 
6887
 
6888
 
6889
-
 
6890
 
6891
  yt.setConfig({
6892
  'GUIDED_HELP_LOCALE': "en_US",
@@ -6936,4 +6936,4 @@ ytcsi.setSpan('st', 783);yt.setConfig({'CSI_SERVICE_NAME': "youtube",'TIMING_ACT
6936
  });
6937
  yt.setConfig('THUMB_DELAY_LOAD_BUFFER', 0);
6938
  if (window.ytcsi) {window.ytcsi.tick("jl", null, '');}</script>
6939
- </body></html>
 
1
  <!DOCTYPE html><html lang="en" data-cast-api-enabled="true"><head><style name="www-roboto">@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');}@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');}</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
 
 
27
  <link rel="stylesheet" href="//s.ytimg.com/yts/cssbin/www-player-new-vfliB0u8F.css" name="www-player">
28
 
29
  <link rel="stylesheet" href="//s.ytimg.com/yts/cssbin/www-pageframe-vfly1fQ8j.css" name="www-pageframe">
30
+
31
  <script>ytimg.preload("https:\/\/r5---sn-ab5l6nle.googlevideo.com\/crossdomain.xml");ytimg.preload("https:\/\/r5---sn-ab5l6nle.googlevideo.com\/generate_204");</script>
32
 
33
 
 
109
  <meta name="twitter:player:width" content="1280">
110
  <meta name="twitter:player:height" content="720">
111
 
112
+ <meta name=attribution content=ygent/>
113
  <style>li.mc-channel-permission-present { width: 100%; } .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">
114
+ <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">
115
  <div id="yt-masthead-container" class="clearfix yt-base-gutter"> <button id="a11y-skip-nav" class="skip-nav" data-target-id="main" tabindex="3">
116
  Skip navigation
117
  </button>
 
141
  </div>
142
  </div>
143
 
144
+ </div><div class="alerts-wrapper"><div id="alerts" class="content-alignment">
145
  <div id="editor-progress-alert-container"></div>
146
  <div class="yt-alert yt-alert-default yt-alert-warn hid " id="editor-progress-alert-template"> <div class="yt-alert-icon">
147
  <span class="icon master-sprite yt-sprite"></span>
 
181
 
182
 
183
  <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">
184
+
185
+
186
  <button 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-expanded="false" aria-label="Actions for the queue" aria-haspopup="true" onclick=";return false;" ><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 class="watch-queue-menu-choice overflow-menu-choice yt-uix-button-menu-item" data-action="remove-all" onclick=";return false;" >Remove all</span></li><li role="menuitem"><span class="watch-queue-menu-choice overflow-menu-choice yt-uix-button-menu-item" data-action="disconnect" onclick=";return false;" >Disconnect</span></li></ul></button>
187
  </span>
188
  <div class="watch-queue-controls">
 
227
  </div>
228
  </div></div>
229
  <div id="player-playlist" class=" content-alignment watch-player-playlist ">
230
+
231
 
232
  <div id="watch-appbar-playlist" class="watch-playlist player-height">
233
  <div class="main-content">
 
235
  <div class="playlist-header-content" data-loop-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-initial-loop-state="false" data-list-author="" data-full-list-id="PLirAqAtl_h2r5g8xGajEwdXd3x1sZh8hC" data-shuffle-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-normal-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-loop_shuffle-auto-clicktracking="CBUQ0TkiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCGF1dG9wbGF5" data-shareable="True" data-list-title="Most Viewed Videos of All Time・(Over 100 million views)">
236
  <div class="appbar-playlist-controls clearfix">
237
 
238
+
239
 
240
  <span class="yt-uix-clickcard">
241
  <span class="yt-uix-clickcard-target" data-position="bottomright" data-orientation="vertical">
 
244
  <div class="signin-clickcard yt-uix-clickcard-content">
245
  <h3 class="signin-clickcard-header">Sign in to YouTube</h3>
246
  <div class="signin-clickcard-message">
247
+
248
  </div>
249
  <a href="https://accounts.google.com/ServiceLogin?passive=true&amp;continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26feature%3D__FEATURE__%26next%3D%252Fwatch%253Fv%253D9bZkp7q19f0%2526list%253DPLirAqAtl_h2r5g8xGajEwdXd3x1sZh8hC%2526index%253D1%26hl%3Den&amp;hl=en&amp;uilel=3&amp;service=youtube" class="yt-uix-button signin-button yt-uix-sessionlink yt-uix-button-primary yt-uix-button-size-default" data-sessionlink="ei=4NURVo25LIbK-AX7mK_oDA"><span class="yt-uix-button-content">Sign in</span></a>
250
  </div>
 
5762
  <div id="watch7-headline" class="clearfix">
5763
  <div id="watch-headline-title">
5764
  <h1 class="yt watch-title-container" >
5765
+
5766
 
5767
  <span id="eow-title" class="watch-title " dir="ltr" title="PSY - GANGNAM STYLE(강남스타일) M/V">
5768
  PSY - GANGNAM STYLE(강남스타일) M/V
 
5787
  </a>
5788
  <div class="yt-user-info">
5789
  <a href="/channel/UCrDkAvwZum-UTjHmzDI2iIw" class="yt-uix-sessionlink g-hovercard spf-link " data-ytid="UCrDkAvwZum-UTjHmzDI2iIw" data-sessionlink="itct=CPsBEOE5IhMIjamRrpqqyAIVBiW-Ch17zAvNKPgd" >officialpsy</a>
5790
+
5791
  <span aria-label="Verified" class="yt-channel-title-icon-verified yt-uix-tooltip yt-sprite" data-tooltip-text="Verified"></span>
5792
  </div>
5793
+ <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-channel-external-id="UCrDkAvwZum-UTjHmzDI2iIw" data-href="https://accounts.google.com/ServiceLogin?passive=true&amp;continue=http%3A%2F%2Fwww.youtube.com%2Fsignin%3Fapp%3Ddesktop%26feature%3Dsubscribe%26action_handle_signin%3Dtrue%26next%3D%252Fchannel%252FUCrDkAvwZum-UTjHmzDI2iIw%26continue_action%3DQUFFLUhqbTVxeGwyWktCZGRuQlNjczRTTW9OT3Y4aE85QXxBQ3Jtc0ttZGJCOW1URzZoLUQ1YTR1R2NIR09NcTN3NmJKeXU4SnAwS3FSeU9VWkNkSVpTc185cU91dklGejNoU0hHbWVwOEJEQzlsYnBDMDRiZFcxTTV4SGVxQnByY2xXV2lUWjV4SjBPNFhPNjBZMV85OFlRSHl6aU1IRkxUSkxwdGF2NElDa3ktNVhELVZqZmU5dTFOaFBDT1hCLU9OanFXV2JLWkFvcjhWT09TU05kYm9MNnhNZ3BnUmlJd1JfUDVCU09jUG5CdTh0RFR1TG1JeGFDdWFxcEJPQ0JudEJR%26hl%3Den&amp;hl=en&amp;uilel=3&amp;service=youtube" data-style-type="branded" data-clicktracking="itct=CPwBEJsrIhMIjamRrpqqyAIVBiW-Ch17zAvNKPgdMgV3YXRjaA"><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-label="Subscription preferences" aria-live="polite" aria-busy="false" data-channel-external-id="UCrDkAvwZum-UTjHmzDI2iIw"><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="8,185,162" aria-label="8,185,162" tabindex="0">8,185,162</span><span class="yt-subscription-button-subscriber-count-branded-horizontal yt-short-subscriber-count" title="8M" aria-label="8M" tabindex="0">8M</span>
5794
  <div class="yt-uix-overlay " data-overlay-style="primary" data-overlay-shape="tiny">
5795
+
5796
  <div class="yt-dialog hid ">
5797
  <div class="yt-dialog-base">
5798
  <span class="yt-dialog-align"></span>
 
5983
  </div>
5984
  </div>
5985
 
5986
+
5987
  <div id="action-panel-rental-required" class="action-panel-content hid">
5988
  <div id="watch-actions-rental-required">
5989
  <strong>Rating is available when the video has been rented.</strong>
 
6038
  <div class="cmt_iframe_holder" data-href="http://www.youtube.com/watch?v=9bZkp7q19f0&amp;list=PLirAqAtl_h2r5g8xGajEwdXd3x1sZh8hC&amp;index=1" data-viewtype="FILTERED" style="display: none;"></div>
6039
 
6040
  <div id="watch-discussion" class="branded-page-box yt-card">
6041
+
6042
+
6043
  <div class="comments-iframe-container">
6044
  <div id="comments-test-iframe"></div>
6045
  <div id="distiller-spinner" class="action-panel-loading">
 
6066
 
6067
  <div id="watch7-sidebar-contents" class="watch-sidebar-gutter yt-card yt-card-has-padding yt-uix-expander yt-uix-expander-collapsed">
6068
  <div id="watch7-sidebar-offer">
6069
+
6070
  </div>
6071
 
6072
  <div id="watch7-sidebar-ads">
 
6081
 
6082
  </div>
6083
  <div id="watch7-sidebar-modules">
6084
+
6085
  <div class="watch-sidebar-section">
6086
  <div class="watch-sidebar-body">
6087
  <ul id="watch-related" class="video-list">
6088
  <li class="video-list-item related-list-item show-video-time related-list-item-compact-radio"><a href="/watch?v=9bZkp7q19f0&amp;list=RD9bZkp7q19f0" class="yt-uix-sessionlink related-playlist yt-pl-thumb-link spf-link mix-playlist resumable-list spf-link " data-sessionlink="itct=CPQBEKMwGAAiEwiNqZGumqrIAhUGJb4KHXvMC80o-B0yCmxpc3Rfb3RoZXJI_evX1fuUmdv1AQ" data-secondary-video-url="/watch?v=ASO_zypdnsQ&amp;list=RD9bZkp7q19f0" rel="spf-prefetch">
6089
  <span class="yt-pl-thumb is-small yt-mix-thumb">
6090
+
6091
  <span class="video-thumb yt-thumb yt-thumb-120"
6092
  >
6093
  <span class="yt-thumb-default">
 
6878
  yt.setConfig('BLOCK_USER_AJAX_XSRF', 'QUFFLUhqbUdSQlg1RlhVR25uSkotc21PX2xva2trNEx0Z3xBQ3Jtc0trdGgyeFFGUUR2Vzhua0xNVW9sMWNUOGVFTXBXRGxrTFVHOFktYzV4T2xUcDNzU2RkUngxSWl6cEwyN0lNM1pacXVYdzBWMXVEWXNlRjFQRVd0QkJFaFliV0J0OHMzOXJtWFFnWmR3cGpNdUtFWEFybmpFNVZvNUdMaktQNjc3U2hkbmhVMWg1QjlCNmJSem5jRWYySTRIY2tldFE=');
6879
 
6880
 
 
6881
 
6882
 
6883
 
 
6885
 
6886
 
6887
 
6888
+
6889
+
6890
 
6891
  yt.setConfig({
6892
  'GUIDED_HELP_LOCALE': "en_US",
 
6936
  });
6937
  yt.setConfig('THUMB_DELAY_LOAD_BUFFER', 0);
6938
  if (window.ytcsi) {window.ytcsi.tick("jl", null, '');}</script>
6939
+ </body></html>
tests/requirements.txt CHANGED
@@ -1,4 +1,5 @@
 
 
1
  mock==1.3.0
2
  nose==1.3.7
3
- coveralls==1.0
4
- bumpversion==0.5.3
 
1
+ bumpversion==0.5.3
2
+ coveralls==1.0
3
  mock==1.3.0
4
  nose==1.3.7
5
+ pre-commit==0.15.0
 
tests/test_p3_pytube.py DELETED
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- from __future__ import unicode_literals
4
- import warnings
5
- import mock
6
- from nose.tools import eq_, raises
7
- from pytube import api
8
- from pytube.exceptions import MultipleObjectsReturned, AgeRestricted, \
9
- DoesNotExist, PytubeError
10
-
11
-
12
- class TestPytube(object):
13
- def setUp(self):
14
- self.url = 'http://www.youtube.com/watch?v=9bZkp7q19f0'
15
-
16
- def test_YT_create_from_url(self):
17
- 'test creation of YouYube Object from url'
18
- yt = api.YouTube(self.url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_pytube.py CHANGED
@@ -1,12 +1,18 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import unicode_literals
 
4
  import warnings
 
5
  import mock
6
- from nose.tools import eq_, raises
 
 
7
  from pytube import api
8
- from pytube.exceptions import MultipleObjectsReturned, AgeRestricted, \
9
- DoesNotExist, PytubeError
 
 
10
 
11
 
12
  class TestPytube(object):
@@ -99,7 +105,7 @@ class TestPytube(object):
99
  """Deprecation warnings get triggered on url set"""
100
  with warnings.catch_warnings(record=True) as w:
101
  # Cause all warnings to always be triggered.
102
- warnings.simplefilter("always")
103
  with mock.patch('pytube.api.urlopen') as urlopen:
104
  urlopen.return_value.read.return_value = self.mock_html
105
  yt = api.YouTube()
@@ -111,7 +117,7 @@ class TestPytube(object):
111
  """Deprecation warnings get triggered on filename set"""
112
  with warnings.catch_warnings(record=True) as w:
113
  # Cause all warnings to always be triggered.
114
- warnings.simplefilter("always")
115
  self.yt.filename = 'Gangnam Style'
116
  eq_(len(w), 1)
117
 
@@ -119,7 +125,7 @@ class TestPytube(object):
119
  """Deprecation warnings get triggered on video getter"""
120
  with warnings.catch_warnings(record=True) as w:
121
  # Cause all warnings to always be triggered.
122
- warnings.simplefilter("always")
123
  self.yt.videos
124
  eq_(len(w), 1)
125
 
@@ -132,9 +138,3 @@ class TestPytube(object):
132
  def test_get_json_offset_with_bad_html(self):
133
  """Raise exception if json offset cannot be found"""
134
  self.yt._get_json_offset('asdfasdf')
135
-
136
- def test_YT_create_from_url(self):
137
- 'test creation of YouYube Object from url'
138
- url = 'http://www.youtube.com/watch?v=9bZkp7q19f0'
139
-
140
- yt = api.YouTube(url)
 
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import unicode_literals
4
+
5
  import warnings
6
+
7
  import mock
8
+ from nose.tools import eq_
9
+ from nose.tools import raises
10
+
11
  from pytube import api
12
+ from pytube.exceptions import AgeRestricted
13
+ from pytube.exceptions import DoesNotExist
14
+ from pytube.exceptions import MultipleObjectsReturned
15
+ from pytube.exceptions import PytubeError
16
 
17
 
18
  class TestPytube(object):
 
105
  """Deprecation warnings get triggered on url set"""
106
  with warnings.catch_warnings(record=True) as w:
107
  # Cause all warnings to always be triggered.
108
+ warnings.simplefilter('always')
109
  with mock.patch('pytube.api.urlopen') as urlopen:
110
  urlopen.return_value.read.return_value = self.mock_html
111
  yt = api.YouTube()
 
117
  """Deprecation warnings get triggered on filename set"""
118
  with warnings.catch_warnings(record=True) as w:
119
  # Cause all warnings to always be triggered.
120
+ warnings.simplefilter('always')
121
  self.yt.filename = 'Gangnam Style'
122
  eq_(len(w), 1)
123
 
 
125
  """Deprecation warnings get triggered on video getter"""
126
  with warnings.catch_warnings(record=True) as w:
127
  # Cause all warnings to always be triggered.
128
+ warnings.simplefilter('always')
129
  self.yt.videos
130
  eq_(len(w), 1)
131
 
 
138
  def test_get_json_offset_with_bad_html(self):
139
  """Raise exception if json offset cannot be found"""
140
  self.yt._get_json_offset('asdfasdf')
 
 
 
 
 
 
tests/test_utils.py CHANGED
@@ -1,12 +1,14 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import unicode_literals
 
4
  from nose.tools import eq_
 
5
  from pytube import utils
6
 
7
 
8
  class TestUtils(object):
9
- blob = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
10
 
11
  def test_truncate(self):
12
  """Truncate string works as expected"""
@@ -15,10 +17,10 @@ class TestUtils(object):
15
 
16
  def test_safe_filename(self):
17
  """Unsafe characters get stripped from generated filename"""
18
- eq_(utils.safe_filename("abc1245$$"), "abc1245")
19
- eq_(utils.safe_filename("abc##"), "abc")
20
- eq_(utils.safe_filename("abc:foo"), "abc -foo")
21
- eq_(utils.safe_filename("abc_foo"), "abc foo")
22
 
23
  def test_sizeof(self):
24
  """Accurately converts the bytes to its humanized equivalent"""
 
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  from __future__ import unicode_literals
4
+
5
  from nose.tools import eq_
6
+
7
  from pytube import utils
8
 
9
 
10
  class TestUtils(object):
11
+ blob = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
12
 
13
  def test_truncate(self):
14
  """Truncate string works as expected"""
 
17
 
18
  def test_safe_filename(self):
19
  """Unsafe characters get stripped from generated filename"""
20
+ eq_(utils.safe_filename('abc1245$$'), 'abc1245')
21
+ eq_(utils.safe_filename('abc##'), 'abc')
22
+ eq_(utils.safe_filename('abc:foo'), 'abc -foo')
23
+ eq_(utils.safe_filename('abc_foo'), 'abc foo')
24
 
25
  def test_sizeof(self):
26
  """Accurately converts the bytes to its humanized equivalent"""