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