Merge branch 'master' into download-file-exists
Browse files- .flake8 +1 -1
- pytube/cli.py +54 -70
- pytube/query.py +4 -2
- tests/test_captions.py +27 -3
- tests/test_cli.py +242 -21
- tests/test_query.py +4 -4
- tests/test_streams.py +16 -16
.flake8
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
[flake8]
|
2 |
ignore = E231,E203,W503,Q000,WPS111,WPS305,WPS348,WPS602,D400,DAR201,S101,DAR101,C812,D104,I001,WPS306,WPS214,D401,WPS229,WPS420,WPS230,WPS414,WPS114,WPS226,WPS442,C819,WPS601,T001,RST304,WPS410,WPS428,A003,A002,I003,WPS221,WPS326,WPS201,S405,DAR301,WPS210,WPS202,WPS213,WPS301,P103,WPS407,WPS432,WPS211,S314,S310,S001,IF100,PT001
|
3 |
-
max-line-length =
|
4 |
|
5 |
[isort]
|
|
|
1 |
[flake8]
|
2 |
ignore = E231,E203,W503,Q000,WPS111,WPS305,WPS348,WPS602,D400,DAR201,S101,DAR101,C812,D104,I001,WPS306,WPS214,D401,WPS229,WPS420,WPS230,WPS414,WPS114,WPS226,WPS442,C819,WPS601,T001,RST304,WPS410,WPS428,A003,A002,I003,WPS221,WPS326,WPS201,S405,DAR301,WPS210,WPS202,WPS213,WPS301,P103,WPS407,WPS432,WPS211,S314,S310,S001,IF100,PT001
|
3 |
+
max-line-length = 95
|
4 |
|
5 |
[isort]
|
pytube/cli.py
CHANGED
@@ -222,7 +222,7 @@ def _download(
|
|
222 |
stream: Stream, target: Optional[str] = None, filename: Optional[str] = None
|
223 |
) -> None:
|
224 |
filesize_megabytes = stream.filesize // 1048576
|
225 |
-
print(f"{stream.default_filename} | {filesize_megabytes} MB")
|
226 |
file_path = stream.get_file_path(filename=filename, output_path=target)
|
227 |
if stream.exists_at_path(file_path):
|
228 |
print(f"Already downloaded at:\n{file_path}")
|
@@ -232,9 +232,7 @@ def _download(
|
|
232 |
sys.stdout.write("\n")
|
233 |
|
234 |
|
235 |
-
def
|
236 |
-
base: str, subtype: Optional[str], video_audio: str, target: str
|
237 |
-
) -> str:
|
238 |
"""
|
239 |
Given a base name, the file format, and the target directory, will generate
|
240 |
a filename unique for that directory and file format.
|
@@ -242,15 +240,17 @@ def unique_name(
|
|
242 |
The given base-name.
|
243 |
:param str subtype:
|
244 |
The filetype of the video which will be downloaded.
|
|
|
|
|
245 |
:param Path target:
|
246 |
Target directory for download.
|
247 |
"""
|
248 |
counter = 0
|
249 |
while True:
|
250 |
-
|
251 |
-
|
252 |
-
if not os.path.exists(
|
253 |
-
return
|
254 |
counter += 1
|
255 |
|
256 |
|
@@ -258,7 +258,7 @@ def ffmpeg_process(
|
|
258 |
youtube: YouTube, resolution: str, target: Optional[str] = None
|
259 |
) -> None:
|
260 |
"""
|
261 |
-
Decides the correct video stream to download, then calls
|
262 |
|
263 |
:param YouTube youtube:
|
264 |
A valid YouTube object.
|
@@ -268,91 +268,78 @@ def ffmpeg_process(
|
|
268 |
Target directory for download
|
269 |
"""
|
270 |
youtube.register_on_progress_callback(on_progress)
|
271 |
-
|
272 |
-
target = os.getcwd()
|
273 |
|
274 |
if resolution == "best":
|
275 |
-
|
276 |
-
youtube.streams.filter(progressive=False)
|
277 |
-
.order_by("resolution")
|
278 |
-
.desc()
|
279 |
-
.first()
|
280 |
)
|
281 |
-
|
282 |
-
video_stream = (
|
283 |
youtube.streams.filter(progressive=False, subtype="mp4")
|
284 |
.order_by("resolution")
|
285 |
-
.
|
286 |
-
.first()
|
287 |
)
|
288 |
-
|
289 |
-
|
290 |
-
ffmpeg_downloader(youtube=youtube, stream=video_stream, target=target)
|
291 |
else:
|
292 |
-
|
293 |
else:
|
294 |
video_stream = youtube.streams.filter(
|
295 |
progressive=False, resolution=resolution, subtype="mp4"
|
296 |
).first()
|
297 |
-
if
|
298 |
-
ffmpeg_downloader(youtube=youtube, stream=video_stream, target=target)
|
299 |
-
else:
|
300 |
video_stream = youtube.streams.filter(
|
301 |
progressive=False, resolution=resolution
|
302 |
).first()
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
309 |
|
310 |
|
311 |
-
def
|
312 |
"""
|
313 |
Given a YouTube Stream object, finds the correct audio stream, downloads them both
|
314 |
giving them a unique name, them uses ffmpeg to create a new file with the audio
|
315 |
and video from the previously downloaded files. Then deletes the original adaptive
|
316 |
streams, leaving the combination.
|
317 |
|
318 |
-
:param
|
319 |
-
A valid
|
320 |
-
:param Stream
|
321 |
-
A valid Stream object
|
322 |
:param Path target:
|
323 |
A valid Path object
|
324 |
"""
|
325 |
-
|
326 |
-
|
327 |
-
.order_by("abr")
|
328 |
-
.desc()
|
329 |
-
.first()
|
330 |
-
)
|
331 |
-
|
332 |
-
video_unique_name = unique_name(
|
333 |
-
safe_filename(stream.title), stream.subtype, "video", target=target
|
334 |
)
|
335 |
-
audio_unique_name =
|
336 |
-
safe_filename(
|
337 |
)
|
338 |
-
_download(stream=
|
|
|
339 |
_download(stream=audio_stream, target=target, filename=audio_unique_name)
|
340 |
|
341 |
-
video_path = os.path.join(target, f"{video_unique_name}.{
|
342 |
-
audio_path = os.path.join(target, f"{audio_unique_name}.{
|
343 |
-
final_path = os.path.join(
|
|
|
|
|
344 |
|
345 |
subprocess.run( # nosec
|
346 |
-
[
|
347 |
-
"ffmpeg",
|
348 |
-
"-i",
|
349 |
-
f"{video_path}",
|
350 |
-
"-i",
|
351 |
-
f"{audio_path}",
|
352 |
-
"-codec",
|
353 |
-
"copy",
|
354 |
-
f"{final_path}",
|
355 |
-
]
|
356 |
)
|
357 |
os.unlink(video_path)
|
358 |
os.unlink(audio_path)
|
@@ -444,11 +431,11 @@ def download_caption(
|
|
444 |
_print_available_captions(youtube.captions)
|
445 |
return
|
446 |
|
447 |
-
|
448 |
-
|
449 |
downloaded_path = caption.download(title=youtube.title, output_path=target)
|
450 |
print(f"Saved caption file to: {downloaded_path}")
|
451 |
-
|
452 |
print(f"Unable to find caption with code: {lang_code}")
|
453 |
_print_available_captions(youtube.captions)
|
454 |
|
@@ -468,10 +455,7 @@ def download_audio(
|
|
468 |
Target directory for download
|
469 |
"""
|
470 |
audio = (
|
471 |
-
youtube.streams.filter(only_audio=True, subtype=filetype)
|
472 |
-
.order_by("abr")
|
473 |
-
.desc()
|
474 |
-
.first()
|
475 |
)
|
476 |
|
477 |
if audio is None:
|
|
|
222 |
stream: Stream, target: Optional[str] = None, filename: Optional[str] = None
|
223 |
) -> None:
|
224 |
filesize_megabytes = stream.filesize // 1048576
|
225 |
+
print(f"{filename or stream.default_filename} | {filesize_megabytes} MB")
|
226 |
file_path = stream.get_file_path(filename=filename, output_path=target)
|
227 |
if stream.exists_at_path(file_path):
|
228 |
print(f"Already downloaded at:\n{file_path}")
|
|
|
232 |
sys.stdout.write("\n")
|
233 |
|
234 |
|
235 |
+
def _unique_name(base: str, subtype: str, media_type: str, target: str) -> str:
|
|
|
|
|
236 |
"""
|
237 |
Given a base name, the file format, and the target directory, will generate
|
238 |
a filename unique for that directory and file format.
|
|
|
240 |
The given base-name.
|
241 |
:param str subtype:
|
242 |
The filetype of the video which will be downloaded.
|
243 |
+
:param str media_type:
|
244 |
+
The media_type of the file, ie. "audio" or "video"
|
245 |
:param Path target:
|
246 |
Target directory for download.
|
247 |
"""
|
248 |
counter = 0
|
249 |
while True:
|
250 |
+
file_name = f"{base}_{media_type}_{counter}"
|
251 |
+
file_path = os.path.join(target, f"{file_name}.{subtype}")
|
252 |
+
if not os.path.exists(file_path):
|
253 |
+
return file_name
|
254 |
counter += 1
|
255 |
|
256 |
|
|
|
258 |
youtube: YouTube, resolution: str, target: Optional[str] = None
|
259 |
) -> None:
|
260 |
"""
|
261 |
+
Decides the correct video stream to download, then calls _ffmpeg_downloader.
|
262 |
|
263 |
:param YouTube youtube:
|
264 |
A valid YouTube object.
|
|
|
268 |
Target directory for download
|
269 |
"""
|
270 |
youtube.register_on_progress_callback(on_progress)
|
271 |
+
target = target or os.getcwd()
|
|
|
272 |
|
273 |
if resolution == "best":
|
274 |
+
highest_quality_stream = (
|
275 |
+
youtube.streams.filter(progressive=False).order_by("resolution").last()
|
|
|
|
|
|
|
276 |
)
|
277 |
+
mp4_stream = (
|
|
|
278 |
youtube.streams.filter(progressive=False, subtype="mp4")
|
279 |
.order_by("resolution")
|
280 |
+
.last()
|
|
|
281 |
)
|
282 |
+
if highest_quality_stream.resolution == mp4_stream.resolution:
|
283 |
+
video_stream = mp4_stream
|
|
|
284 |
else:
|
285 |
+
video_stream = highest_quality_stream
|
286 |
else:
|
287 |
video_stream = youtube.streams.filter(
|
288 |
progressive=False, resolution=resolution, subtype="mp4"
|
289 |
).first()
|
290 |
+
if not video_stream:
|
|
|
|
|
291 |
video_stream = youtube.streams.filter(
|
292 |
progressive=False, resolution=resolution
|
293 |
).first()
|
294 |
+
if video_stream is None:
|
295 |
+
print(f"Could not find a stream with resolution: {resolution}")
|
296 |
+
print("Try one of these:")
|
297 |
+
display_streams(youtube)
|
298 |
+
sys.exit()
|
299 |
+
|
300 |
+
audio_stream = youtube.streams.get_audio_only(video_stream.subtype)
|
301 |
+
if not audio_stream:
|
302 |
+
audio_stream = youtube.streams.filter(only_audio=True).order_by("abr").last()
|
303 |
+
if not audio_stream:
|
304 |
+
print("Could not find an audio only stream")
|
305 |
+
sys.exit()
|
306 |
+
_ffmpeg_downloader(
|
307 |
+
audio_stream=audio_stream, video_stream=video_stream, target=target
|
308 |
+
)
|
309 |
|
310 |
|
311 |
+
def _ffmpeg_downloader(audio_stream: Stream, video_stream: Stream, target: str) -> None:
|
312 |
"""
|
313 |
Given a YouTube Stream object, finds the correct audio stream, downloads them both
|
314 |
giving them a unique name, them uses ffmpeg to create a new file with the audio
|
315 |
and video from the previously downloaded files. Then deletes the original adaptive
|
316 |
streams, leaving the combination.
|
317 |
|
318 |
+
:param Stream audio_stream:
|
319 |
+
A valid Stream object representing the audio to download
|
320 |
+
:param Stream video_stream:
|
321 |
+
A valid Stream object representing the video to download
|
322 |
:param Path target:
|
323 |
A valid Path object
|
324 |
"""
|
325 |
+
video_unique_name = _unique_name(
|
326 |
+
safe_filename(video_stream.title), video_stream.subtype, "video", target=target
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
)
|
328 |
+
audio_unique_name = _unique_name(
|
329 |
+
safe_filename(video_stream.title), audio_stream.subtype, "audio", target=target
|
330 |
)
|
331 |
+
_download(stream=video_stream, target=target, filename=video_unique_name)
|
332 |
+
print("Loading audio...")
|
333 |
_download(stream=audio_stream, target=target, filename=audio_unique_name)
|
334 |
|
335 |
+
video_path = os.path.join(target, f"{video_unique_name}.{video_stream.subtype}")
|
336 |
+
audio_path = os.path.join(target, f"{audio_unique_name}.{audio_stream.subtype}")
|
337 |
+
final_path = os.path.join(
|
338 |
+
target, f"{safe_filename(video_stream.title)}.{video_stream.subtype}"
|
339 |
+
)
|
340 |
|
341 |
subprocess.run( # nosec
|
342 |
+
["ffmpeg", "-i", video_path, "-i", audio_path, "-codec", "copy", final_path,]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
)
|
344 |
os.unlink(video_path)
|
345 |
os.unlink(audio_path)
|
|
|
431 |
_print_available_captions(youtube.captions)
|
432 |
return
|
433 |
|
434 |
+
try:
|
435 |
+
caption = youtube.captions[lang_code]
|
436 |
downloaded_path = caption.download(title=youtube.title, output_path=target)
|
437 |
print(f"Saved caption file to: {downloaded_path}")
|
438 |
+
except KeyError:
|
439 |
print(f"Unable to find caption with code: {lang_code}")
|
440 |
_print_available_captions(youtube.captions)
|
441 |
|
|
|
455 |
Target directory for download
|
456 |
"""
|
457 |
audio = (
|
458 |
+
youtube.streams.filter(only_audio=True, subtype=filetype).order_by("abr").last()
|
|
|
|
|
|
|
459 |
)
|
460 |
|
461 |
if audio is None:
|
pytube/query.py
CHANGED
@@ -369,7 +369,9 @@ class CaptionQuery(Mapping):
|
|
369 |
self.lang_code_index = {c.code: c for c in captions}
|
370 |
|
371 |
@deprecated("This object can be treated as a dictionary, i.e. captions['en']")
|
372 |
-
def get_by_language_code(
|
|
|
|
|
373 |
"""Get the :class:`Caption <Caption>` for a given ``lang_code``.
|
374 |
|
375 |
:param str lang_code:
|
@@ -397,7 +399,7 @@ class CaptionQuery(Mapping):
|
|
397 |
return len(self.lang_code_index)
|
398 |
|
399 |
def __iter__(self):
|
400 |
-
return iter(self.lang_code_index)
|
401 |
|
402 |
def __repr__(self) -> str:
|
403 |
return f"{self.lang_code_index}"
|
|
|
369 |
self.lang_code_index = {c.code: c for c in captions}
|
370 |
|
371 |
@deprecated("This object can be treated as a dictionary, i.e. captions['en']")
|
372 |
+
def get_by_language_code(
|
373 |
+
self, lang_code: str
|
374 |
+
) -> Optional[Caption]: # pragma: no cover
|
375 |
"""Get the :class:`Caption <Caption>` for a given ``lang_code``.
|
376 |
|
377 |
:param str lang_code:
|
|
|
399 |
return len(self.lang_code_index)
|
400 |
|
401 |
def __iter__(self):
|
402 |
+
return iter(self.lang_code_index.values())
|
403 |
|
404 |
def __repr__(self) -> str:
|
405 |
return f"{self.lang_code_index}"
|
tests/test_captions.py
CHANGED
@@ -26,7 +26,8 @@ def test_caption_query_sequence():
|
|
26 |
assert caption_query["en"] == caption1
|
27 |
assert caption_query["fr"] == caption2
|
28 |
with pytest.raises(KeyError):
|
29 |
-
caption_query["nada"]
|
|
|
30 |
|
31 |
|
32 |
def test_caption_query_get_by_language_code_when_exists():
|
@@ -37,7 +38,7 @@ def test_caption_query_get_by_language_code_when_exists():
|
|
37 |
{"url": "url2", "name": {"simpleText": "name2"}, "languageCode": "fr"}
|
38 |
)
|
39 |
caption_query = CaptionQuery(captions=[caption1, caption2])
|
40 |
-
assert caption_query
|
41 |
|
42 |
|
43 |
def test_caption_query_get_by_language_code_when_not_exists():
|
@@ -48,7 +49,9 @@ def test_caption_query_get_by_language_code_when_not_exists():
|
|
48 |
{"url": "url2", "name": {"simpleText": "name2"}, "languageCode": "fr"}
|
49 |
)
|
50 |
caption_query = CaptionQuery(captions=[caption1, caption2])
|
51 |
-
|
|
|
|
|
52 |
|
53 |
|
54 |
@mock.patch("pytube.captions.Caption.generate_srt_captions")
|
@@ -118,3 +121,24 @@ def test_xml_captions(request_get):
|
|
118 |
{"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
|
119 |
)
|
120 |
assert caption.xml_captions == "test"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
assert caption_query["en"] == caption1
|
27 |
assert caption_query["fr"] == caption2
|
28 |
with pytest.raises(KeyError):
|
29 |
+
not_exists = caption_query["nada"]
|
30 |
+
assert not_exists is not None # should never reach this
|
31 |
|
32 |
|
33 |
def test_caption_query_get_by_language_code_when_exists():
|
|
|
38 |
{"url": "url2", "name": {"simpleText": "name2"}, "languageCode": "fr"}
|
39 |
)
|
40 |
caption_query = CaptionQuery(captions=[caption1, caption2])
|
41 |
+
assert caption_query["en"] == caption1
|
42 |
|
43 |
|
44 |
def test_caption_query_get_by_language_code_when_not_exists():
|
|
|
49 |
{"url": "url2", "name": {"simpleText": "name2"}, "languageCode": "fr"}
|
50 |
)
|
51 |
caption_query = CaptionQuery(captions=[caption1, caption2])
|
52 |
+
with pytest.raises(KeyError):
|
53 |
+
not_found = caption_query["hello"]
|
54 |
+
assert not_found is not None # should never reach here
|
55 |
|
56 |
|
57 |
@mock.patch("pytube.captions.Caption.generate_srt_captions")
|
|
|
121 |
{"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
|
122 |
)
|
123 |
assert caption.xml_captions == "test"
|
124 |
+
|
125 |
+
|
126 |
+
@mock.patch("pytube.captions.request")
|
127 |
+
def test_generate_srt_captions(request):
|
128 |
+
request.get.return_value = (
|
129 |
+
'<?xml version="1.0" encoding="utf-8" ?><transcript><text start="6.5" dur="1.7">['
|
130 |
+
'Herb, Software Engineer]\n本影片包含隱藏式字幕。</text><text start="8.3" dur="2.7">'
|
131 |
+
"如要啓動字幕,請按一下這裡的圖示。</text></transcript>"
|
132 |
+
)
|
133 |
+
caption = Caption(
|
134 |
+
{"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
|
135 |
+
)
|
136 |
+
assert caption.generate_srt_captions() == (
|
137 |
+
"1\n"
|
138 |
+
"00:00:06,500 --> 00:00:08,200\n"
|
139 |
+
"[Herb, Software Engineer] 本影片包含隱藏式字幕。\n"
|
140 |
+
"\n"
|
141 |
+
"2\n"
|
142 |
+
"00:00:08,300 --> 00:00:11,000\n"
|
143 |
+
"如要啓動字幕,請按一下這裡的圖示。"
|
144 |
+
)
|
tests/test_cli.py
CHANGED
@@ -6,10 +6,20 @@ from unittest.mock import MagicMock, patch
|
|
6 |
import pytest
|
7 |
|
8 |
from pytube import cli, StreamQuery, Caption, CaptionQuery
|
|
|
9 |
|
10 |
parse_args = cli._parse_args
|
11 |
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
@mock.patch("pytube.cli.display_streams")
|
14 |
@mock.patch("pytube.cli.YouTube")
|
15 |
def test_download_when_itag_not_found(youtube, display_streams):
|
@@ -92,6 +102,22 @@ def test_download_caption_with_lang_not_found(youtube, print_available):
|
|
92 |
print_available.assert_called_with(youtube.captions)
|
93 |
|
94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
def test_display_progress_bar(capsys):
|
96 |
cli.display_progress_bar(bytes_received=25, filesize=100, scale=0.55)
|
97 |
out, _ = capsys.readouterr()
|
@@ -193,14 +219,46 @@ def test_download_by_resolution_flag(youtube, download_by_resolution):
|
|
193 |
download_by_resolution.assert_called()
|
194 |
|
195 |
|
|
|
196 |
@mock.patch("pytube.cli.Playlist")
|
197 |
-
|
|
|
|
|
198 |
cli.safe_filename = MagicMock(return_value="safe_title")
|
199 |
parser = argparse.ArgumentParser()
|
200 |
args = parse_args(parser, ["https://www.youtube.com/playlist?list=PLyn"])
|
201 |
cli._parse_args = MagicMock(return_value=args)
|
|
|
|
|
|
|
|
|
202 |
cli.main()
|
|
|
203 |
playlist.assert_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
|
205 |
|
206 |
@mock.patch("pytube.cli.YouTube")
|
@@ -228,39 +286,191 @@ def test_download_by_resolution_not_exists(youtube, stream_query):
|
|
228 |
|
229 |
|
230 |
@mock.patch("pytube.cli.YouTube")
|
231 |
-
@mock.patch("pytube.cli.
|
232 |
-
def
|
|
|
233 |
parser = argparse.ArgumentParser()
|
234 |
args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-f", "best"])
|
235 |
cli._parse_args = MagicMock(return_value=args)
|
236 |
-
|
237 |
-
cli.
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
242 |
|
243 |
|
|
|
244 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
245 |
-
def
|
|
|
246 |
parser = argparse.ArgumentParser()
|
247 |
args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-a", "mp4"])
|
248 |
cli._parse_args = MagicMock(return_value=args)
|
249 |
-
|
250 |
cli.main()
|
|
|
251 |
youtube.assert_called()
|
252 |
-
|
253 |
|
254 |
|
255 |
-
@mock.patch("pytube.cli.
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
264 |
|
265 |
|
266 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
@@ -272,3 +482,14 @@ def test_perform_args_on_youtube(youtube):
|
|
272 |
cli.main()
|
273 |
youtube.assert_called()
|
274 |
cli._perform_args_on_youtube.assert_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
import pytest
|
7 |
|
8 |
from pytube import cli, StreamQuery, Caption, CaptionQuery
|
9 |
+
from pytube.exceptions import PytubeError
|
10 |
|
11 |
parse_args = cli._parse_args
|
12 |
|
13 |
|
14 |
+
@mock.patch("pytube.cli._parse_args")
|
15 |
+
def test_main_invalid_url(_parse_args):
|
16 |
+
parser = argparse.ArgumentParser()
|
17 |
+
args = parse_args(parser, ["crikey",],)
|
18 |
+
_parse_args.return_value = args
|
19 |
+
with pytest.raises(SystemExit):
|
20 |
+
cli.main()
|
21 |
+
|
22 |
+
|
23 |
@mock.patch("pytube.cli.display_streams")
|
24 |
@mock.patch("pytube.cli.YouTube")
|
25 |
def test_download_when_itag_not_found(youtube, display_streams):
|
|
|
102 |
print_available.assert_called_with(youtube.captions)
|
103 |
|
104 |
|
105 |
+
def test_print_available_captions(capsys):
|
106 |
+
# Given
|
107 |
+
caption1 = Caption(
|
108 |
+
{"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
|
109 |
+
)
|
110 |
+
caption2 = Caption(
|
111 |
+
{"url": "url2", "name": {"simpleText": "name2"}, "languageCode": "fr"}
|
112 |
+
)
|
113 |
+
query = CaptionQuery([caption1, caption2])
|
114 |
+
# When
|
115 |
+
cli._print_available_captions(query)
|
116 |
+
# Then
|
117 |
+
captured = capsys.readouterr()
|
118 |
+
assert captured.out == "Available caption codes are: en, fr\n"
|
119 |
+
|
120 |
+
|
121 |
def test_display_progress_bar(capsys):
|
122 |
cli.display_progress_bar(bytes_received=25, filesize=100, scale=0.55)
|
123 |
out, _ = capsys.readouterr()
|
|
|
219 |
download_by_resolution.assert_called()
|
220 |
|
221 |
|
222 |
+
@mock.patch("pytube.cli.YouTube")
|
223 |
@mock.patch("pytube.cli.Playlist")
|
224 |
+
@mock.patch("pytube.cli._perform_args_on_youtube")
|
225 |
+
def test_download_with_playlist(perform_args_on_youtube, playlist, youtube):
|
226 |
+
# Given
|
227 |
cli.safe_filename = MagicMock(return_value="safe_title")
|
228 |
parser = argparse.ArgumentParser()
|
229 |
args = parse_args(parser, ["https://www.youtube.com/playlist?list=PLyn"])
|
230 |
cli._parse_args = MagicMock(return_value=args)
|
231 |
+
videos = [youtube]
|
232 |
+
playlist_instance = playlist.return_value
|
233 |
+
playlist_instance.videos = videos
|
234 |
+
# When
|
235 |
cli.main()
|
236 |
+
# Then
|
237 |
playlist.assert_called()
|
238 |
+
perform_args_on_youtube.assert_called_with(youtube, args)
|
239 |
+
|
240 |
+
|
241 |
+
@mock.patch("pytube.cli.YouTube")
|
242 |
+
@mock.patch("pytube.cli.Playlist")
|
243 |
+
@mock.patch("pytube.cli._perform_args_on_youtube")
|
244 |
+
def test_download_with_playlist_video_error(
|
245 |
+
perform_args_on_youtube, playlist, youtube, capsys
|
246 |
+
):
|
247 |
+
# Given
|
248 |
+
cli.safe_filename = MagicMock(return_value="safe_title")
|
249 |
+
parser = argparse.ArgumentParser()
|
250 |
+
args = parse_args(parser, ["https://www.youtube.com/playlist?list=PLyn"])
|
251 |
+
cli._parse_args = MagicMock(return_value=args)
|
252 |
+
videos = [youtube]
|
253 |
+
playlist_instance = playlist.return_value
|
254 |
+
playlist_instance.videos = videos
|
255 |
+
perform_args_on_youtube.side_effect = PytubeError()
|
256 |
+
# When
|
257 |
+
cli.main()
|
258 |
+
# Then
|
259 |
+
playlist.assert_called()
|
260 |
+
captured = capsys.readouterr()
|
261 |
+
assert "There was an error with video" in captured.out
|
262 |
|
263 |
|
264 |
@mock.patch("pytube.cli.YouTube")
|
|
|
286 |
|
287 |
|
288 |
@mock.patch("pytube.cli.YouTube")
|
289 |
+
@mock.patch("pytube.cli.ffmpeg_process")
|
290 |
+
def test_perform_args_should_ffmpeg_process(ffmpeg_process, youtube):
|
291 |
+
# Given
|
292 |
parser = argparse.ArgumentParser()
|
293 |
args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-f", "best"])
|
294 |
cli._parse_args = MagicMock(return_value=args)
|
295 |
+
# When
|
296 |
+
cli._perform_args_on_youtube(youtube, args)
|
297 |
+
# Then
|
298 |
+
ffmpeg_process.assert_called_with(youtube=youtube, resolution="best", target=None)
|
299 |
+
|
300 |
+
|
301 |
+
@mock.patch("pytube.cli.YouTube")
|
302 |
+
@mock.patch("pytube.cli._ffmpeg_downloader")
|
303 |
+
def test_ffmpeg_process_best_should_download(_ffmpeg_downloader, youtube):
|
304 |
+
# Given
|
305 |
+
target = "/target"
|
306 |
+
streams = MagicMock()
|
307 |
+
youtube.streams = streams
|
308 |
+
video_stream = MagicMock()
|
309 |
+
streams.filter.return_value.order_by.return_value.last.return_value = video_stream
|
310 |
+
audio_stream = MagicMock()
|
311 |
+
streams.get_audio_only.return_value = audio_stream
|
312 |
+
# When
|
313 |
+
cli.ffmpeg_process(youtube, "best", target)
|
314 |
+
# Then
|
315 |
+
_ffmpeg_downloader.assert_called_with(
|
316 |
+
audio_stream=audio_stream, video_stream=video_stream, target=target
|
317 |
+
)
|
318 |
+
|
319 |
+
|
320 |
+
@mock.patch("pytube.cli.YouTube")
|
321 |
+
@mock.patch("pytube.cli._ffmpeg_downloader")
|
322 |
+
def test_ffmpeg_process_res_should_download(_ffmpeg_downloader, youtube):
|
323 |
+
# Given
|
324 |
+
target = "/target"
|
325 |
+
streams = MagicMock()
|
326 |
+
youtube.streams = streams
|
327 |
+
video_stream = MagicMock()
|
328 |
+
streams.filter.return_value.first.return_value = video_stream
|
329 |
+
audio_stream = MagicMock()
|
330 |
+
streams.get_audio_only.return_value = audio_stream
|
331 |
+
# When
|
332 |
+
cli.ffmpeg_process(youtube, "XYZp", target)
|
333 |
+
# Then
|
334 |
+
_ffmpeg_downloader.assert_called_with(
|
335 |
+
audio_stream=audio_stream, video_stream=video_stream, target=target
|
336 |
+
)
|
337 |
+
|
338 |
+
|
339 |
+
@mock.patch("pytube.cli.YouTube")
|
340 |
+
@mock.patch("pytube.cli._ffmpeg_downloader")
|
341 |
+
def test_ffmpeg_process_res_none_should_not_download(_ffmpeg_downloader, youtube):
|
342 |
+
# Given
|
343 |
+
target = "/target"
|
344 |
+
streams = MagicMock()
|
345 |
+
youtube.streams = streams
|
346 |
+
streams.filter.return_value.first.return_value = None
|
347 |
+
audio_stream = MagicMock()
|
348 |
+
streams.get_audio_only.return_value = audio_stream
|
349 |
+
# When
|
350 |
+
with pytest.raises(SystemExit):
|
351 |
+
cli.ffmpeg_process(youtube, "XYZp", target)
|
352 |
+
# Then
|
353 |
+
_ffmpeg_downloader.assert_not_called()
|
354 |
+
|
355 |
+
|
356 |
+
@mock.patch("pytube.cli.YouTube")
|
357 |
+
@mock.patch("pytube.cli._ffmpeg_downloader")
|
358 |
+
def test_ffmpeg_process_audio_none_should_fallback_download(
|
359 |
+
_ffmpeg_downloader, youtube
|
360 |
+
):
|
361 |
+
# Given
|
362 |
+
target = "/target"
|
363 |
+
streams = MagicMock()
|
364 |
+
youtube.streams = streams
|
365 |
+
stream = MagicMock()
|
366 |
+
streams.filter.return_value.order_by.return_value.last.return_value = stream
|
367 |
+
streams.get_audio_only.return_value = None
|
368 |
+
# When
|
369 |
+
cli.ffmpeg_process(youtube, "best", target)
|
370 |
+
# Then
|
371 |
+
_ffmpeg_downloader.assert_called_with(
|
372 |
+
audio_stream=stream, video_stream=stream, target=target
|
373 |
+
)
|
374 |
+
|
375 |
+
|
376 |
+
@mock.patch("pytube.cli.YouTube")
|
377 |
+
@mock.patch("pytube.cli._ffmpeg_downloader")
|
378 |
+
def test_ffmpeg_process_audio_fallback_none_should_exit(_ffmpeg_downloader, youtube):
|
379 |
+
# Given
|
380 |
+
target = "/target"
|
381 |
+
streams = MagicMock()
|
382 |
+
youtube.streams = streams
|
383 |
+
stream = MagicMock()
|
384 |
+
streams.filter.return_value.order_by.return_value.last.side_effect = [
|
385 |
+
stream,
|
386 |
+
stream,
|
387 |
+
None,
|
388 |
+
]
|
389 |
+
streams.get_audio_only.return_value = None
|
390 |
+
# When
|
391 |
+
with pytest.raises(SystemExit):
|
392 |
+
cli.ffmpeg_process(youtube, "best", target)
|
393 |
+
# Then
|
394 |
+
_ffmpeg_downloader.assert_not_called()
|
395 |
+
|
396 |
+
|
397 |
+
@mock.patch("pytube.cli.os.unlink", return_value=None)
|
398 |
+
@mock.patch("pytube.cli.subprocess.run", return_value=None)
|
399 |
+
@mock.patch("pytube.cli._download", return_value=None)
|
400 |
+
@mock.patch("pytube.cli._unique_name", return_value=None)
|
401 |
+
def test_ffmpeg_downloader(unique_name, download, run, unlink):
|
402 |
+
# Given
|
403 |
+
target = "target"
|
404 |
+
audio_stream = MagicMock()
|
405 |
+
video_stream = MagicMock()
|
406 |
+
video_stream.id = "video_id"
|
407 |
+
audio_stream.subtype = "audio_subtype"
|
408 |
+
video_stream.subtype = "video_subtype"
|
409 |
+
unique_name.side_effect = ["video_name", "audio_name"]
|
410 |
+
|
411 |
+
# When
|
412 |
+
cli._ffmpeg_downloader(
|
413 |
+
audio_stream=audio_stream, video_stream=video_stream, target=target
|
414 |
+
)
|
415 |
+
# Then
|
416 |
+
download.assert_called()
|
417 |
+
run.assert_called_with(
|
418 |
+
[
|
419 |
+
"ffmpeg",
|
420 |
+
"-i",
|
421 |
+
"target/video_name.video_subtype",
|
422 |
+
"-i",
|
423 |
+
"target/audio_name.audio_subtype",
|
424 |
+
"-codec",
|
425 |
+
"copy",
|
426 |
+
"target/safe_title.video_subtype",
|
427 |
+
]
|
428 |
+
)
|
429 |
+
unlink.assert_called()
|
430 |
|
431 |
|
432 |
+
@mock.patch("pytube.cli.download_audio")
|
433 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
434 |
+
def test_download_audio_args(youtube, download_audio):
|
435 |
+
# Given
|
436 |
parser = argparse.ArgumentParser()
|
437 |
args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-a", "mp4"])
|
438 |
cli._parse_args = MagicMock(return_value=args)
|
439 |
+
# When
|
440 |
cli.main()
|
441 |
+
# Then
|
442 |
youtube.assert_called()
|
443 |
+
download_audio.assert_called()
|
444 |
|
445 |
|
446 |
+
@mock.patch("pytube.cli._download")
|
447 |
+
@mock.patch("pytube.cli.YouTube")
|
448 |
+
def test_download_audio(youtube, download):
|
449 |
+
# Given
|
450 |
+
youtube_instance = youtube.return_value
|
451 |
+
audio_stream = MagicMock()
|
452 |
+
youtube_instance.streams.filter.return_value.order_by.return_value.last.return_value = (
|
453 |
+
audio_stream
|
454 |
+
)
|
455 |
+
# When
|
456 |
+
cli.download_audio(youtube_instance, "filetype", "target")
|
457 |
+
# Then
|
458 |
+
download.assert_called_with(audio_stream, target="target")
|
459 |
+
|
460 |
+
|
461 |
+
@mock.patch("pytube.cli._download")
|
462 |
+
@mock.patch("pytube.cli.YouTube")
|
463 |
+
def test_download_audio_none(youtube, download):
|
464 |
+
# Given
|
465 |
+
youtube_instance = youtube.return_value
|
466 |
+
youtube_instance.streams.filter.return_value.order_by.return_value.last.return_value = (
|
467 |
+
None
|
468 |
+
)
|
469 |
+
# When
|
470 |
+
with pytest.raises(SystemExit):
|
471 |
+
cli.download_audio(youtube_instance, "filetype", "target")
|
472 |
+
# Then
|
473 |
+
download.assert_not_called()
|
474 |
|
475 |
|
476 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
|
|
482 |
cli.main()
|
483 |
youtube.assert_called()
|
484 |
cli._perform_args_on_youtube.assert_called()
|
485 |
+
|
486 |
+
|
487 |
+
@mock.patch("pytube.cli.os.path.exists", return_value=False)
|
488 |
+
def test_unique_name(path_exists):
|
489 |
+
assert cli._unique_name("base", "subtype", "video", "target") == "base_video_0"
|
490 |
+
|
491 |
+
|
492 |
+
@mock.patch("pytube.cli.os.path.exists")
|
493 |
+
def test_unique_name_counter(path_exists):
|
494 |
+
path_exists.side_effect = [True, False]
|
495 |
+
assert cli._unique_name("base", "subtype", "video", "target") == "base_video_1"
|
tests/test_query.py
CHANGED
@@ -44,14 +44,14 @@ def test_get_last(cipher_signature):
|
|
44 |
"""Ensure :meth:`~pytube.StreamQuery.last` returns the expected
|
45 |
:class:`Stream <Stream>`.
|
46 |
"""
|
47 |
-
assert cipher_signature.streams.
|
48 |
|
49 |
|
50 |
def test_get_first(cipher_signature):
|
51 |
"""Ensure :meth:`~pytube.StreamQuery.first` returns the expected
|
52 |
:class:`Stream <Stream>`.
|
53 |
"""
|
54 |
-
assert cipher_signature.streams.
|
55 |
|
56 |
|
57 |
def test_order_by(cipher_signature):
|
@@ -154,10 +154,10 @@ def test_sequence(cipher_signature):
|
|
154 |
|
155 |
|
156 |
def test_otf(cipher_signature):
|
157 |
-
non_otf = cipher_signature.streams.otf()
|
158 |
assert len(non_otf) == 22
|
159 |
|
160 |
-
otf = cipher_signature.streams.otf(True)
|
161 |
assert len(otf) == 0
|
162 |
|
163 |
|
|
|
44 |
"""Ensure :meth:`~pytube.StreamQuery.last` returns the expected
|
45 |
:class:`Stream <Stream>`.
|
46 |
"""
|
47 |
+
assert cipher_signature.streams[-1].itag == 251
|
48 |
|
49 |
|
50 |
def test_get_first(cipher_signature):
|
51 |
"""Ensure :meth:`~pytube.StreamQuery.first` returns the expected
|
52 |
:class:`Stream <Stream>`.
|
53 |
"""
|
54 |
+
assert cipher_signature.streams[0].itag == 18
|
55 |
|
56 |
|
57 |
def test_order_by(cipher_signature):
|
|
|
154 |
|
155 |
|
156 |
def test_otf(cipher_signature):
|
157 |
+
non_otf = cipher_signature.streams.otf()
|
158 |
assert len(non_otf) == 22
|
159 |
|
160 |
+
otf = cipher_signature.streams.otf(True)
|
161 |
assert len(otf) == 0
|
162 |
|
163 |
|
tests/test_streams.py
CHANGED
@@ -11,13 +11,13 @@ from pytube import Stream, streams
|
|
11 |
def test_filesize(cipher_signature, mocker):
|
12 |
mocker.patch.object(request, "head")
|
13 |
request.head.return_value = {"content-length": "6796391"}
|
14 |
-
assert cipher_signature.streams.
|
15 |
|
16 |
|
17 |
def test_filesize_approx(cipher_signature, mocker):
|
18 |
mocker.patch.object(request, "head")
|
19 |
request.head.return_value = {"content-length": "123"}
|
20 |
-
stream = cipher_signature.streams
|
21 |
assert stream.filesize_approx == 22350604
|
22 |
stream.bitrate = None
|
23 |
assert stream.filesize_approx == 123
|
@@ -25,7 +25,7 @@ def test_filesize_approx(cipher_signature, mocker):
|
|
25 |
|
26 |
def test_default_filename(cipher_signature):
|
27 |
expected = "PSY - GANGNAM STYLE(강남스타일) MV.mp4"
|
28 |
-
stream = cipher_signature.streams
|
29 |
assert stream.default_filename == expected
|
30 |
|
31 |
|
@@ -103,7 +103,7 @@ def test_download(cipher_signature, mocker):
|
|
103 |
mocker.patch.object(request, "stream")
|
104 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
105 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
106 |
-
stream = cipher_signature.streams
|
107 |
stream.download()
|
108 |
|
109 |
|
@@ -114,7 +114,7 @@ def test_download_with_prefix(cipher_signature, mocker):
|
|
114 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
115 |
streams.target_directory = MagicMock(return_value="/target")
|
116 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
117 |
-
stream = cipher_signature.streams
|
118 |
file_path = stream.download(filename_prefix="prefix")
|
119 |
assert file_path == "/target/prefixPSY - GANGNAM STYLE(강남스타일) MV.mp4"
|
120 |
|
@@ -126,7 +126,7 @@ def test_download_with_filename(cipher_signature, mocker):
|
|
126 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
127 |
streams.target_directory = MagicMock(return_value="/target")
|
128 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
129 |
-
stream = cipher_signature.streams
|
130 |
file_path = stream.download(filename="cool name bro")
|
131 |
assert file_path == "/target/cool name bro.mp4"
|
132 |
|
@@ -139,7 +139,7 @@ def test_download_with_existing(cipher_signature, mocker):
|
|
139 |
mocker.patch.object(os.path, "isfile")
|
140 |
os.path.isfile.return_value = True
|
141 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
142 |
-
stream = cipher_signature.streams
|
143 |
mocker.patch.object(os.path, "getsize")
|
144 |
os.path.getsize.return_value = stream.filesize
|
145 |
file_path = stream.download()
|
@@ -156,7 +156,7 @@ def test_download_with_existing_no_skip(cipher_signature, mocker):
|
|
156 |
mocker.patch.object(os.path, "isfile")
|
157 |
os.path.isfile.return_value = True
|
158 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
159 |
-
stream = cipher_signature.streams
|
160 |
mocker.patch.object(os.path, "getsize")
|
161 |
os.path.getsize.return_value = stream.filesize
|
162 |
file_path = stream.download(skip_existing=False)
|
@@ -165,12 +165,12 @@ def test_download_with_existing_no_skip(cipher_signature, mocker):
|
|
165 |
|
166 |
|
167 |
def test_progressive_streams_return_includes_audio_track(cipher_signature):
|
168 |
-
stream = cipher_signature.streams.filter(progressive=True)
|
169 |
assert stream.includes_audio_track
|
170 |
|
171 |
|
172 |
def test_progressive_streams_return_includes_video_track(cipher_signature):
|
173 |
-
stream = cipher_signature.streams.filter(progressive=True)
|
174 |
assert stream.includes_video_track
|
175 |
|
176 |
|
@@ -184,7 +184,7 @@ def test_on_progress_hook(cipher_signature, mocker):
|
|
184 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
185 |
|
186 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
187 |
-
stream = cipher_signature.streams
|
188 |
stream.download()
|
189 |
assert callback_fn.called
|
190 |
args, _ = callback_fn.call_args
|
@@ -203,7 +203,7 @@ def test_on_complete_hook(cipher_signature, mocker):
|
|
203 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
204 |
|
205 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
206 |
-
stream = cipher_signature.streams
|
207 |
stream.download()
|
208 |
assert callback_fn.called
|
209 |
|
@@ -233,7 +233,7 @@ def test_thumbnail_when_not_in_details(cipher_signature):
|
|
233 |
|
234 |
|
235 |
def test_repr_for_audio_streams(cipher_signature):
|
236 |
-
stream = str(cipher_signature.streams.filter(only_audio=True)
|
237 |
expected = (
|
238 |
'<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" '
|
239 |
'acodec="mp4a.40.2" progressive="False" type="audio">'
|
@@ -242,7 +242,7 @@ def test_repr_for_audio_streams(cipher_signature):
|
|
242 |
|
243 |
|
244 |
def test_repr_for_video_streams(cipher_signature):
|
245 |
-
stream = str(cipher_signature.streams.filter(only_video=True)
|
246 |
expected = (
|
247 |
'<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" '
|
248 |
'vcodec="avc1.640028" progressive="False" type="video">'
|
@@ -251,7 +251,7 @@ def test_repr_for_video_streams(cipher_signature):
|
|
251 |
|
252 |
|
253 |
def test_repr_for_progressive_streams(cipher_signature):
|
254 |
-
stream = str(cipher_signature.streams.filter(progressive=True)
|
255 |
expected = (
|
256 |
'<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" '
|
257 |
'vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">'
|
@@ -260,7 +260,7 @@ def test_repr_for_progressive_streams(cipher_signature):
|
|
260 |
|
261 |
|
262 |
def test_repr_for_adaptive_streams(cipher_signature):
|
263 |
-
stream = str(cipher_signature.streams.filter(adaptive=True)
|
264 |
expected = (
|
265 |
'<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" '
|
266 |
'vcodec="avc1.640028" progressive="False" type="video">'
|
|
|
11 |
def test_filesize(cipher_signature, mocker):
|
12 |
mocker.patch.object(request, "head")
|
13 |
request.head.return_value = {"content-length": "6796391"}
|
14 |
+
assert cipher_signature.streams[0].filesize == 6796391
|
15 |
|
16 |
|
17 |
def test_filesize_approx(cipher_signature, mocker):
|
18 |
mocker.patch.object(request, "head")
|
19 |
request.head.return_value = {"content-length": "123"}
|
20 |
+
stream = cipher_signature.streams[0]
|
21 |
assert stream.filesize_approx == 22350604
|
22 |
stream.bitrate = None
|
23 |
assert stream.filesize_approx == 123
|
|
|
25 |
|
26 |
def test_default_filename(cipher_signature):
|
27 |
expected = "PSY - GANGNAM STYLE(강남스타일) MV.mp4"
|
28 |
+
stream = cipher_signature.streams[0]
|
29 |
assert stream.default_filename == expected
|
30 |
|
31 |
|
|
|
103 |
mocker.patch.object(request, "stream")
|
104 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
105 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
106 |
+
stream = cipher_signature.streams[0]
|
107 |
stream.download()
|
108 |
|
109 |
|
|
|
114 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
115 |
streams.target_directory = MagicMock(return_value="/target")
|
116 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
117 |
+
stream = cipher_signature.streams[0]
|
118 |
file_path = stream.download(filename_prefix="prefix")
|
119 |
assert file_path == "/target/prefixPSY - GANGNAM STYLE(강남스타일) MV.mp4"
|
120 |
|
|
|
126 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
127 |
streams.target_directory = MagicMock(return_value="/target")
|
128 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
129 |
+
stream = cipher_signature.streams[0]
|
130 |
file_path = stream.download(filename="cool name bro")
|
131 |
assert file_path == "/target/cool name bro.mp4"
|
132 |
|
|
|
139 |
mocker.patch.object(os.path, "isfile")
|
140 |
os.path.isfile.return_value = True
|
141 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
142 |
+
stream = cipher_signature.streams[0]
|
143 |
mocker.patch.object(os.path, "getsize")
|
144 |
os.path.getsize.return_value = stream.filesize
|
145 |
file_path = stream.download()
|
|
|
156 |
mocker.patch.object(os.path, "isfile")
|
157 |
os.path.isfile.return_value = True
|
158 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
159 |
+
stream = cipher_signature.streams[0]
|
160 |
mocker.patch.object(os.path, "getsize")
|
161 |
os.path.getsize.return_value = stream.filesize
|
162 |
file_path = stream.download(skip_existing=False)
|
|
|
165 |
|
166 |
|
167 |
def test_progressive_streams_return_includes_audio_track(cipher_signature):
|
168 |
+
stream = cipher_signature.streams.filter(progressive=True)[0]
|
169 |
assert stream.includes_audio_track
|
170 |
|
171 |
|
172 |
def test_progressive_streams_return_includes_video_track(cipher_signature):
|
173 |
+
stream = cipher_signature.streams.filter(progressive=True)[0]
|
174 |
assert stream.includes_video_track
|
175 |
|
176 |
|
|
|
184 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
185 |
|
186 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
187 |
+
stream = cipher_signature.streams[0]
|
188 |
stream.download()
|
189 |
assert callback_fn.called
|
190 |
args, _ = callback_fn.call_args
|
|
|
203 |
request.stream.return_value = iter([str(random.getrandbits(8 * 1024))])
|
204 |
|
205 |
with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
|
206 |
+
stream = cipher_signature.streams[0]
|
207 |
stream.download()
|
208 |
assert callback_fn.called
|
209 |
|
|
|
233 |
|
234 |
|
235 |
def test_repr_for_audio_streams(cipher_signature):
|
236 |
+
stream = str(cipher_signature.streams.filter(only_audio=True)[0])
|
237 |
expected = (
|
238 |
'<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" '
|
239 |
'acodec="mp4a.40.2" progressive="False" type="audio">'
|
|
|
242 |
|
243 |
|
244 |
def test_repr_for_video_streams(cipher_signature):
|
245 |
+
stream = str(cipher_signature.streams.filter(only_video=True)[0])
|
246 |
expected = (
|
247 |
'<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" '
|
248 |
'vcodec="avc1.640028" progressive="False" type="video">'
|
|
|
251 |
|
252 |
|
253 |
def test_repr_for_progressive_streams(cipher_signature):
|
254 |
+
stream = str(cipher_signature.streams.filter(progressive=True)[0])
|
255 |
expected = (
|
256 |
'<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" '
|
257 |
'vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">'
|
|
|
260 |
|
261 |
|
262 |
def test_repr_for_adaptive_streams(cipher_signature):
|
263 |
+
stream = str(cipher_signature.streams.filter(adaptive=True)[0])
|
264 |
expected = (
|
265 |
'<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" '
|
266 |
'vcodec="avc1.640028" progressive="False" type="video">'
|