hbmartin commited on
Commit
3047241
·
1 Parent(s): 8cddc3f

refactor StreamQuery.order_by and add tests

Browse files
.idea/dictionaries/haroldmartin.xml CHANGED
@@ -5,8 +5,10 @@
5
  <w>descrambler</w>
6
  <w>ficano</w>
7
  <w>itag</w>
 
8
  <w>noqa</w>
9
  <w>pytube</w>
 
10
  </words>
11
  </dictionary>
12
  </component>
 
5
  <w>descrambler</w>
6
  <w>ficano</w>
7
  <w>itag</w>
8
+ <w>mypy</w>
9
  <w>noqa</w>
10
  <w>pytube</w>
11
+ <w>webm</w>
12
  </words>
13
  </dictionary>
14
  </component>
pytube/itags.py CHANGED
@@ -95,6 +95,8 @@ HDR = [330, 331, 332, 333, 334, 335, 336, 337]
95
  _60FPS = [298, 299, 302, 303, 308, 315] + HDR
96
  _3D = [82, 83, 84, 85, 100, 101, 102]
97
  LIVE = [91, 92, 93, 94, 95, 96, 132, 151]
 
 
98
 
99
 
100
  def get_format_profile(itag):
 
95
  _60FPS = [298, 299, 302, 303, 308, 315] + HDR
96
  _3D = [82, 83, 84, 85, 100, 101, 102]
97
  LIVE = [91, 92, 93, 94, 95, 96, 132, 151]
98
+ DASH_MP4_VIDEO = [133, 134, 135, 136, 137, 138, 160, 212, 264, 266, 298, 299]
99
+ DASH_MP4_AUDIO = [139, 140, 141, 256, 258, 325, 328]
100
 
101
 
102
  def get_format_profile(itag):
pytube/query.py CHANGED
@@ -1,6 +1,6 @@
1
  # -*- coding: utf-8 -*-
2
  """This module provides a query interface for media streams and captions."""
3
- from typing import List, Optional
4
 
5
  from pytube import Stream, Caption
6
 
@@ -166,34 +166,45 @@ class StreamQuery:
166
  fmt_streams = list(filter(fn, fmt_streams))
167
  return StreamQuery(fmt_streams)
168
 
169
- def order_by(self, attribute_name):
170
- """Apply a sort order to a resultset.
171
 
172
  :param str attribute_name:
173
  The name of the attribute to sort by.
174
  """
175
- integer_attr_repr = {}
176
- for stream in self.fmt_streams:
177
- attr = getattr(stream, attribute_name)
178
- if attr is None:
179
- break
180
- # TODO: improve this so tests can work
181
- num = "".join(x for x in attr if x.isdigit())
182
- integer_attr_repr[attr] = int("".join(num)) if num else None
183
-
184
- # if every attribute has an integer representation
185
- if integer_attr_repr and all(integer_attr_repr.values()):
186
-
187
- def key(s):
188
- return integer_attr_repr[getattr(s, attribute_name)]
189
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  else:
191
-
192
- def key(s):
193
- return getattr(s, attribute_name)
194
-
195
- fmt_streams = sorted(self.fmt_streams, key=key,)
196
- return StreamQuery(fmt_streams)
197
 
198
  def desc(self):
199
  """Sort streams in descending order.
 
1
  # -*- coding: utf-8 -*-
2
  """This module provides a query interface for media streams and captions."""
3
+ from typing import List, Optional, Dict
4
 
5
  from pytube import Stream, Caption
6
 
 
166
  fmt_streams = list(filter(fn, fmt_streams))
167
  return StreamQuery(fmt_streams)
168
 
169
+ def order_by(self, attribute_name: str) -> "StreamQuery":
170
+ """Apply a sort order to a resultset. Implicitly Filters out stream the do not have the attribute.
171
 
172
  :param str attribute_name:
173
  The name of the attribute to sort by.
174
  """
175
+ has_attribute = [
176
+ s for s in self.fmt_streams if getattr(s, attribute_name) is not None
177
+ ]
178
+ integer_attr_repr: Optional[Dict[str, int]] = None
179
+
180
+ # check that the attribute value is a string
181
+ if len(has_attribute) > 0 and isinstance(
182
+ getattr(has_attribute[0], attribute_name), str
183
+ ):
184
+ # attempt to extract numerical values from string
185
+ try:
186
+ integer_attr_repr = {
187
+ getattr(s, attribute_name): int(
188
+ "".join(list(filter(str.isdigit, getattr(s, attribute_name))))
189
+ )
190
+ for s in has_attribute
191
+ }
192
+ except ValueError:
193
+ integer_attr_repr = None
194
+ pass
195
+
196
+ # lookup integer values if we have them
197
+ if integer_attr_repr is not None:
198
+ return StreamQuery( # mypy: ignore
199
+ sorted(
200
+ has_attribute,
201
+ key=lambda s: integer_attr_repr[getattr(s, attribute_name)], # type: ignore
202
+ )
203
+ )
204
  else:
205
+ return StreamQuery(
206
+ sorted(has_attribute, key=lambda s: getattr(s, attribute_name))
207
+ )
 
 
 
208
 
209
  def desc(self):
210
  """Sort streams in descending order.
tests/test_query.py CHANGED
@@ -9,40 +9,38 @@ def test_count(cipher_signature):
9
 
10
 
11
  @pytest.mark.parametrize(
12
- 'test_input,expected', [
13
- ({'progressive': True}, [18]),
14
- ({'resolution': '720p'}, [136, 247]),
15
- ({'res': '720p'}, [136, 247]),
16
- ({'fps': 30, 'resolution': '480p'}, [135, 244]),
17
- ({'mime_type': 'audio/mp4'}, [140]),
18
- ({'type': 'audio'}, [140, 249, 250, 251]),
19
- ({'subtype': '3gpp'}, []),
20
- ({'abr': '128kbps'}, [140]),
21
- ({'bitrate': '128kbps'}, [140]),
22
- ({'audio_codec': 'opus'}, [249, 250, 251]),
23
- ({'video_codec': 'vp9'}, [248, 247, 244, 243, 242, 278]),
24
- ({'only_audio': True}, [140, 249, 250, 251]),
25
- ({'only_video': True, 'video_codec': 'avc1.4d4015'}, [133]),
26
- ({'adaptive': True, 'resolution': '1080p'}, [137, 248]),
27
- ({'custom_filter_functions': [lambda s: s.itag == 18]}, [18]),
 
28
  ],
29
  )
30
  def test_filters(test_input, expected, cipher_signature):
31
  """Ensure filters produce the expected results."""
32
- result = [
33
- s.itag for s
34
- in cipher_signature.streams.filter(**test_input).all()
35
- ]
36
  assert result == expected
37
 
38
 
39
- @pytest.mark.parametrize('test_input', ['first', 'last'])
40
  def test_empty(test_input, cipher_signature):
41
  """Ensure :meth:`~pytube.StreamQuery.last` and
42
  :meth:`~pytube.StreamQuery.first` return None if the resultset is
43
  empty.
44
  """
45
- query = cipher_signature.streams.filter(video_codec='vp20')
46
  fn = getattr(query, test_input)
47
  assert fn() is None
48
 
@@ -61,78 +59,72 @@ def test_get_first(cipher_signature):
61
  assert cipher_signature.streams.first().itag == 18
62
 
63
 
64
- @pytest.mark.skip(reason="Error with int sorting")
65
  def test_order_by(cipher_signature):
66
  """Ensure :meth:`~pytube.StreamQuery.order_by` sorts the list of
67
  :class:`Stream <Stream>` instances in the expected order.
68
  """
69
  itags = [
70
- s.itag for s in cipher_signature.streams
71
- .filter(progressive=True)
72
- .order_by('itag')
73
- .all()
74
  ]
 
75
 
76
- assert itags == ['17', '18', '22', '36', '43']
77
 
78
-
79
- @pytest.mark.skip(reason="Error with int sorting")
80
  def test_order_by_descending(cipher_signature):
81
  """Ensure :meth:`~pytube.StreamQuery.desc` sorts the list of
82
  :class:`Stream <Stream>` instances in the reverse order.
83
  """
84
  # numerical values
85
  itags = [
86
- s.itag for s in cipher_signature.streams
87
- .filter(progressive=True)
88
- .order_by('itag')
89
  .desc()
90
  .all()
91
  ]
92
- assert itags == ['43', '36', '22', '18', '17']
 
93
 
94
- # non numerical values
95
  mime_types = [
96
- s.mime_type for s in cipher_signature.streams
97
- .filter(progressive=True)
98
- .order_by('mime_type')
99
  .desc()
100
  .all()
101
  ]
102
- assert mime_types == [
103
- 'video/webm', 'video/mp4',
104
- 'video/mp4', 'video/3gpp', 'video/3gpp',
105
- ]
106
 
107
 
108
- @pytest.mark.skip(reason="Error with int sorting")
109
  def test_order_by_ascending(cipher_signature):
110
  """Ensure :meth:`~pytube.StreamQuery.desc` sorts the list of
111
  :class:`Stream <Stream>` instances in ascending order.
112
  """
113
  # numerical values
114
  itags = [
115
- s.itag for s in cipher_signature.streams
116
- .filter(progressive=True)
117
- .order_by('itag')
118
  .asc()
119
  .all()
120
  ]
 
121
 
122
- assert itags == ['17', '18', '22', '36', '43']
123
 
124
- # non numerical values
125
  mime_types = [
126
- s.mime_type for s in cipher_signature.streams
127
- .filter(progressive=True)
128
- .order_by('mime_type')
129
  .asc()
130
  .all()
131
  ]
132
- assert mime_types == [
133
- 'video/3gpp', 'video/3gpp',
134
- 'video/mp4', 'video/mp4', 'video/webm',
135
- ]
 
 
136
 
137
 
138
  def test_get_by_itag(cipher_signature):
@@ -140,7 +132,7 @@ def test_get_by_itag(cipher_signature):
140
  :class:`Stream <Stream>`.
141
  """
142
  assert cipher_signature.streams.get_by_itag(18).itag == 18
143
- assert cipher_signature.streams.get_by_itag('18').itag == 18
144
 
145
 
146
  def test_get_by_non_existent_itag(cipher_signature):
 
9
 
10
 
11
  @pytest.mark.parametrize(
12
+ "test_input,expected",
13
+ [
14
+ ({"progressive": True}, [18]),
15
+ ({"resolution": "720p"}, [136, 247]),
16
+ ({"res": "720p"}, [136, 247]),
17
+ ({"fps": 30, "resolution": "480p"}, [135, 244]),
18
+ ({"mime_type": "audio/mp4"}, [140]),
19
+ ({"type": "audio"}, [140, 249, 250, 251]),
20
+ ({"subtype": "3gpp"}, []),
21
+ ({"abr": "128kbps"}, [140]),
22
+ ({"bitrate": "128kbps"}, [140]),
23
+ ({"audio_codec": "opus"}, [249, 250, 251]),
24
+ ({"video_codec": "vp9"}, [248, 247, 244, 243, 242, 278]),
25
+ ({"only_audio": True}, [140, 249, 250, 251]),
26
+ ({"only_video": True, "video_codec": "avc1.4d4015"}, [133]),
27
+ ({"adaptive": True, "resolution": "1080p"}, [137, 248]),
28
+ ({"custom_filter_functions": [lambda s: s.itag == 18]}, [18]),
29
  ],
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).all()]
 
 
 
34
  assert result == expected
35
 
36
 
37
+ @pytest.mark.parametrize("test_input", ["first", "last"])
38
  def test_empty(test_input, cipher_signature):
39
  """Ensure :meth:`~pytube.StreamQuery.last` and
40
  :meth:`~pytube.StreamQuery.first` return None if the resultset is
41
  empty.
42
  """
43
+ query = cipher_signature.streams.filter(video_codec="vp20")
44
  fn = getattr(query, test_input)
45
  assert fn() is None
46
 
 
59
  assert cipher_signature.streams.first().itag == 18
60
 
61
 
 
62
  def test_order_by(cipher_signature):
63
  """Ensure :meth:`~pytube.StreamQuery.order_by` sorts the list of
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
 
 
72
 
 
 
73
  def test_order_by_descending(cipher_signature):
74
  """Ensure :meth:`~pytube.StreamQuery.desc` sorts the list of
75
  :class:`Stream <Stream>` instances in the reverse order.
76
  """
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
+
87
 
88
+ def test_order_by_non_numerical(cipher_signature):
89
  mime_types = [
90
+ s.mime_type
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
 
98
 
 
99
  def test_order_by_ascending(cipher_signature):
100
  """Ensure :meth:`~pytube.StreamQuery.desc` sorts the list of
101
  :class:`Stream <Stream>` instances in ascending order.
102
  """
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
 
 
113
 
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().all()]
127
+ assert abrs == ["50kbps", "70kbps", "96kbps", "128kbps", "160kbps"]
128
 
129
 
130
  def test_get_by_itag(cipher_signature):
 
132
  :class:`Stream <Stream>`.
133
  """
134
  assert cipher_signature.streams.get_by_itag(18).itag == 18
135
+ assert cipher_signature.streams.get_by_itag("18").itag == 18
136
 
137
 
138
  def test_get_by_non_existent_itag(cipher_signature):