hbmartin commited on
Commit
724e315
·
1 Parent(s): 61e7e36

add self.player_response and lazy caption init

Browse files
pytube/__main__.py CHANGED
@@ -67,12 +67,12 @@ class YouTube:
67
  self.watch_html: Optional[str] = None # the html of /watch?v=<video_id>
68
  self.embed_html: Optional[str] = None
69
  self.player_config_args: Dict = {} # inline js in the html containing
 
70
  # streams
71
  self.age_restricted: Optional[bool] = None
72
  self.vid_descr: Optional[str] = None
73
 
74
  self.fmt_streams: List[Stream] = []
75
- self.caption_tracks: List[Caption] = []
76
 
77
  # video_id part of /watch?v=<video_id>
78
  self.video_id = extract.video_id(url)
@@ -153,11 +153,9 @@ class YouTube:
153
  self.initialize_stream_objects(fmt)
154
 
155
  # load the player_response object (contains subtitle information)
156
- self.player_config_args["player_response"] = json.loads(
157
- self.player_config_args["player_response"]
158
- )
159
 
160
- self.initialize_caption_objects()
161
  logger.info("init finished successfully")
162
 
163
  def prefetch(self) -> None:
@@ -173,8 +171,7 @@ class YouTube:
173
  self.watch_html = request.get(url=self.watch_url)
174
  if (
175
  self.watch_html is None
176
- or '<img class="icon meh" src="/yts/img' # noqa: W503
177
- not in self.watch_html # noqa: W503
178
  ):
179
  raise VideoUnavailable(video_id=self.video_id)
180
 
@@ -214,26 +211,19 @@ class YouTube:
214
  )
215
  self.fmt_streams.append(video)
216
 
217
- def initialize_caption_objects(self) -> None:
218
- """Populate instances of :class:`Caption <Caption>`.
219
-
220
- Take the unscrambled player response data, and use it to initialize
221
- instances of :class:`Caption <Caption>`.
222
-
223
- :rtype: None
224
 
 
225
  """
226
- if "captions" not in self.player_config_args["player_response"]:
227
- return
228
- # https://github.com/nficano/pytube/issues/167
229
- caption_tracks = (
230
- self.player_config_args.get("player_response", {})
231
  .get("captions", {})
232
  .get("playerCaptionsTracklistRenderer", {})
233
  .get("captionTracks", [])
234
  )
235
- for caption_track in caption_tracks:
236
- self.caption_tracks.append(Caption(caption_track))
237
 
238
  @property
239
  def captions(self) -> CaptionQuery:
@@ -258,9 +248,8 @@ class YouTube:
258
  :rtype: str
259
 
260
  """
261
- player_response = self.player_config_args.get("player_response", {})
262
  thumbnail_details = (
263
- player_response.get("videoDetails", {})
264
  .get("thumbnail", {})
265
  .get("thumbnails")
266
  )
@@ -278,7 +267,7 @@ class YouTube:
278
 
279
  """
280
  return self.player_config_args.get("title") or (
281
- self.player_config_args.get("player_response", {})
282
  .get("videoDetails", {})
283
  .get("title")
284
  )
@@ -291,7 +280,7 @@ class YouTube:
291
 
292
  """
293
  return self.vid_descr or (
294
- self.player_config_args.get("player_response", {})
295
  .get("videoDetails", {})
296
  .get("shortDescription")
297
  )
@@ -304,7 +293,7 @@ class YouTube:
304
 
305
  """
306
  return (
307
- self.player_config_args.get("player_response", {})
308
  .get("videoDetails", {})
309
  .get("averageRating")
310
  )
@@ -319,7 +308,7 @@ class YouTube:
319
  return int(
320
  self.player_config_args.get("length_seconds")
321
  or (
322
- self.player_config_args.get("player_response", {})
323
  .get("videoDetails", {})
324
  .get("lengthSeconds")
325
  )
@@ -333,7 +322,7 @@ class YouTube:
333
 
334
  """
335
  return int(
336
- self.player_config_args.get("player_response", {})
337
  .get("videoDetails", {})
338
  .get("viewCount")
339
  )
@@ -344,7 +333,7 @@ class YouTube:
344
  :rtype: str
345
  """
346
  return (
347
- self.player_config_args.get("player_response", {})
348
  .get("videoDetails", {})
349
  .get("author", "unknown")
350
  )
 
67
  self.watch_html: Optional[str] = None # the html of /watch?v=<video_id>
68
  self.embed_html: Optional[str] = None
69
  self.player_config_args: Dict = {} # inline js in the html containing
70
+ self.player_response: Dict = {}
71
  # streams
72
  self.age_restricted: Optional[bool] = None
73
  self.vid_descr: Optional[str] = None
74
 
75
  self.fmt_streams: List[Stream] = []
 
76
 
77
  # video_id part of /watch?v=<video_id>
78
  self.video_id = extract.video_id(url)
 
153
  self.initialize_stream_objects(fmt)
154
 
155
  # load the player_response object (contains subtitle information)
156
+ self.player_response = json.loads(self.player_config_args["player_response"])
157
+ del self.player_config_args["player_response"]
 
158
 
 
159
  logger.info("init finished successfully")
160
 
161
  def prefetch(self) -> None:
 
171
  self.watch_html = request.get(url=self.watch_url)
172
  if (
173
  self.watch_html is None
174
+ or '<img class="icon meh" src="/yts/img' not in self.watch_html
 
175
  ):
176
  raise VideoUnavailable(video_id=self.video_id)
177
 
 
211
  )
212
  self.fmt_streams.append(video)
213
 
214
+ @property
215
+ def caption_tracks(self) -> List[Caption]:
216
+ """Get a list of :class:`Caption <Caption>`.
 
 
 
 
217
 
218
+ :rtype: List[Caption]
219
  """
220
+ raw_tracks = (
221
+ self.player_response
 
 
 
222
  .get("captions", {})
223
  .get("playerCaptionsTracklistRenderer", {})
224
  .get("captionTracks", [])
225
  )
226
+ return [Caption(track) for track in raw_tracks]
 
227
 
228
  @property
229
  def captions(self) -> CaptionQuery:
 
248
  :rtype: str
249
 
250
  """
 
251
  thumbnail_details = (
252
+ self.player_response.get("videoDetails", {})
253
  .get("thumbnail", {})
254
  .get("thumbnails")
255
  )
 
267
 
268
  """
269
  return self.player_config_args.get("title") or (
270
+ self.player_response
271
  .get("videoDetails", {})
272
  .get("title")
273
  )
 
280
 
281
  """
282
  return self.vid_descr or (
283
+ self.player_response
284
  .get("videoDetails", {})
285
  .get("shortDescription")
286
  )
 
293
 
294
  """
295
  return (
296
+ self.player_response
297
  .get("videoDetails", {})
298
  .get("averageRating")
299
  )
 
308
  return int(
309
  self.player_config_args.get("length_seconds")
310
  or (
311
+ self.player_response
312
  .get("videoDetails", {})
313
  .get("lengthSeconds")
314
  )
 
322
 
323
  """
324
  return int(
325
+ self.player_response
326
  .get("videoDetails", {})
327
  .get("viewCount")
328
  )
 
333
  :rtype: str
334
  """
335
  return (
336
+ self.player_response
337
  .get("videoDetails", {})
338
  .get("author", "unknown")
339
  )
pytube/contrib/playlist.py CHANGED
@@ -45,8 +45,7 @@ class Playlist:
45
 
46
  @staticmethod
47
  def _find_load_more_url(req: str) -> Optional[str]:
48
- """Given an html page or fragment, returns the "load more" url if found.
49
- """
50
  match = re.search(
51
  r"data-uix-load-more-href=\"(/browse_ajax\?" 'action_continuation=.*?)"',
52
  req,
@@ -58,6 +57,10 @@ class Playlist:
58
 
59
  @deprecated("This function will be removed in the future, please use .video_urls")
60
  def parse_links(self) -> List[str]: # pragma: no cover
 
 
 
 
61
  return self.video_urls
62
 
63
  def _paginate(self, until_watch_id: Optional[str] = None) -> Iterable[List[str]]:
 
45
 
46
  @staticmethod
47
  def _find_load_more_url(req: str) -> Optional[str]:
48
+ """Given an html page or fragment, returns the "load more" url if found."""
 
49
  match = re.search(
50
  r"data-uix-load-more-href=\"(/browse_ajax\?" 'action_continuation=.*?)"',
51
  req,
 
57
 
58
  @deprecated("This function will be removed in the future, please use .video_urls")
59
  def parse_links(self) -> List[str]: # pragma: no cover
60
+ """ Deprecated function for returning list of URLs
61
+
62
+ :return: List[str]
63
+ """
64
  return self.video_urls
65
 
66
  def _paginate(self, until_watch_id: Optional[str] = None) -> Iterable[List[str]]:
pytube/monostate.py CHANGED
@@ -1,9 +1,8 @@
 
1
  import io
2
  from typing import Any, Optional
3
  from typing_extensions import Protocol
4
 
5
- # from __future__ import annotations
6
-
7
 
8
  class OnProgress(Protocol):
9
  def __call__(
 
1
+ # -*- coding: utf-8 -*-
2
  import io
3
  from typing import Any, Optional
4
  from typing_extensions import Protocol
5
 
 
 
6
 
7
  class OnProgress(Protocol):
8
  def __call__(
pytube/query.py CHANGED
@@ -1,4 +1,5 @@
1
  # -*- coding: utf-8 -*-
 
2
  """This module provides a query interface for media streams and captions."""
3
  from typing import List, Optional
4
 
 
1
  # -*- coding: utf-8 -*-
2
+
3
  """This module provides a query interface for media streams and captions."""
4
  from typing import List, Optional
5
 
pytube/streams.py CHANGED
@@ -1,4 +1,5 @@
1
  # -*- coding: utf-8 -*-
 
2
  """
3
  This module contains a container for stream manifest data.
4
 
@@ -169,7 +170,6 @@ class Stream:
169
  :returns:
170
  An os file system compatible filename.
171
  """
172
-
173
  filename = safe_filename(self.title)
174
  return f"{filename}.{self.subtype}"
175
 
 
1
  # -*- coding: utf-8 -*-
2
+
3
  """
4
  This module contains a container for stream manifest data.
5
 
 
170
  :returns:
171
  An os file system compatible filename.
172
  """
 
173
  filename = safe_filename(self.title)
174
  return f"{filename}.{self.subtype}"
175
 
pytube/version.py CHANGED
@@ -1,3 +1,4 @@
 
1
  __version__ = "9.6.1"
2
 
3
  if __name__ == "__main__":
 
1
+ # -*- coding: utf-8 -*-
2
  __version__ = "9.6.1"
3
 
4
  if __name__ == "__main__":
tests/test_streams.py CHANGED
@@ -199,29 +199,23 @@ def test_on_complete_hook(cipher_signature, mocker):
199
 
200
  def test_author(cipher_signature):
201
  expected = "Test author"
202
- cipher_signature.player_config_args = {
203
- "player_response": {"videoDetails": {"author": expected}}
204
- }
205
  assert cipher_signature.author == expected
206
 
207
  expected = "unknown"
208
- cipher_signature.player_config_args = {}
209
  assert cipher_signature.author == expected
210
 
211
 
212
  def test_thumbnail_when_in_details(cipher_signature):
213
  expected = "some url"
214
- cipher_signature.player_config_args = {
215
- "player_response": {
216
- "videoDetails": {"thumbnail": {"thumbnails": [{"url": expected}]}}
217
- }
218
- }
219
  assert cipher_signature.thumbnail_url == expected
220
 
221
 
222
  def test_thumbnail_when_not_in_details(cipher_signature):
223
  expected = "https://img.youtube.com/vi/9bZkp7q19f0/maxresdefault.jpg"
224
- cipher_signature.player_config_args = {}
225
  assert cipher_signature.thumbnail_url == expected
226
 
227
 
 
199
 
200
  def test_author(cipher_signature):
201
  expected = "Test author"
202
+ cipher_signature.player_response = {"videoDetails": {"author": expected}}
 
 
203
  assert cipher_signature.author == expected
204
 
205
  expected = "unknown"
206
+ cipher_signature.player_response = {}
207
  assert cipher_signature.author == expected
208
 
209
 
210
  def test_thumbnail_when_in_details(cipher_signature):
211
  expected = "some url"
212
+ cipher_signature.player_response = {"videoDetails": {"thumbnail": {"thumbnails": [{"url": expected}]}}}
 
 
 
 
213
  assert cipher_signature.thumbnail_url == expected
214
 
215
 
216
  def test_thumbnail_when_not_in_details(cipher_signature):
217
  expected = "https://img.youtube.com/vi/9bZkp7q19f0/maxresdefault.jpg"
218
+ cipher_signature.player_response = {}
219
  assert cipher_signature.thumbnail_url == expected
220
 
221