CaptionQuery and StreamQuery implement Sequence
Browse files- pytube/cli.py +2 -2
- pytube/contrib/playlist.py +1 -1
- pytube/query.py +21 -4
- tests/contrib/test_playlist.py +1 -0
- tests/test_captions.py +3 -1
- tests/test_query.py +12 -23
- tests/test_streams.py +1 -1
pytube/cli.py
CHANGED
@@ -413,12 +413,12 @@ def display_streams(youtube: YouTube) -> None:
|
|
413 |
A valid YouTube watch URL.
|
414 |
|
415 |
"""
|
416 |
-
for stream in youtube.streams
|
417 |
print(stream)
|
418 |
|
419 |
|
420 |
def _print_available_captions(captions: CaptionQuery) -> None:
|
421 |
-
print(f"Available caption codes are: {', '.join(c.code for c in captions
|
422 |
|
423 |
|
424 |
def download_caption(
|
|
|
413 |
A valid YouTube watch URL.
|
414 |
|
415 |
"""
|
416 |
+
for stream in youtube.streams:
|
417 |
print(stream)
|
418 |
|
419 |
|
420 |
def _print_available_captions(captions: CaptionQuery) -> None:
|
421 |
+
print(f"Available caption codes are: {', '.join(c.code for c in captions)}")
|
422 |
|
423 |
|
424 |
def download_caption(
|
pytube/contrib/playlist.py
CHANGED
@@ -143,7 +143,7 @@ class Playlist(Sequence):
|
|
143 |
"""
|
144 |
yield from (YouTube(url) for url in self.video_urls)
|
145 |
|
146 |
-
def __getitem__(self, i: Union[slice, int]):
|
147 |
return self.video_urls[i]
|
148 |
|
149 |
def __len__(self) -> int:
|
|
|
143 |
"""
|
144 |
yield from (YouTube(url) for url in self.video_urls)
|
145 |
|
146 |
+
def __getitem__(self, i: Union[slice, int]) -> Union[str, List[str]]:
|
147 |
return self.video_urls[i]
|
148 |
|
149 |
def __len__(self) -> int:
|
pytube/query.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
|
3 |
"""This module provides a query interface for media streams and captions."""
|
4 |
-
from typing import List, Optional
|
|
|
5 |
|
6 |
from pytube import Stream, Caption
|
|
|
7 |
|
8 |
|
9 |
-
class StreamQuery:
|
10 |
"""Interface for querying the available media streams."""
|
11 |
|
12 |
def __init__(self, fmt_streams):
|
@@ -313,14 +315,16 @@ class StreamQuery:
|
|
313 |
except IndexError:
|
314 |
pass
|
315 |
|
|
|
316 |
def count(self) -> int:
|
317 |
"""Get the count the query would return.
|
318 |
|
319 |
:rtype: int
|
320 |
|
321 |
"""
|
322 |
-
return len(self
|
323 |
|
|
|
324 |
def all(self) -> List[Stream]:
|
325 |
"""Get all the results represented by this query as a list.
|
326 |
|
@@ -329,8 +333,14 @@ class StreamQuery:
|
|
329 |
"""
|
330 |
return self.fmt_streams
|
331 |
|
|
|
|
|
|
|
|
|
|
|
|
|
332 |
|
333 |
-
class CaptionQuery:
|
334 |
"""Interface for querying the available captions."""
|
335 |
|
336 |
def __init__(self, captions: List[Caption]):
|
@@ -355,6 +365,7 @@ class CaptionQuery:
|
|
355 |
"""
|
356 |
return self.lang_code_index.get(lang_code)
|
357 |
|
|
|
358 |
def all(self) -> List[Caption]:
|
359 |
"""Get all the results represented by this query as a list.
|
360 |
|
@@ -362,3 +373,9 @@ class CaptionQuery:
|
|
362 |
|
363 |
"""
|
364 |
return self.captions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
|
3 |
"""This module provides a query interface for media streams and captions."""
|
4 |
+
from typing import List, Optional, Union
|
5 |
+
from collections.abc import Sequence
|
6 |
|
7 |
from pytube import Stream, Caption
|
8 |
+
from pytube.helpers import deprecated
|
9 |
|
10 |
|
11 |
+
class StreamQuery(Sequence):
|
12 |
"""Interface for querying the available media streams."""
|
13 |
|
14 |
def __init__(self, fmt_streams):
|
|
|
315 |
except IndexError:
|
316 |
pass
|
317 |
|
318 |
+
@deprecated("Get the size of this list directly using len()")
|
319 |
def count(self) -> int:
|
320 |
"""Get the count the query would return.
|
321 |
|
322 |
:rtype: int
|
323 |
|
324 |
"""
|
325 |
+
return len(self)
|
326 |
|
327 |
+
@deprecated("This object can be treated as a list, all() is useless")
|
328 |
def all(self) -> List[Stream]:
|
329 |
"""Get all the results represented by this query as a list.
|
330 |
|
|
|
333 |
"""
|
334 |
return self.fmt_streams
|
335 |
|
336 |
+
def __getitem__(self, i: Union[slice, int]):
|
337 |
+
return self.fmt_streams[i]
|
338 |
+
|
339 |
+
def __len__(self) -> int:
|
340 |
+
return len(self.fmt_streams)
|
341 |
+
|
342 |
|
343 |
+
class CaptionQuery(Sequence):
|
344 |
"""Interface for querying the available captions."""
|
345 |
|
346 |
def __init__(self, captions: List[Caption]):
|
|
|
365 |
"""
|
366 |
return self.lang_code_index.get(lang_code)
|
367 |
|
368 |
+
@deprecated("This object can be treated as a list, all() is useless")
|
369 |
def all(self) -> List[Caption]:
|
370 |
"""Get all the results represented by this query as a list.
|
371 |
|
|
|
373 |
|
374 |
"""
|
375 |
return self.captions
|
376 |
+
|
377 |
+
def __getitem__(self, i: Union[slice, int]):
|
378 |
+
return self.captions[i]
|
379 |
+
|
380 |
+
def __len__(self) -> int:
|
381 |
+
return len(self.captions)
|
tests/contrib/test_playlist.py
CHANGED
@@ -90,6 +90,7 @@ def test_sequence(request_get, playlist_html):
|
|
90 |
assert playlist[0] == "https://www.youtube.com/watch?v=ujTCoH21GlA"
|
91 |
assert len(playlist) == 12
|
92 |
|
|
|
93 |
@mock.patch("pytube.contrib.playlist.request.get")
|
94 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
95 |
def test_videos(youtube, request_get, playlist_html):
|
|
|
90 |
assert playlist[0] == "https://www.youtube.com/watch?v=ujTCoH21GlA"
|
91 |
assert len(playlist) == 12
|
92 |
|
93 |
+
|
94 |
@mock.patch("pytube.contrib.playlist.request.get")
|
95 |
@mock.patch("pytube.cli.YouTube.__init__", return_value=None)
|
96 |
def test_videos(youtube, request_get, playlist_html):
|
tests/test_captions.py
CHANGED
@@ -12,7 +12,7 @@ def test_float_to_srt_time_format():
|
|
12 |
assert caption1.float_to_srt_time_format(3.89) == "00:00:03,890"
|
13 |
|
14 |
|
15 |
-
def
|
16 |
caption1 = Caption(
|
17 |
{"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
|
18 |
)
|
@@ -21,6 +21,8 @@ def test_caption_query_all():
|
|
21 |
)
|
22 |
caption_query = CaptionQuery(captions=[caption1, caption2])
|
23 |
assert caption_query.captions == [caption1, caption2]
|
|
|
|
|
24 |
|
25 |
|
26 |
def test_caption_query_get_by_language_code_when_exists():
|
|
|
12 |
assert caption1.float_to_srt_time_format(3.89) == "00:00:03,890"
|
13 |
|
14 |
|
15 |
+
def test_caption_query_sequence():
|
16 |
caption1 = Caption(
|
17 |
{"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
|
18 |
)
|
|
|
21 |
)
|
22 |
caption_query = CaptionQuery(captions=[caption1, caption2])
|
23 |
assert caption_query.captions == [caption1, caption2]
|
24 |
+
assert len(caption_query) == 2
|
25 |
+
assert caption_query[0] == caption1
|
26 |
|
27 |
|
28 |
def test_caption_query_get_by_language_code_when_exists():
|
tests/test_query.py
CHANGED
@@ -3,11 +3,6 @@
|
|
3 |
import pytest
|
4 |
|
5 |
|
6 |
-
def test_count(cipher_signature):
|
7 |
-
"""Ensure :meth:`~pytube.StreamQuery.count` returns an accurate amount."""
|
8 |
-
assert cipher_signature.streams.count() == 22
|
9 |
-
|
10 |
-
|
11 |
@pytest.mark.parametrize(
|
12 |
("test_input", "expected"),
|
13 |
[
|
@@ -30,7 +25,7 @@ def test_count(cipher_signature):
|
|
30 |
)
|
31 |
def test_filters(test_input, expected, cipher_signature):
|
32 |
"""Ensure filters produce the expected results."""
|
33 |
-
result = [s.itag for s in cipher_signature.streams.filter(**test_input)
|
34 |
assert result == expected
|
35 |
|
36 |
|
@@ -64,8 +59,7 @@ def test_order_by(cipher_signature):
|
|
64 |
:class:`Stream <Stream>` instances in the expected order.
|
65 |
"""
|
66 |
itags = [
|
67 |
-
s.itag
|
68 |
-
for s in cipher_signature.streams.filter(type="audio").order_by("itag").all()
|
69 |
]
|
70 |
assert itags == [140, 249, 250, 251]
|
71 |
|
@@ -77,10 +71,7 @@ def test_order_by_descending(cipher_signature):
|
|
77 |
# numerical values
|
78 |
itags = [
|
79 |
s.itag
|
80 |
-
for s in cipher_signature.streams.filter(type="audio")
|
81 |
-
.order_by("itag")
|
82 |
-
.desc()
|
83 |
-
.all()
|
84 |
]
|
85 |
assert itags == [251, 250, 249, 140]
|
86 |
|
@@ -91,7 +82,6 @@ def test_order_by_non_numerical(cipher_signature):
|
|
91 |
for s in cipher_signature.streams.filter(res="360p")
|
92 |
.order_by("mime_type")
|
93 |
.desc()
|
94 |
-
.all()
|
95 |
]
|
96 |
assert mime_types == ["video/webm", "video/mp4", "video/mp4"]
|
97 |
|
@@ -103,10 +93,7 @@ def test_order_by_ascending(cipher_signature):
|
|
103 |
# numerical values
|
104 |
itags = [
|
105 |
s.itag
|
106 |
-
for s in cipher_signature.streams.filter(type="audio")
|
107 |
-
.order_by("itag")
|
108 |
-
.asc()
|
109 |
-
.all()
|
110 |
]
|
111 |
assert itags == [140, 249, 250, 251]
|
112 |
|
@@ -114,16 +101,13 @@ def test_order_by_ascending(cipher_signature):
|
|
114 |
def test_order_by_non_numerical_ascending(cipher_signature):
|
115 |
mime_types = [
|
116 |
s.mime_type
|
117 |
-
for s in cipher_signature.streams.filter(res="360p")
|
118 |
-
.order_by("mime_type")
|
119 |
-
.asc()
|
120 |
-
.all()
|
121 |
]
|
122 |
assert mime_types == ["video/mp4", "video/mp4", "video/webm"]
|
123 |
|
124 |
|
125 |
def test_order_by_with_none_values(cipher_signature):
|
126 |
-
abrs = [s.abr for s in cipher_signature.streams.order_by("abr").asc()
|
127 |
assert abrs == ["50kbps", "70kbps", "96kbps", "128kbps", "160kbps"]
|
128 |
|
129 |
|
@@ -151,7 +135,7 @@ def test_get_highest_resolution(cipher_signature):
|
|
151 |
|
152 |
|
153 |
def test_filter_is_dash(cipher_signature):
|
154 |
-
streams = cipher_signature.streams.filter(is_dash=False)
|
155 |
itags = [s.itag for s in streams]
|
156 |
assert itags == [18, 398, 397, 396, 395, 394]
|
157 |
|
@@ -162,3 +146,8 @@ def test_get_audio_only(cipher_signature):
|
|
162 |
|
163 |
def test_get_audio_only_with_subtype(cipher_signature):
|
164 |
assert cipher_signature.streams.get_audio_only(subtype="webm").itag == 251
|
|
|
|
|
|
|
|
|
|
|
|
3 |
import pytest
|
4 |
|
5 |
|
|
|
|
|
|
|
|
|
|
|
6 |
@pytest.mark.parametrize(
|
7 |
("test_input", "expected"),
|
8 |
[
|
|
|
25 |
)
|
26 |
def test_filters(test_input, expected, cipher_signature):
|
27 |
"""Ensure filters produce the expected results."""
|
28 |
+
result = [s.itag for s in cipher_signature.streams.filter(**test_input)]
|
29 |
assert result == expected
|
30 |
|
31 |
|
|
|
59 |
:class:`Stream <Stream>` instances in the expected order.
|
60 |
"""
|
61 |
itags = [
|
62 |
+
s.itag for s in cipher_signature.streams.filter(type="audio").order_by("itag")
|
|
|
63 |
]
|
64 |
assert itags == [140, 249, 250, 251]
|
65 |
|
|
|
71 |
# numerical values
|
72 |
itags = [
|
73 |
s.itag
|
74 |
+
for s in cipher_signature.streams.filter(type="audio").order_by("itag").desc()
|
|
|
|
|
|
|
75 |
]
|
76 |
assert itags == [251, 250, 249, 140]
|
77 |
|
|
|
82 |
for s in cipher_signature.streams.filter(res="360p")
|
83 |
.order_by("mime_type")
|
84 |
.desc()
|
|
|
85 |
]
|
86 |
assert mime_types == ["video/webm", "video/mp4", "video/mp4"]
|
87 |
|
|
|
93 |
# numerical values
|
94 |
itags = [
|
95 |
s.itag
|
96 |
+
for s in cipher_signature.streams.filter(type="audio").order_by("itag").asc()
|
|
|
|
|
|
|
97 |
]
|
98 |
assert itags == [140, 249, 250, 251]
|
99 |
|
|
|
101 |
def test_order_by_non_numerical_ascending(cipher_signature):
|
102 |
mime_types = [
|
103 |
s.mime_type
|
104 |
+
for s in cipher_signature.streams.filter(res="360p").order_by("mime_type").asc()
|
|
|
|
|
|
|
105 |
]
|
106 |
assert mime_types == ["video/mp4", "video/mp4", "video/webm"]
|
107 |
|
108 |
|
109 |
def test_order_by_with_none_values(cipher_signature):
|
110 |
+
abrs = [s.abr for s in cipher_signature.streams.order_by("abr").asc()]
|
111 |
assert abrs == ["50kbps", "70kbps", "96kbps", "128kbps", "160kbps"]
|
112 |
|
113 |
|
|
|
135 |
|
136 |
|
137 |
def test_filter_is_dash(cipher_signature):
|
138 |
+
streams = cipher_signature.streams.filter(is_dash=False)
|
139 |
itags = [s.itag for s in streams]
|
140 |
assert itags == [18, 398, 397, 396, 395, 394]
|
141 |
|
|
|
146 |
|
147 |
def test_get_audio_only_with_subtype(cipher_signature):
|
148 |
assert cipher_signature.streams.get_audio_only(subtype="webm").itag == 251
|
149 |
+
|
150 |
+
|
151 |
+
def test_sequence(cipher_signature):
|
152 |
+
assert len(cipher_signature.streams) == 22
|
153 |
+
assert cipher_signature.streams[0] is not None
|
tests/test_streams.py
CHANGED
@@ -41,7 +41,7 @@ def test_caption_tracks(presigned_video):
|
|
41 |
|
42 |
|
43 |
def test_captions(presigned_video):
|
44 |
-
assert len(presigned_video.captions
|
45 |
|
46 |
|
47 |
def test_description(cipher_signature):
|
|
|
41 |
|
42 |
|
43 |
def test_captions(presigned_video):
|
44 |
+
assert len(presigned_video.captions) == 13
|
45 |
|
46 |
|
47 |
def test_description(cipher_signature):
|