Merge pull request #253 from johnvanderholt/master
Browse files- pytube/contrib/playlist.py +42 -3
- pytube/streams.py +13 -1
- tests/test_playlist.py +51 -26
pytube/contrib/playlist.py
CHANGED
@@ -66,26 +66,65 @@ class Playlist(object):
|
|
66 |
complete_url = base_url + video_id
|
67 |
self.video_urls.append(complete_url)
|
68 |
|
69 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
"""Download all the videos in the the playlist. Initially, download
|
71 |
resolution is 720p (or highest available), later more option
|
72 |
should be added to download resolution of choice
|
73 |
|
74 |
TODO(nficano): Add option to download resolution of user's choice
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
"""
|
76 |
|
77 |
self.populate_video_urls()
|
78 |
logger.debug('total videos found: ', len(self.video_urls))
|
79 |
logger.debug('starting download')
|
80 |
|
|
|
|
|
81 |
for link in self.video_urls:
|
82 |
yt = YouTube(link)
|
83 |
-
|
84 |
# TODO: this should not be hardcoded to a single user's preference
|
85 |
dl_stream = yt.streams.filter(
|
86 |
progressive=True, subtype='mp4',
|
87 |
).order_by('resolution').desc().first()
|
88 |
|
89 |
logger.debug('download path: %s', download_path)
|
90 |
-
|
|
|
|
|
|
|
|
|
|
|
91 |
logger.debug('download complete')
|
|
|
66 |
complete_url = base_url + video_id
|
67 |
self.video_urls.append(complete_url)
|
68 |
|
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(self, download_path=None, prefix_number=True,
|
88 |
+
reverse_numbering=False):
|
89 |
"""Download all the videos in the the playlist. Initially, download
|
90 |
resolution is 720p (or highest available), later more option
|
91 |
should be added to download resolution of choice
|
92 |
|
93 |
TODO(nficano): Add option to download resolution of user's choice
|
94 |
+
|
95 |
+
:param download_path:
|
96 |
+
(optional) Output path for the playlist If one is not
|
97 |
+
specified, defaults to the current working directory.
|
98 |
+
This is passed along to the Stream objects.
|
99 |
+
:type download_path: str or None
|
100 |
+
:param prefix_number:
|
101 |
+
(optional) Automatically numbers playlists using the
|
102 |
+
_path_num_prefix_generator function.
|
103 |
+
:type prefix_number: bool
|
104 |
+
:param reverse_numbering:
|
105 |
+
(optional) Lets you number playlists in reverse, since some
|
106 |
+
playlists are ordered newest -> oldests.
|
107 |
+
:type reverse_numbering: bool
|
108 |
"""
|
109 |
|
110 |
self.populate_video_urls()
|
111 |
logger.debug('total videos found: ', len(self.video_urls))
|
112 |
logger.debug('starting download')
|
113 |
|
114 |
+
prefix_gen = self._path_num_prefix_generator(reverse_numbering)
|
115 |
+
|
116 |
for link in self.video_urls:
|
117 |
yt = YouTube(link)
|
|
|
118 |
# TODO: this should not be hardcoded to a single user's preference
|
119 |
dl_stream = yt.streams.filter(
|
120 |
progressive=True, subtype='mp4',
|
121 |
).order_by('resolution').desc().first()
|
122 |
|
123 |
logger.debug('download path: %s', download_path)
|
124 |
+
if prefix_number:
|
125 |
+
prefix = next(prefix_gen)
|
126 |
+
logger.debug('file prefix is: %s', prefix)
|
127 |
+
dl_stream.download(download_path, filename_prefix=prefix)
|
128 |
+
else:
|
129 |
+
dl_stream.download(download_path)
|
130 |
logger.debug('download complete')
|
pytube/streams.py
CHANGED
@@ -174,7 +174,7 @@ class Stream(object):
|
|
174 |
filename = safe_filename(title)
|
175 |
return '{filename}.{s.subtype}'.format(filename=filename, s=self)
|
176 |
|
177 |
-
def download(self, output_path=None, filename=None):
|
178 |
"""Write the media stream to disk.
|
179 |
|
180 |
:param output_path:
|
@@ -185,6 +185,13 @@ class Stream(object):
|
|
185 |
(optional) Output filename (stem only) for writing media file.
|
186 |
If one is not specified, the default filename is used.
|
187 |
:type filename: str or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
:rtype: str
|
190 |
|
@@ -195,6 +202,11 @@ class Stream(object):
|
|
195 |
filename = '{filename}.{s.subtype}'.format(filename=safe, s=self)
|
196 |
filename = filename or self.default_filename
|
197 |
|
|
|
|
|
|
|
|
|
|
|
198 |
# file path
|
199 |
fp = os.path.join(output_path, filename)
|
200 |
bytes_remaining = self.filesize
|
|
|
174 |
filename = safe_filename(title)
|
175 |
return '{filename}.{s.subtype}'.format(filename=filename, s=self)
|
176 |
|
177 |
+
def download(self, output_path=None, filename=None, filename_prefix=None):
|
178 |
"""Write the media stream to disk.
|
179 |
|
180 |
:param output_path:
|
|
|
185 |
(optional) Output filename (stem only) for writing media file.
|
186 |
If one is not specified, the default filename is used.
|
187 |
:type filename: str or None
|
188 |
+
:param filename_prefix:
|
189 |
+
(optional) A string that will be prepended to the filename.
|
190 |
+
For example a number in a playlist or the name of a series.
|
191 |
+
If one is not specified, nothing will be prepended
|
192 |
+
This is seperate from filename so you can use the default
|
193 |
+
filename but still add a prefix.
|
194 |
+
:type filename_prefix: str or None
|
195 |
|
196 |
:rtype: str
|
197 |
|
|
|
202 |
filename = '{filename}.{s.subtype}'.format(filename=safe, s=self)
|
203 |
filename = filename or self.default_filename
|
204 |
|
205 |
+
if filename_prefix:
|
206 |
+
filename = "{prefix}{filename}"\
|
207 |
+
.format(prefix=safe_filename(filename_prefix),
|
208 |
+
filename=filename)
|
209 |
+
|
210 |
# file path
|
211 |
fp = os.path.join(output_path, filename)
|
212 |
bytes_remaining = self.filesize
|
tests/test_playlist.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
from pytube import Playlist
|
3 |
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
def test_construct():
|
6 |
-
ob = Playlist(
|
7 |
-
'https://www.youtube.com/watch?v=m5q2GCsteQs&list='
|
8 |
-
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw',
|
9 |
-
)
|
10 |
expected = 'https://www.youtube.com/' \
|
11 |
'playlist?list=' \
|
12 |
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw'
|
@@ -14,25 +16,8 @@ def test_construct():
|
|
14 |
assert ob.construct_playlist_url() == expected
|
15 |
|
16 |
|
17 |
-
def test_link_parse():
|
18 |
-
ob = Playlist(
|
19 |
-
'https://www.youtube.com/watch?v=m5q2GCsteQs&list='
|
20 |
-
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw',
|
21 |
-
)
|
22 |
-
|
23 |
-
expected = [
|
24 |
-
'/watch?v=m5q2GCsteQs',
|
25 |
-
'/watch?v=5YK63cXyJ2Q',
|
26 |
-
'/watch?v=Rzt4rUPFYD4',
|
27 |
-
]
|
28 |
-
assert ob.parse_links() == expected
|
29 |
-
|
30 |
-
|
31 |
def test_populate():
|
32 |
-
ob = Playlist(
|
33 |
-
'https://www.youtube.com/watch?v=m5q2GCsteQs&list='
|
34 |
-
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw',
|
35 |
-
)
|
36 |
expected = [
|
37 |
'https://www.youtube.com/watch?v=m5q2GCsteQs',
|
38 |
'https://www.youtube.com/watch?v=5YK63cXyJ2Q',
|
@@ -43,10 +28,50 @@ def test_populate():
|
|
43 |
assert ob.video_urls == expected
|
44 |
|
45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
def test_download():
|
47 |
-
ob = Playlist(
|
48 |
-
'https://www.youtube.com/watch?v=lByG_AgKS9k&list='
|
49 |
-
'PL525f8ds9RvuerPZ3bZygmNiYw2sP4BDk',
|
50 |
-
)
|
51 |
ob.download_all()
|
52 |
ob.download_all('.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
from pytube import Playlist
|
3 |
|
4 |
+
short_test_pl = 'https://www.youtube.com/watch?v=' \
|
5 |
+
'm5q2GCsteQs&list=PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw'
|
6 |
+
long_test_pl = "https://www.youtube.com/watch?v=" \
|
7 |
+
"9CHDoAsX1yo&list=UUXuqSBlHAE6Xw-yeJA0Tunw"
|
8 |
+
|
9 |
|
10 |
def test_construct():
|
11 |
+
ob = Playlist(short_test_pl)
|
|
|
|
|
|
|
12 |
expected = 'https://www.youtube.com/' \
|
13 |
'playlist?list=' \
|
14 |
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw'
|
|
|
16 |
assert ob.construct_playlist_url() == expected
|
17 |
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
def test_populate():
|
20 |
+
ob = Playlist(short_test_pl)
|
|
|
|
|
|
|
21 |
expected = [
|
22 |
'https://www.youtube.com/watch?v=m5q2GCsteQs',
|
23 |
'https://www.youtube.com/watch?v=5YK63cXyJ2Q',
|
|
|
28 |
assert ob.video_urls == expected
|
29 |
|
30 |
|
31 |
+
def test_link_parse():
|
32 |
+
ob = Playlist(short_test_pl)
|
33 |
+
|
34 |
+
expected = [
|
35 |
+
'/watch?v=m5q2GCsteQs',
|
36 |
+
'/watch?v=5YK63cXyJ2Q',
|
37 |
+
'/watch?v=Rzt4rUPFYD4',
|
38 |
+
]
|
39 |
+
assert ob.parse_links() == expected
|
40 |
+
|
41 |
+
|
42 |
def test_download():
|
43 |
+
ob = Playlist(short_test_pl)
|
|
|
|
|
|
|
44 |
ob.download_all()
|
45 |
ob.download_all('.')
|
46 |
+
ob.download_all(prefix_number=False)
|
47 |
+
ob.download_all('.', prefix_number=False)
|
48 |
+
|
49 |
+
|
50 |
+
def test_numbering():
|
51 |
+
ob = Playlist(short_test_pl)
|
52 |
+
ob.populate_video_urls()
|
53 |
+
gen = ob._path_num_prefix_generator(reverse=False)
|
54 |
+
assert "1" in next(gen)
|
55 |
+
assert "2" in next(gen)
|
56 |
+
|
57 |
+
ob = Playlist(short_test_pl)
|
58 |
+
ob.populate_video_urls()
|
59 |
+
gen = ob._path_num_prefix_generator(reverse=True)
|
60 |
+
assert str(len(ob.video_urls)) in next(gen)
|
61 |
+
assert str(len(ob.video_urls) - 1) in next(gen)
|
62 |
+
|
63 |
+
ob = Playlist(long_test_pl)
|
64 |
+
ob.populate_video_urls()
|
65 |
+
gen = ob._path_num_prefix_generator(reverse=False)
|
66 |
+
nxt = next(gen)
|
67 |
+
assert len(nxt) > 1
|
68 |
+
assert "1" in nxt
|
69 |
+
nxt = next(gen)
|
70 |
+
assert len(nxt) > 1
|
71 |
+
assert "2" in nxt
|
72 |
+
|
73 |
+
ob = Playlist(long_test_pl)
|
74 |
+
ob.populate_video_urls()
|
75 |
+
gen = ob._path_num_prefix_generator(reverse=True)
|
76 |
+
assert str(len(ob.video_urls)) in next(gen)
|
77 |
+
assert str(len(ob.video_urls) - 1) in next(gen)
|