add -r resolution download tag
Browse files- pytube/cli.py +43 -7
- pytube/contrib/playlist.py +6 -19
- tests/test_cli.py +5 -5
pytube/cli.py
CHANGED
@@ -11,7 +11,7 @@ import sys
|
|
11 |
from io import BufferedWriter
|
12 |
from typing import Tuple, Any, Optional, List
|
13 |
|
14 |
-
from pytube import __version__, CaptionQuery
|
15 |
from pytube import YouTube
|
16 |
|
17 |
|
@@ -36,9 +36,11 @@ def main():
|
|
36 |
if args.build_playback_report:
|
37 |
build_playback_report(youtube)
|
38 |
if args.itag:
|
39 |
-
|
40 |
if hasattr(args, "caption_code"):
|
41 |
download_caption(youtube=youtube, lang_code=args.caption_code)
|
|
|
|
|
42 |
|
43 |
|
44 |
def _parse_args(
|
@@ -51,6 +53,9 @@ def _parse_args(
|
|
51 |
parser.add_argument(
|
52 |
"--itag", type=int, help="The itag for the desired stream",
|
53 |
)
|
|
|
|
|
|
|
54 |
parser.add_argument(
|
55 |
"-l",
|
56 |
"--list",
|
@@ -165,12 +170,18 @@ def on_progress(
|
|
165 |
display_progress_bar(bytes_received, filesize)
|
166 |
|
167 |
|
168 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
"""Start downloading a YouTube video.
|
170 |
|
171 |
:param YouTube youtube:
|
172 |
A valid YouTube object.
|
173 |
-
:param
|
174 |
YouTube format identifier code.
|
175 |
|
176 |
"""
|
@@ -184,10 +195,35 @@ def download(youtube: YouTube, itag: int) -> None:
|
|
184 |
sys.exit()
|
185 |
|
186 |
youtube.register_on_progress_callback(on_progress)
|
187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
try:
|
189 |
-
stream
|
190 |
-
sys.stdout.write("\n")
|
191 |
except KeyboardInterrupt:
|
192 |
sys.exit()
|
193 |
|
|
|
11 |
from io import BufferedWriter
|
12 |
from typing import Tuple, Any, Optional, List
|
13 |
|
14 |
+
from pytube import __version__, CaptionQuery, Stream
|
15 |
from pytube import YouTube
|
16 |
|
17 |
|
|
|
36 |
if args.build_playback_report:
|
37 |
build_playback_report(youtube)
|
38 |
if args.itag:
|
39 |
+
download_by_itag(youtube=youtube, itag=args.itag)
|
40 |
if hasattr(args, "caption_code"):
|
41 |
download_caption(youtube=youtube, lang_code=args.caption_code)
|
42 |
+
if args.resolution:
|
43 |
+
download_by_resolution(youtube=youtube, resolution=args.resolution)
|
44 |
|
45 |
|
46 |
def _parse_args(
|
|
|
53 |
parser.add_argument(
|
54 |
"--itag", type=int, help="The itag for the desired stream",
|
55 |
)
|
56 |
+
parser.add_argument(
|
57 |
+
"-r", "--resolution", type=str, help="The resolution for the desired stream",
|
58 |
+
)
|
59 |
parser.add_argument(
|
60 |
"-l",
|
61 |
"--list",
|
|
|
170 |
display_progress_bar(bytes_received, filesize)
|
171 |
|
172 |
|
173 |
+
def _download(stream: Stream) -> None:
|
174 |
+
print("\n{fn} | {fs} bytes".format(fn=stream.default_filename, fs=stream.filesize, ))
|
175 |
+
stream.download()
|
176 |
+
sys.stdout.write("\n")
|
177 |
+
|
178 |
+
|
179 |
+
def download_by_itag(youtube: YouTube, itag: int) -> None:
|
180 |
"""Start downloading a YouTube video.
|
181 |
|
182 |
:param YouTube youtube:
|
183 |
A valid YouTube object.
|
184 |
+
:param int itag:
|
185 |
YouTube format identifier code.
|
186 |
|
187 |
"""
|
|
|
195 |
sys.exit()
|
196 |
|
197 |
youtube.register_on_progress_callback(on_progress)
|
198 |
+
|
199 |
+
try:
|
200 |
+
_download(stream)
|
201 |
+
except KeyboardInterrupt:
|
202 |
+
sys.exit()
|
203 |
+
|
204 |
+
|
205 |
+
def download_by_resolution(youtube: YouTube, resolution: str) -> None:
|
206 |
+
"""Start downloading a YouTube video.
|
207 |
+
|
208 |
+
:param YouTube youtube:
|
209 |
+
A valid YouTube object.
|
210 |
+
:param str resolution:
|
211 |
+
YouTube video resolution.
|
212 |
+
|
213 |
+
"""
|
214 |
+
# TODO(nficano): allow download target to be specified
|
215 |
+
# TODO(nficano): allow dash itags to be selected
|
216 |
+
stream = youtube.streams.get_by_resolution(resolution)
|
217 |
+
if stream is None:
|
218 |
+
print("Could not find a stream with resolution: {resolution}".format(resolution=resolution))
|
219 |
+
print("Try one of these:")
|
220 |
+
display_streams(youtube)
|
221 |
+
sys.exit()
|
222 |
+
|
223 |
+
youtube.register_on_progress_callback(on_progress)
|
224 |
+
|
225 |
try:
|
226 |
+
_download(stream)
|
|
|
227 |
except KeyboardInterrupt:
|
228 |
sys.exit()
|
229 |
|
pytube/contrib/playlist.py
CHANGED
@@ -19,25 +19,14 @@ class Playlist:
|
|
19 |
"""
|
20 |
|
21 |
def __init__(self, url: str, suppress_exception: bool = False):
|
22 |
-
self.playlist_url = url
|
23 |
self.video_urls: List[str] = []
|
24 |
self.suppress_exception = suppress_exception
|
|
|
25 |
|
26 |
-
|
27 |
-
"""There are two kinds of playlist urls in YouTube. One that contains
|
28 |
-
watch?v= in URL, another one contains the "playlist?list=" portion. It
|
29 |
-
is preferable to work with the later one.
|
30 |
-
|
31 |
-
:return: playlist url
|
32 |
-
"""
|
33 |
-
|
34 |
-
if "watch?v=" in self.playlist_url:
|
35 |
base_url = "https://www.youtube.com/playlist?list="
|
36 |
-
playlist_code = self.playlist_url.split("&list=")[1]
|
37 |
-
|
38 |
-
|
39 |
-
# url is already in the desired format, so just return it
|
40 |
-
return self.playlist_url
|
41 |
|
42 |
@staticmethod
|
43 |
def _find_load_more_url(req: str) -> Optional[str]:
|
@@ -59,8 +48,7 @@ class Playlist:
|
|
59 |
It's an alternative for BeautifulSoup
|
60 |
"""
|
61 |
|
62 |
-
|
63 |
-
req = request.get(url)
|
64 |
|
65 |
# split the page source by line and process each line
|
66 |
content = [x for x in req.split("\n") if "pl-video-title-link" in x]
|
@@ -176,8 +164,7 @@ class Playlist:
|
|
176 |
|
177 |
def title(self) -> Optional[str]:
|
178 |
"""return playlist title (name)"""
|
179 |
-
|
180 |
-
req = request.get(url)
|
181 |
open_tag = "<title>"
|
182 |
end_tag = "</title>"
|
183 |
pattern = re.compile(open_tag + "(.+?)" + end_tag)
|
|
|
19 |
"""
|
20 |
|
21 |
def __init__(self, url: str, suppress_exception: bool = False):
|
|
|
22 |
self.video_urls: List[str] = []
|
23 |
self.suppress_exception = suppress_exception
|
24 |
+
self.playlist_url:str = url
|
25 |
|
26 |
+
if "watch?v=" in url:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
base_url = "https://www.youtube.com/playlist?list="
|
28 |
+
playlist_code = self.playlist_url.split("&list=")[1] # TODO: should be parse q
|
29 |
+
self.playlist_url = base_url + playlist_code
|
|
|
|
|
|
|
30 |
|
31 |
@staticmethod
|
32 |
def _find_load_more_url(req: str) -> Optional[str]:
|
|
|
48 |
It's an alternative for BeautifulSoup
|
49 |
"""
|
50 |
|
51 |
+
req = request.get(self.playlist_url)
|
|
|
52 |
|
53 |
# split the page source by line and process each line
|
54 |
content = [x for x in req.split("\n") if "pl-video-title-link" in x]
|
|
|
164 |
|
165 |
def title(self) -> Optional[str]:
|
166 |
"""return playlist title (name)"""
|
167 |
+
req = request.get(self.playlist_url)
|
|
|
168 |
open_tag = "<title>"
|
169 |
end_tag = "</title>"
|
170 |
pattern = re.compile(open_tag + "(.+?)" + end_tag)
|
tests/test_cli.py
CHANGED
@@ -16,7 +16,7 @@ def test_download_when_itag_not_found(youtube):
|
|
16 |
youtube.streams.all.return_value = []
|
17 |
youtube.streams.get_by_itag.return_value = None
|
18 |
with pytest.raises(SystemExit):
|
19 |
-
cli.
|
20 |
youtube.streams.get_by_itag.assert_called_with(123)
|
21 |
|
22 |
|
@@ -28,7 +28,7 @@ def test_download_when_itag_is_found(youtube, stream):
|
|
28 |
with patch.object(
|
29 |
youtube.streams, "get_by_itag", wraps=youtube.streams.get_by_itag
|
30 |
) as wrapped_itag:
|
31 |
-
cli.
|
32 |
wrapped_itag.assert_called_with(123)
|
33 |
youtube.register_on_progress_callback.assert_called_with(cli.on_progress)
|
34 |
stream.download.assert_called()
|
@@ -115,14 +115,14 @@ def test_parse_args_truthy():
|
|
115 |
|
116 |
|
117 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
118 |
-
def
|
119 |
parser = argparse.ArgumentParser()
|
120 |
args = parse_args(parser, ["urlhere", "--itag=10"])
|
121 |
cli._parse_args = MagicMock(return_value=args)
|
122 |
-
cli.
|
123 |
cli.main()
|
124 |
youtube.assert_called()
|
125 |
-
cli.
|
126 |
|
127 |
|
128 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
|
|
16 |
youtube.streams.all.return_value = []
|
17 |
youtube.streams.get_by_itag.return_value = None
|
18 |
with pytest.raises(SystemExit):
|
19 |
+
cli.download_by_itag(youtube, 123)
|
20 |
youtube.streams.get_by_itag.assert_called_with(123)
|
21 |
|
22 |
|
|
|
28 |
with patch.object(
|
29 |
youtube.streams, "get_by_itag", wraps=youtube.streams.get_by_itag
|
30 |
) as wrapped_itag:
|
31 |
+
cli.download_by_itag(youtube, 123)
|
32 |
wrapped_itag.assert_called_with(123)
|
33 |
youtube.register_on_progress_callback.assert_called_with(cli.on_progress)
|
34 |
stream.download.assert_called()
|
|
|
115 |
|
116 |
|
117 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
118 |
+
def test_main_download_by_itag(youtube):
|
119 |
parser = argparse.ArgumentParser()
|
120 |
args = parse_args(parser, ["urlhere", "--itag=10"])
|
121 |
cli._parse_args = MagicMock(return_value=args)
|
122 |
+
cli.download_by_itag = MagicMock()
|
123 |
cli.main()
|
124 |
youtube.assert_called()
|
125 |
+
cli.download_by_itag.assert_called()
|
126 |
|
127 |
|
128 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|