Skip to content

Commit 51d3253

Browse files
committed
new version of pytube
1 parent bbd29d9 commit 51d3253

28 files changed

+224
-36
lines changed

pytube/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Pytube: a very serious Python library for downloading YouTube Videos.
66
"""
77
__title__ = 'pytube'
8-
__version__ = '9.2.2'
8+
__version__ = '9.2.3'
99
__author__ = 'Nick Ficano'
1010
__license__ = 'MIT License'
1111
__copyright__ = 'Copyright 2017 Nick Ficano'

pytube/__init__.pyc

5 Bytes
Binary file not shown.

pytube/__main__.py

+40
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from pytube import StreamQuery
2222
from pytube.compat import install_proxy
2323
from pytube.compat import parse_qsl
24+
from pytube.exceptions import VideoUnavailable
2425
from pytube.helpers import apply_mixin
2526

2627
logger = logging.getLogger(__name__)
@@ -116,6 +117,7 @@ def init(self):
116117
self.watch_html,
117118
)['args']
118119

120+
self.vid_descr = extract.get_vid_descr(self.watch_html)
119121
# https://github.com/nficano/pytube/issues/165
120122
stream_maps = ['url_encoded_fmt_stream_map']
121123
if 'adaptive_fmts' in self.player_config_args:
@@ -156,6 +158,8 @@ def prefetch(self):
156158
157159
"""
158160
self.watch_html = request.get(url=self.watch_url)
161+
if '<img class="icon meh" src="/yts/img' not in self.watch_html:
162+
raise VideoUnavailable('This video is unavailable.')
159163
self.embed_html = request.get(url=self.embed_url)
160164
self.age_restricted = extract.is_age_restricted(self.watch_html)
161165
self.vid_info_url = extract.video_info_url(
@@ -249,6 +253,42 @@ def title(self):
249253
"""
250254
return self.player_config_args['title']
251255

256+
@property
257+
def description(self):
258+
"""Get the video description.
259+
260+
:rtype: str
261+
262+
"""
263+
return self.vid_descr
264+
265+
@property
266+
def rating(self):
267+
"""Get the video average rating.
268+
269+
:rtype: str
270+
271+
"""
272+
return self.player_config_args['avg_rating']
273+
274+
@property
275+
def length(self):
276+
"""Get the video length in seconds.
277+
278+
:rtype: str
279+
280+
"""
281+
return self.player_config_args['length_seconds']
282+
283+
@property
284+
def views(self):
285+
"""Get the number of the times the video has been viewed.
286+
287+
:rtype: str
288+
289+
"""
290+
return self.player_config_args['view_count']
291+
252292
def register_on_progress_callback(self, func):
253293
"""Register a download progress callback function post initialization.
254294

pytube/__main__.pyc

1.29 KB
Binary file not shown.

pytube/captions.pyc

40 Bytes
Binary file not shown.

pytube/cipher.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ def get_initial_function_name(js):
3535
3636
"""
3737
# c&&d.set("signature", EE(c));
38-
pattern = r'"signature",\s?([a-zA-Z0-9$]+)\('
38+
pattern = [
39+
r'yt\.akamaized\.net/\)\s*\|\|\s*'
40+
r'.*?\s*c\s*&&\s*d\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
41+
r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
42+
r'\bc\s*&&\s*d\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
43+
]
3944
logger.debug('finding initial function name')
4045
return regex_search(pattern, js, group=1)
4146

pytube/cipher.pyc

224 Bytes
Binary file not shown.

pytube/cli.pyc

40 Bytes
Binary file not shown.

pytube/compat.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
"""Python 2/3 compatibility support."""
55
import sys
66

7+
78
PY2 = sys.version_info[0] == 2
89
PY3 = sys.version_info[0] == 3
910
PY33 = sys.version_info[0:2] >= (3, 3)
1011

11-
1212
if PY2:
13+
reload(sys)
14+
sys.setdefaultencoding('utf8')
1315
import urllib2
1416
from urllib import urlencode
1517
from urllib2 import URLError

pytube/compat.pyc

105 Bytes
Binary file not shown.

pytube/contrib/__init__.pyc

5 Bytes
Binary file not shown.

pytube/contrib/playlist.py

+45-4
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,67 @@ def populate_video_urls(self):
6666
complete_url = base_url + video_id
6767
self.video_urls.append(complete_url)
6868

69-
def download_all(self, download_path=None):
69+
def _path_num_prefix_generator(self, reverse=False):
70+
"""
71+
This generator function generates number prefixes, for the items
72+
in the playlist.
73+
If the number of digits required to name a file,is less than is
74+
required to name the last file,it prepends 0s.
75+
So if you have a playlist of 100 videos it will number them like:
76+
001, 002, 003 ect, up to 100.
77+
It also adds a space after the number.
78+
:return: prefix string generator : generator
79+
"""
80+
digits = len(str(len(self.video_urls)))
81+
if reverse:
82+
start, stop, step = (len(self.video_urls), 0, -1)
83+
else:
84+
start, stop, step = (1, len(self.video_urls) + 1, 1)
85+
return (str(i).zfill(digits) for i in range(start, stop, step))
86+
87+
def download_all(
88+
self, download_path=None, prefix_number=True,
89+
reverse_numbering=False,
90+
):
7091
"""Download all the videos in the the playlist. Initially, download
7192
resolution is 720p (or highest available), later more option
7293
should be added to download resolution of choice
7394
7495
TODO(nficano): Add option to download resolution of user's choice
96+
97+
:param download_path:
98+
(optional) Output path for the playlist If one is not
99+
specified, defaults to the current working directory.
100+
This is passed along to the Stream objects.
101+
:type download_path: str or None
102+
:param prefix_number:
103+
(optional) Automatically numbers playlists using the
104+
_path_num_prefix_generator function.
105+
:type prefix_number: bool
106+
:param reverse_numbering:
107+
(optional) Lets you number playlists in reverse, since some
108+
playlists are ordered newest -> oldests.
109+
:type reverse_numbering: bool
75110
"""
76111

77112
self.populate_video_urls()
78-
logger.debug('total videos found: ', len(self.video_urls))
113+
logger.debug('total videos found: %d', len(self.video_urls))
79114
logger.debug('starting download')
80115

116+
prefix_gen = self._path_num_prefix_generator(reverse_numbering)
117+
81118
for link in self.video_urls:
82119
yt = YouTube(link)
83-
84120
# TODO: this should not be hardcoded to a single user's preference
85121
dl_stream = yt.streams.filter(
86122
progressive=True, subtype='mp4',
87123
).order_by('resolution').desc().first()
88124

89125
logger.debug('download path: %s', download_path)
90-
dl_stream.download(download_path)
126+
if prefix_number:
127+
prefix = next(prefix_gen)
128+
logger.debug('file prefix is: %s', prefix)
129+
dl_stream.download(download_path, filename_prefix=prefix)
130+
else:
131+
dl_stream.download(download_path)
91132
logger.debug('download complete')

pytube/contrib/playlist.pyc

1.92 KB
Binary file not shown.

pytube/exceptions.py

+8
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,11 @@ def __init__(self, msg, video_id=None):
3434

3535
class RegexMatchError(ExtractError):
3636
"""Regex pattern did not return any matches."""
37+
38+
39+
class LiveStreamError(ExtractError):
40+
"""Video is a live stream."""
41+
42+
43+
class VideoUnavailable(PytubeError):
44+
"""Video is unavailable."""

pytube/exceptions.pyc

480 Bytes
Binary file not shown.

pytube/extract.py

+26
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,32 @@
33
import json
44
from collections import OrderedDict
55

6+
from pytube.compat import HTMLParser
67
from pytube.compat import quote
78
from pytube.compat import urlencode
89
from pytube.exceptions import RegexMatchError
910
from pytube.helpers import regex_search
1011

1112

13+
class PytubeHTMLParser(HTMLParser):
14+
in_vid_descr = False
15+
vid_descr = ''
16+
17+
def handle_starttag(self, tag, attrs):
18+
if tag == 'p':
19+
for attr in attrs:
20+
if attr[0] == 'id' and attr[1] == 'eow-description':
21+
self.in_vid_descr = True
22+
23+
def handle_endtag(self, tag):
24+
if tag == 'p' and self.in_vid_descr:
25+
self.in_vid_descr = False
26+
27+
def handle_data(self, data):
28+
if self.in_vid_descr:
29+
self.vid_descr += data
30+
31+
1232
def is_age_restricted(watch_html):
1333
"""Check if content is age restricted.
1434
@@ -173,3 +193,9 @@ def get_ytplayer_config(html, age_restricted=False):
173193
pattern = r';ytplayer\.config\s*=\s*({.*?});'
174194
yt_player_config = regex_search(pattern, html, group=1)
175195
return json.loads(yt_player_config)
196+
197+
198+
def get_vid_descr(html):
199+
html_parser = PytubeHTMLParser()
200+
html_parser.feed(html)
201+
return html_parser.vid_descr

pytube/extract.pyc

1.29 KB
Binary file not shown.

pytube/helpers.py

+47-21
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,55 @@ def regex_search(pattern, string, groups=False, group=None, flags=0):
3131
:returns:
3232
Substring pattern matches.
3333
"""
34-
regex = re.compile(pattern, flags)
35-
results = regex.search(string)
36-
if not results:
37-
raise RegexMatchError(
38-
'regex pattern ({pattern}) had zero matches'
39-
.format(pattern=pattern),
40-
)
34+
if type(pattern) == list:
35+
for p in pattern:
36+
regex = re.compile(p, flags)
37+
results = regex.search(string)
38+
if not results:
39+
raise RegexMatchError(
40+
'regex pattern ({pattern}) had zero matches'
41+
.format(pattern=p),
42+
)
43+
else:
44+
logger.debug(
45+
'finished regex search: %s',
46+
pprint.pformat(
47+
{
48+
'pattern': p,
49+
'results': results.group(0),
50+
}, indent=2,
51+
),
52+
)
53+
if groups:
54+
return results.groups()
55+
elif group is not None:
56+
return results.group(group)
57+
else:
58+
return results
4159
else:
42-
logger.debug(
43-
'finished regex search: %s',
44-
pprint.pformat(
45-
{
46-
'pattern': pattern,
47-
'results': results.group(0),
48-
}, indent=2,
49-
),
50-
)
51-
if groups:
52-
return results.groups()
53-
elif group is not None:
54-
return results.group(group)
60+
regex = re.compile(pattern, flags)
61+
results = regex.search(string)
62+
if not results:
63+
raise RegexMatchError(
64+
'regex pattern ({pattern}) had zero matches'
65+
.format(pattern=pattern),
66+
)
5567
else:
56-
return results
68+
logger.debug(
69+
'finished regex search: %s',
70+
pprint.pformat(
71+
{
72+
'pattern': pattern,
73+
'results': results.group(0),
74+
}, indent=2,
75+
),
76+
)
77+
if groups:
78+
return results.groups()
79+
elif group is not None:
80+
return results.group(group)
81+
else:
82+
return results
5783

5884

5985
def apply_mixin(dct, key, func, *args, **kwargs):

pytube/helpers.pyc

287 Bytes
Binary file not shown.

pytube/itags.pyc

10 Bytes
Binary file not shown.

pytube/logging.pyc

10 Bytes
Binary file not shown.

pytube/mixins.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
"""Applies in-place data mutations."""
33
from __future__ import absolute_import
44

5+
import json
56
import logging
67
import pprint
78

89
from pytube import cipher
910
from pytube.compat import parse_qsl
1011
from pytube.compat import unquote
12+
from pytube.exceptions import LiveStreamError
1113

1214

1315
logger = logging.getLogger(__name__)
@@ -27,8 +29,14 @@ def apply_signature(config_args, fmt, js):
2729
2830
"""
2931
stream_manifest = config_args[fmt]
32+
live_stream = json.loads(config_args['player_response']).get(
33+
'playabilityStatus', {},
34+
).get('liveStreamability')
3035
for i, stream in enumerate(stream_manifest):
31-
url = stream['url']
36+
if 'url' in stream:
37+
url = stream['url']
38+
elif live_stream:
39+
raise LiveStreamError('Video is currently being streamed live')
3240

3341
if 'signature=' in url:
3442
# For certain videos, YouTube will just provide them pre-signed, in

pytube/mixins.pyc

352 Bytes
Binary file not shown.

pytube/query.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,23 @@ def order_by(self, attribute_name):
162162
:param str attribute_name:
163163
The name of the attribute to sort by.
164164
"""
165+
integer_attr_repr = {}
166+
for stream in self.fmt_streams:
167+
attr = getattr(stream, attribute_name)
168+
if attr is None:
169+
break
170+
num = [char for char in attr if char.isdigit()]
171+
integer_attr_repr[attr] = int(''.join(num)) if num else None
172+
173+
# if every attribute has an integer representation
174+
if integer_attr_repr and all(integer_attr_repr.values()):
175+
def key(s): return integer_attr_repr[getattr(s, attribute_name)]
176+
else:
177+
def key(s): return getattr(s, attribute_name)
178+
165179
fmt_streams = sorted(
166180
self.fmt_streams,
167-
key=lambda s: getattr(s, attribute_name),
181+
key=key,
168182
)
169183
return StreamQuery(fmt_streams)
170184

@@ -187,7 +201,7 @@ def asc(self):
187201
def get_by_itag(self, itag):
188202
"""Get the corresponding :class:`Stream <Stream>` for a given itag.
189203
190-
:param str itag:
204+
:param str int itag:
191205
YouTube format identifier code.
192206
:rtype: :class:`Stream <Stream>` or None
193207
:returns:
@@ -196,7 +210,7 @@ def get_by_itag(self, itag):
196210
197211
"""
198212
try:
199-
return self.itag_index[itag]
213+
return self.itag_index[int(itag)]
200214
except KeyError:
201215
pass
202216

pytube/query.pyc

639 Bytes
Binary file not shown.

pytube/request.pyc

20 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)