hbmartin commited on
Commit
f12f4af
ยท
2 Parent(s): 8adc165 51df399

Merge branch 'master' into sequence-objects

Browse files
Pipfile CHANGED
@@ -21,11 +21,14 @@ flake8-executable = "*"
21
  flake8-if-expr = "*"
22
  flake8-isort = "*"
23
  flake8-logging-format = "*"
 
 
24
  flake8-print = "*"
25
  flake8-pytest = "*"
26
  flake8-pytest-style = "*"
27
  flake8-quotes = "*"
28
  flake8-return = "*"
 
29
  flake8-string-format = "*"
30
  mypy = "*"
31
  pep8-naming = "*"
 
21
  flake8-if-expr = "*"
22
  flake8-isort = "*"
23
  flake8-logging-format = "*"
24
+ flake8-mock = "*"
25
+ flake8-mutable = "*"
26
  flake8-print = "*"
27
  flake8-pytest = "*"
28
  flake8-pytest-style = "*"
29
  flake8-quotes = "*"
30
  flake8-return = "*"
31
+ flake8-strict = "*"
32
  flake8-string-format = "*"
33
  mypy = "*"
34
  pep8-naming = "*"
pytube/__main__.py CHANGED
@@ -20,7 +20,7 @@ from pytube import extract
20
  from pytube import request
21
  from pytube import Stream
22
  from pytube import StreamQuery
23
- from pytube.extract import apply_descrambler, apply_signature
24
  from pytube.helpers import install_proxy
25
  from pytube.exceptions import VideoUnavailable
26
  from pytube.monostate import OnProgress, OnComplete, Monostate
@@ -76,12 +76,10 @@ class YouTube:
76
  # video_id part of /watch?v=<video_id>
77
  self.video_id = extract.video_id(url)
78
 
79
- # https://www.youtube.com/watch?v=<video_id>
80
- self.watch_url = extract.watch_url(self.video_id)
81
 
82
- self.embed_url = extract.embed_url(self.video_id)
83
- # A dictionary shared between all instances of :class:`Stream <Stream>`
84
- # (Borg pattern). Boooooo.
85
  self.stream_monostate = Monostate(
86
  on_progress=on_progress_callback, on_complete=on_complete_callback
87
  )
@@ -111,9 +109,7 @@ class YouTube:
111
  self.player_config_args = self.vid_info
112
  else:
113
  assert self.watch_html is not None
114
- self.player_config_args = extract.get_ytplayer_config(self.watch_html,)[
115
- "args"
116
- ]
117
 
118
  # Fix for KeyError: 'title' issue #434
119
  if "title" not in self.player_config_args: # type: ignore
@@ -140,8 +136,9 @@ class YouTube:
140
  self.player_config_args, fmt, self.js # type: ignore
141
  )
142
  except TypeError:
143
- assert self.embed_html is not None
144
- self.js_url = extract.js_url(self.embed_html, self.age_restricted)
 
145
  self.js = request.get(self.js_url)
146
  assert self.js is not None
147
  apply_signature(self.player_config_args, fmt, self.js)
@@ -152,6 +149,8 @@ class YouTube:
152
  # load the player_response object (contains subtitle information)
153
  self.player_response = json.loads(self.player_config_args["player_response"])
154
  del self.player_config_args["player_response"]
 
 
155
 
156
  logger.info("init finished successfully")
157
 
@@ -166,23 +165,29 @@ class YouTube:
166
 
167
  """
168
  self.watch_html = request.get(url=self.watch_url)
169
- if (
170
- self.watch_html is None
171
- or '<img class="icon meh" src="/yts/img' not in self.watch_html
 
 
 
172
  ):
173
  raise VideoUnavailable(video_id=self.video_id)
174
 
175
- self.embed_html = request.get(url=self.embed_url)
176
- self.age_restricted = extract.is_age_restricted(self.watch_html)
177
- self.vid_info_url = extract.video_info_url(
178
- video_id=self.video_id,
179
- watch_url=self.watch_url,
180
- embed_html=self.embed_html,
181
- age_restricted=self.age_restricted,
182
- )
 
 
 
183
  self.vid_info_raw = request.get(self.vid_info_url)
184
  if not self.age_restricted:
185
- self.js_url = extract.js_url(self.watch_html, self.age_restricted)
186
  self.js = request.get(self.js_url)
187
 
188
  def initialize_stream_objects(self, fmt: str) -> None:
@@ -275,7 +280,7 @@ class YouTube:
275
  """
276
  return self.player_response.get("videoDetails", {}).get(
277
  "shortDescription"
278
- ) or extract.get_vid_descr(self.watch_html)
279
 
280
  @property
281
  def rating(self) -> float:
 
20
  from pytube import request
21
  from pytube import Stream
22
  from pytube import StreamQuery
23
+ from pytube.extract import apply_descrambler, apply_signature, get_ytplayer_config
24
  from pytube.helpers import install_proxy
25
  from pytube.exceptions import VideoUnavailable
26
  from pytube.monostate import OnProgress, OnComplete, Monostate
 
76
  # video_id part of /watch?v=<video_id>
77
  self.video_id = extract.video_id(url)
78
 
79
+ self.watch_url = f"https://youtube.com/watch?v={self.video_id}"
80
+ self.embed_url = f"https://www.youtube.com/embed/{self.video_id}"
81
 
82
+ # Shared between all instances of `Stream` (Borg pattern).
 
 
83
  self.stream_monostate = Monostate(
84
  on_progress=on_progress_callback, on_complete=on_complete_callback
85
  )
 
109
  self.player_config_args = self.vid_info
110
  else:
111
  assert self.watch_html is not None
112
+ self.player_config_args = get_ytplayer_config(self.watch_html)["args"]
 
 
113
 
114
  # Fix for KeyError: 'title' issue #434
115
  if "title" not in self.player_config_args: # type: ignore
 
136
  self.player_config_args, fmt, self.js # type: ignore
137
  )
138
  except TypeError:
139
+ if not self.embed_html:
140
+ self.embed_html = request.get(url=self.embed_url)
141
+ self.js_url = extract.js_url(self.embed_html)
142
  self.js = request.get(self.js_url)
143
  assert self.js is not None
144
  apply_signature(self.player_config_args, fmt, self.js)
 
149
  # load the player_response object (contains subtitle information)
150
  self.player_response = json.loads(self.player_config_args["player_response"])
151
  del self.player_config_args["player_response"]
152
+ self.stream_monostate.title = self.title
153
+ self.stream_monostate.duration = self.length
154
 
155
  logger.info("init finished successfully")
156
 
 
165
 
166
  """
167
  self.watch_html = request.get(url=self.watch_url)
168
+ if self.watch_html is None:
169
+ raise VideoUnavailable(video_id=self.video_id)
170
+ self.age_restricted = extract.is_age_restricted(self.watch_html)
171
+ if not self.age_restricted and (
172
+ "yt-badge-live" in self.watch_html
173
+ or "This video is private" in self.watch_html
174
  ):
175
  raise VideoUnavailable(video_id=self.video_id)
176
 
177
+ if self.age_restricted:
178
+ if not self.embed_html:
179
+ self.embed_html = request.get(url=self.embed_url)
180
+ self.vid_info_url = extract.video_info_url_age_restricted(
181
+ self.video_id, self.watch_url
182
+ )
183
+ else:
184
+ self.vid_info_url = extract.video_info_url(
185
+ video_id=self.video_id, watch_url=self.watch_url
186
+ )
187
+
188
  self.vid_info_raw = request.get(self.vid_info_url)
189
  if not self.age_restricted:
190
+ self.js_url = extract.js_url(self.watch_html)
191
  self.js = request.get(self.js_url)
192
 
193
  def initialize_stream_objects(self, fmt: str) -> None:
 
280
  """
281
  return self.player_response.get("videoDetails", {}).get(
282
  "shortDescription"
283
+ ) or extract._get_vid_descr(self.watch_html)
284
 
285
  @property
286
  def rating(self) -> float:
pytube/extract.py CHANGED
@@ -77,66 +77,58 @@ def video_id(url: str) -> str:
77
  return regex_search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", url, group=1)
78
 
79
 
80
- def watch_url(video_id: str) -> str:
81
- """Construct a sanitized YouTube watch url, given a video id.
82
 
83
  :param str video_id:
84
  A YouTube video identifier.
 
 
85
  :rtype: str
86
  :returns:
87
- Sanitized YouTube watch url.
 
88
  """
89
- return "https://youtube.com/watch?v=" + video_id
90
-
91
-
92
- def embed_url(video_id: str) -> str:
93
- return f"https://www.youtube.com/embed/{video_id}"
94
-
95
-
96
- def eurl(video_id: str) -> str:
97
- return f"https://youtube.googleapis.com/v/{video_id}"
 
98
 
99
 
100
- def video_info_url(
101
- video_id: str, watch_url: str, embed_html: Optional[str], age_restricted: bool,
102
- ) -> str:
103
  """Construct the video_info url.
104
 
105
  :param str video_id:
106
  A YouTube video identifier.
107
- :param str watch_url:
108
- A YouTube watch url.
109
  :param str embed_html:
110
  The html contents of the embed page (for age restricted videos).
111
- :param bool age_restricted:
112
- Is video age restricted.
113
  :rtype: str
114
  :returns:
115
  :samp:`https://youtube.com/get_video_info` with necessary GET
116
  parameters.
117
  """
118
- if age_restricted:
119
- assert embed_html is not None
120
  sts = regex_search(r'"sts"\s*:\s*(\d+)', embed_html, group=1)
121
- # Here we use ``OrderedDict`` so that the output is consistent between
122
- # Python 2.7+.
123
- params = OrderedDict(
124
- [("video_id", video_id), ("eurl", eurl(video_id)), ("sts", sts),]
125
- )
126
- else:
127
- params = OrderedDict(
128
- [
129
- ("video_id", video_id),
130
- ("el", "$el"),
131
- ("ps", "default"),
132
- ("eurl", quote(watch_url)),
133
- ("hl", "en_US"),
134
- ]
135
- )
136
  return "https://youtube.com/get_video_info?" + urlencode(params)
137
 
138
 
139
- def js_url(html: str, age_restricted: Optional[bool] = False) -> str:
140
  """Get the base JavaScript url.
141
 
142
  Construct the base JavaScript url, which contains the decipher
@@ -144,12 +136,8 @@ def js_url(html: str, age_restricted: Optional[bool] = False) -> str:
144
 
145
  :param str html:
146
  The html contents of the watch page.
147
- :param bool age_restricted:
148
- Is video age restricted.
149
-
150
  """
151
- ytplayer_config = get_ytplayer_config(html, age_restricted or False)
152
- base_js = ytplayer_config["assets"]["js"]
153
  return "https://youtube.com" + base_js
154
 
155
 
@@ -180,7 +168,7 @@ def mime_type_codec(mime_type_codec: str) -> Tuple[str, List[str]]:
180
  return mime_type, [c.strip() for c in codecs.split(",")]
181
 
182
 
183
- def get_ytplayer_config(html: str, age_restricted: bool = False) -> Any:
184
  """Get the YouTube player configuration data from the watch html.
185
 
186
  Extract the ``ytplayer_config``, which is json data embedded within the
@@ -189,21 +177,29 @@ def get_ytplayer_config(html: str, age_restricted: bool = False) -> Any:
189
 
190
  :param str html:
191
  The html contents of the watch page.
192
- :param bool age_restricted:
193
- Is video age restricted.
194
  :rtype: str
195
  :returns:
196
  Substring of the html containing the encoded manifest data.
197
  """
198
- if age_restricted:
199
- pattern = r";yt\.setConfig\(\{'PLAYER_CONFIG':\s*({.*})(,'EXPERIMENT_FLAGS'|;)" # noqa: E501
200
- else:
201
- pattern = r";ytplayer\.config\s*=\s*({.*?});"
202
- yt_player_config = regex_search(pattern, html, group=1)
203
- return json.loads(yt_player_config)
204
-
205
-
206
- def get_vid_descr(html: Optional[str]) -> str:
 
 
 
 
 
 
 
 
 
 
207
  html_parser = PytubeHTMLParser()
208
  if html:
209
  html_parser.feed(html)
@@ -278,6 +274,8 @@ def apply_descrambler(stream_data: Dict, key: str) -> None:
278
  {'foo': [{'bar': '1', 'var': 'test'}, {'em': '5', 't': 'url encoded'}]}
279
 
280
  """
 
 
281
  if key == "url_encoded_fmt_stream_map" and not stream_data.get(
282
  "url_encoded_fmt_stream_map"
283
  ):
@@ -294,6 +292,8 @@ def apply_descrambler(stream_data: Dict, key: str) -> None:
294
  "type": format_item["mimeType"],
295
  "quality": format_item["quality"],
296
  "itag": format_item["itag"],
 
 
297
  }
298
  for format_item in formats
299
  ]
@@ -308,6 +308,8 @@ def apply_descrambler(stream_data: Dict, key: str) -> None:
308
  "type": format_item["mimeType"],
309
  "quality": format_item["quality"],
310
  "itag": format_item["itag"],
 
 
311
  }
312
  for i, format_item in enumerate(formats)
313
  ]
 
77
  return regex_search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", url, group=1)
78
 
79
 
80
+ def video_info_url(video_id: str, watch_url: str) -> str:
81
+ """Construct the video_info url.
82
 
83
  :param str video_id:
84
  A YouTube video identifier.
85
+ :param str watch_url:
86
+ A YouTube watch url.
87
  :rtype: str
88
  :returns:
89
+ :samp:`https://youtube.com/get_video_info` with necessary GET
90
+ parameters.
91
  """
92
+ params = OrderedDict(
93
+ [
94
+ ("video_id", video_id),
95
+ ("el", "$el"),
96
+ ("ps", "default"),
97
+ ("eurl", quote(watch_url)),
98
+ ("hl", "en_US"),
99
+ ]
100
+ )
101
+ return _video_info_url(params)
102
 
103
 
104
+ def video_info_url_age_restricted(video_id: str, embed_html: str) -> str:
 
 
105
  """Construct the video_info url.
106
 
107
  :param str video_id:
108
  A YouTube video identifier.
 
 
109
  :param str embed_html:
110
  The html contents of the embed page (for age restricted videos).
 
 
111
  :rtype: str
112
  :returns:
113
  :samp:`https://youtube.com/get_video_info` with necessary GET
114
  parameters.
115
  """
116
+ try:
 
117
  sts = regex_search(r'"sts"\s*:\s*(\d+)', embed_html, group=1)
118
+ except RegexMatchError:
119
+ sts = ""
120
+ # Here we use ``OrderedDict`` so that the output is consistent between
121
+ # Python 2.7+.
122
+ eurl = f"https://youtube.googleapis.com/v/{video_id}"
123
+ params = OrderedDict([("video_id", video_id), ("eurl", eurl), ("sts", sts),])
124
+ return _video_info_url(params)
125
+
126
+
127
+ def _video_info_url(params: OrderedDict) -> str:
 
 
 
 
 
128
  return "https://youtube.com/get_video_info?" + urlencode(params)
129
 
130
 
131
+ def js_url(html: str) -> str:
132
  """Get the base JavaScript url.
133
 
134
  Construct the base JavaScript url, which contains the decipher
 
136
 
137
  :param str html:
138
  The html contents of the watch page.
 
 
 
139
  """
140
+ base_js = get_ytplayer_config(html)["assets"]["js"]
 
141
  return "https://youtube.com" + base_js
142
 
143
 
 
168
  return mime_type, [c.strip() for c in codecs.split(",")]
169
 
170
 
171
+ def get_ytplayer_config(html: str) -> Any:
172
  """Get the YouTube player configuration data from the watch html.
173
 
174
  Extract the ``ytplayer_config``, which is json data embedded within the
 
177
 
178
  :param str html:
179
  The html contents of the watch page.
 
 
180
  :rtype: str
181
  :returns:
182
  Substring of the html containing the encoded manifest data.
183
  """
184
+ config_patterns = [
185
+ r";ytplayer\.config\s*=\s*({.*?});",
186
+ r";ytplayer\.config\s*=\s*({.+?});ytplayer",
187
+ r";yt\.setConfig\(\{'PLAYER_CONFIG':\s*({.*})}\);",
188
+ r";yt\.setConfig\(\{'PLAYER_CONFIG':\s*({.*})(,'EXPERIMENT_FLAGS'|;)", # noqa: E501
189
+ ]
190
+ logger.debug("finding initial function name")
191
+ for pattern in config_patterns:
192
+ regex = re.compile(pattern)
193
+ function_match = regex.search(html)
194
+ if function_match:
195
+ logger.debug("finished regex search, matched: %s", pattern)
196
+ yt_player_config = function_match.group(1)
197
+ return json.loads(yt_player_config)
198
+
199
+ raise RegexMatchError(caller="get_ytplayer_config", pattern="config_patterns")
200
+
201
+
202
+ def _get_vid_descr(html: Optional[str]) -> str:
203
  html_parser = PytubeHTMLParser()
204
  if html:
205
  html_parser.feed(html)
 
274
  {'foo': [{'bar': '1', 'var': 'test'}, {'em': '5', 't': 'url encoded'}]}
275
 
276
  """
277
+ otf_type = "FORMAT_STREAM_TYPE_OTF"
278
+
279
  if key == "url_encoded_fmt_stream_map" and not stream_data.get(
280
  "url_encoded_fmt_stream_map"
281
  ):
 
292
  "type": format_item["mimeType"],
293
  "quality": format_item["quality"],
294
  "itag": format_item["itag"],
295
+ "bitrate": format_item.get("bitrate"),
296
+ "is_otf": (format_item.get("type") == otf_type),
297
  }
298
  for format_item in formats
299
  ]
 
308
  "type": format_item["mimeType"],
309
  "quality": format_item["quality"],
310
  "itag": format_item["itag"],
311
+ "bitrate": format_item.get("bitrate"),
312
+ "is_otf": (format_item.get("type") == otf_type),
313
  }
314
  for i, format_item in enumerate(formats)
315
  ]
pytube/monostate.py CHANGED
@@ -52,7 +52,13 @@ class OnComplete(Protocol):
52
 
53
  class Monostate:
54
  def __init__(
55
- self, on_progress: Optional[OnProgress], on_complete: Optional[OnComplete]
 
 
 
 
56
  ):
57
  self.on_progress = on_progress
58
  self.on_complete = on_complete
 
 
 
52
 
53
  class Monostate:
54
  def __init__(
55
+ self,
56
+ on_progress: Optional[OnProgress],
57
+ on_complete: Optional[OnComplete],
58
+ title: Optional[str] = None,
59
+ duration: Optional[int] = None,
60
  ):
61
  self.on_progress = on_progress
62
  self.on_complete = on_complete
63
+ self.title = title
64
+ self.duration = duration
pytube/query.py CHANGED
@@ -1,7 +1,7 @@
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
@@ -170,9 +170,12 @@ class StreamQuery(Sequence):
170
  if is_dash is not None:
171
  filters.append(lambda s: s.is_dash == is_dash)
172
 
 
 
 
173
  fmt_streams = self.fmt_streams
174
- for fn in filters:
175
- fmt_streams = filter(fn, fmt_streams)
176
  return StreamQuery(list(fmt_streams))
177
 
178
  def order_by(self, attribute_name: str) -> "StreamQuery":
@@ -283,10 +286,18 @@ class StreamQuery(Sequence):
283
  :returns:
284
  The :class:`Stream <Stream>` matching the given itag or None if
285
  not found.
286
-
287
  """
288
  return self.filter(only_audio=True, subtype=subtype).order_by("abr").last()
289
 
 
 
 
 
 
 
 
 
 
290
  def first(self) -> Optional[Stream]:
291
  """Get the first :class:`Stream <Stream>` in the results.
292
 
 
1
  # -*- coding: utf-8 -*-
2
 
3
  """This module provides a query interface for media streams and captions."""
4
+ from typing import Callable, List, Optional, Union
5
  from collections.abc import Sequence
6
 
7
  from pytube import Stream, Caption
 
170
  if is_dash is not None:
171
  filters.append(lambda s: s.is_dash == is_dash)
172
 
173
+ return self._filter(filters)
174
+
175
+ def _filter(self, filters: List[Callable]) -> "StreamQuery":
176
  fmt_streams = self.fmt_streams
177
+ for filter_lambda in filters:
178
+ fmt_streams = filter(filter_lambda, fmt_streams)
179
  return StreamQuery(list(fmt_streams))
180
 
181
  def order_by(self, attribute_name: str) -> "StreamQuery":
 
286
  :returns:
287
  The :class:`Stream <Stream>` matching the given itag or None if
288
  not found.
 
289
  """
290
  return self.filter(only_audio=True, subtype=subtype).order_by("abr").last()
291
 
292
+ def otf(self, is_otf: bool = False) -> "StreamQuery":
293
+ """Filter stream by OTF, useful if some streams have 404 URLs
294
+
295
+ :param bool is_otf: Set to False to retrieve only non-OTF streams
296
+ :rtype: :class:`StreamQuery <StreamQuery>`
297
+ :returns: A StreamQuery object with otf filtered streams
298
+ """
299
+ return self._filter([lambda s: s.is_otf == is_otf])
300
+
301
  def first(self) -> Optional[Stream]:
302
  """Get the first :class:`Stream <Stream>` in the results.
303
 
pytube/streams.py CHANGED
@@ -58,6 +58,9 @@ class Stream:
58
  # streams return NoneType for audio/video depending.
59
  self.video_codec, self.audio_codec = self.parse_codecs()
60
 
 
 
 
61
  self._filesize: Optional[int] = None # filesize in bytes
62
 
63
  # Additional information about the stream format, such as resolution,
@@ -152,15 +155,22 @@ class Stream:
152
  :returns:
153
  Youtube video title
154
  """
155
- return (
156
- self.player_config_args.get("title")
157
- or (
158
- self.player_config_args.get("player_response", {})
159
- .get("videoDetails", {})
160
- .get("title")
161
- )
162
- or "Unknown YouTube Video Title"
163
- )
 
 
 
 
 
 
 
164
 
165
  @property
166
  def default_filename(self) -> str:
 
58
  # streams return NoneType for audio/video depending.
59
  self.video_codec, self.audio_codec = self.parse_codecs()
60
 
61
+ self.is_otf: bool = stream["is_otf"]
62
+ self.bitrate: Optional[int] = stream["bitrate"]
63
+
64
  self._filesize: Optional[int] = None # filesize in bytes
65
 
66
  # Additional information about the stream format, such as resolution,
 
155
  :returns:
156
  Youtube video title
157
  """
158
+ return self._monostate.title or "Unknown YouTube Video Title"
159
+
160
+ @property
161
+ def filesize_approx(self) -> int:
162
+ """Get approximate filesize of the video
163
+
164
+ Falls back to HTTP call if there is not sufficient information to approximate
165
+
166
+ :rtype: int
167
+ :returns: size of video in bytes
168
+ """
169
+ if self._monostate.duration and self.bitrate:
170
+ bits_in_byte = 8
171
+ return int((self._monostate.duration * self.bitrate) / bits_in_byte)
172
+
173
+ return self.filesize
174
 
175
  @property
176
  def default_filename(self) -> str:
tests/conftest.py CHANGED
@@ -48,7 +48,7 @@ def presigned_video():
48
  @pytest.fixture
49
  def age_restricted():
50
  """Youtube instance initialized with video id zRbsm3e2ltw."""
51
- filename = "yt-video-zRbsm3e2ltw-1507777044.json.gz"
52
  return load_playback_file(filename)
53
 
54
 
 
48
  @pytest.fixture
49
  def age_restricted():
50
  """Youtube instance initialized with video id zRbsm3e2ltw."""
51
+ filename = "yt-video-irauhITDrsE.json.gz"
52
  return load_playback_file(filename)
53
 
54
 
tests/generate_fixture.py CHANGED
@@ -19,6 +19,7 @@ output = {
19
  "watch_html": yt.watch_html,
20
  "video_info": yt.vid_info,
21
  "js": yt.js,
 
22
  }
23
 
24
  outpath = path.join(currentdir, "mocks", "yt-video-" + yt.video_id + ".json")
 
19
  "watch_html": yt.watch_html,
20
  "video_info": yt.vid_info,
21
  "js": yt.js,
22
+ "embed_html": yt.embed_html,
23
  }
24
 
25
  outpath = path.join(currentdir, "mocks", "yt-video-" + yt.video_id + ".json")
tests/mocks/yt-video-irauhITDrsE.json.gz ADDED
Binary file (38.5 kB). View file
 
tests/mocks/yt-video-zRbsm3e2ltw-1507777044.json.gz DELETED
Binary file (20.6 kB)
 
tests/test_extract.py CHANGED
@@ -12,18 +12,20 @@ def test_extract_video_id():
12
  assert video_id == "9bZkp7q19f0"
13
 
14
 
15
- def test_extract_watch_url():
16
- video_id = "9bZkp7q19f0"
17
- watch_url = extract.watch_url(video_id)
18
- assert watch_url == "https://youtube.com/watch?v=9bZkp7q19f0"
 
 
 
 
 
19
 
20
 
21
- def test_info_url(cipher_signature):
22
  video_info_url = extract.video_info_url(
23
- video_id=cipher_signature.video_id,
24
- watch_url=cipher_signature.watch_url,
25
- embed_html="",
26
- age_restricted=False,
27
  )
28
  expected = (
29
  "https://youtube.com/get_video_info?video_id=9bZkp7q19f0&el=%24el"
@@ -63,12 +65,7 @@ def test_get_vid_desc(cipher_signature):
63
  "http://sptfy.com/PSY\n"
64
  "http://weibo.com/psyoppa"
65
  )
66
- assert extract.get_vid_descr(cipher_signature.watch_html) == expected
67
-
68
-
69
- def test_eurl():
70
- url = extract.eurl("videoid")
71
- assert url == "https://youtube.googleapis.com/v/videoid"
72
 
73
 
74
  def test_mime_type_codec():
 
12
  assert video_id == "9bZkp7q19f0"
13
 
14
 
15
+ def test_info_url(age_restricted):
16
+ video_info_url = extract.video_info_url_age_restricted(
17
+ video_id="QRS8MkLhQmM", embed_html=age_restricted["embed_html"],
18
+ )
19
+ expected = (
20
+ "https://youtube.com/get_video_info?video_id=QRS8MkLhQmM&eurl"
21
+ "=https%3A%2F%2Fyoutube.googleapis.com%2Fv%2FQRS8MkLhQmM&sts="
22
+ )
23
+ assert video_info_url == expected
24
 
25
 
26
+ def test_info_url_age_restricted(cipher_signature):
27
  video_info_url = extract.video_info_url(
28
+ video_id=cipher_signature.video_id, watch_url=cipher_signature.watch_url
 
 
 
29
  )
30
  expected = (
31
  "https://youtube.com/get_video_info?video_id=9bZkp7q19f0&el=%24el"
 
65
  "http://sptfy.com/PSY\n"
66
  "http://weibo.com/psyoppa"
67
  )
68
+ assert extract._get_vid_descr(cipher_signature.watch_html) == expected
 
 
 
 
 
69
 
70
 
71
  def test_mime_type_codec():
tests/test_query.py CHANGED
@@ -151,3 +151,11 @@ def test_get_audio_only_with_subtype(cipher_signature):
151
  def test_sequence(cipher_signature):
152
  assert len(cipher_signature.streams) == 22
153
  assert cipher_signature.streams[0] is not None
 
 
 
 
 
 
 
 
 
151
  def test_sequence(cipher_signature):
152
  assert len(cipher_signature.streams) == 22
153
  assert cipher_signature.streams[0] is not None
154
+
155
+
156
+ def test_otf(cipher_signature):
157
+ non_otf = cipher_signature.streams.otf().all()
158
+ assert len(non_otf) == 22
159
+
160
+ otf = cipher_signature.streams.otf(True).all()
161
+ assert len(otf) == 0
tests/test_streams.py CHANGED
@@ -14,6 +14,15 @@ def test_filesize(cipher_signature, mocker):
14
  assert cipher_signature.streams.first().filesize == 6796391
15
 
16
 
 
 
 
 
 
 
 
 
 
17
  def test_default_filename(cipher_signature):
18
  expected = "PSY - GANGNAM STYLE(๊ฐ•๋‚จ์Šคํƒ€์ผ) MV.mp4"
19
  stream = cipher_signature.streams.first()
@@ -21,19 +30,14 @@ def test_default_filename(cipher_signature):
21
 
22
 
23
  def test_title(cipher_signature):
24
- expected = "PSY - GANGNAM STYLE(๊ฐ•๋‚จ์Šคํƒ€์ผ) M/V"
25
- stream = cipher_signature.streams.first()
26
- assert stream.title == expected
27
-
28
- expected = "PSY - GANGNAM STYLE(๊ฐ•๋‚จ์Šคํƒ€์ผ)"
29
- stream.player_config_args = {
30
- "player_response": {"videoDetails": {"title": expected}},
31
- }
32
- assert stream.title == expected
33
-
34
- expected = "Unknown YouTube Video Title"
35
- stream.player_config_args = {}
36
- assert stream.title == expected
37
 
38
 
39
  def test_caption_tracks(presigned_video):
 
14
  assert cipher_signature.streams.first().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.first()
21
+ assert stream.filesize_approx == 22350604
22
+ stream.bitrate = None
23
+ assert stream.filesize_approx == 123
24
+
25
+
26
  def test_default_filename(cipher_signature):
27
  expected = "PSY - GANGNAM STYLE(๊ฐ•๋‚จ์Šคํƒ€์ผ) MV.mp4"
28
  stream = cipher_signature.streams.first()
 
30
 
31
 
32
  def test_title(cipher_signature):
33
+ expected = "title"
34
+ cipher_signature.player_config_args["title"] = expected
35
+ assert cipher_signature.title == expected
36
+
37
+ expected = "title2"
38
+ del cipher_signature.player_config_args["title"]
39
+ cipher_signature.player_response = {"videoDetails": {"title": expected}}
40
+ assert cipher_signature.title == expected
 
 
 
 
 
41
 
42
 
43
  def test_caption_tracks(presigned_video):