Taylor Fox Dahlin commited on
Commit
e573655
1 Parent(s): 181c88c

Bugfixes (#1045)

Browse files

* Removed special character from __author__ attribute.

* Changed -v CLI arg to have a single setting, rather than multiple.

* Add retry functionality for IncompleteRead errors.

* Extract contentLength from info where possible.

* Mock open in final streams test to prevent file from being written.

* Exception handling for accessing titles of private videos.

pytube/__init__.py CHANGED
@@ -4,7 +4,7 @@
4
  Pytube: a very serious Python library for downloading YouTube Videos.
5
  """
6
  __title__ = "pytube"
7
- __author__ = "Ronnie Ghose, Taylor Fox Dahlin, Nick Ficano"
8
  __license__ = "The Unlicense (Unlicense)"
9
  __js__ = None
10
  __js_url__ = None
 
4
  Pytube: a very serious Python library for downloading YouTube Videos.
5
  """
6
  __title__ = "pytube"
7
+ __author__ = "Ronnie Ghose, Taylor Fox Dahlin, Nick Ficano"
8
  __license__ = "The Unlicense (Unlicense)"
9
  __js__ = None
10
  __js_url__ = None
pytube/__main__.py CHANGED
@@ -351,7 +351,20 @@ class YouTube:
351
  """
352
  if self._title:
353
  return self._title
354
- self._title = self.player_response['videoDetails']['title']
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  return self._title
356
 
357
  @title.setter
 
351
  """
352
  if self._title:
353
  return self._title
354
+
355
+ try:
356
+ self._title = self.player_response['videoDetails']['title']
357
+ except KeyError:
358
+ # Check_availability will raise the correct exception in most cases
359
+ # if it doesn't, ask for a report.
360
+ self.check_availability()
361
+ raise exceptions.PytubeError(
362
+ (
363
+ f'Exception while accessing title of {self.watch_url}. '
364
+ 'Please file a bug report at https://github.com/pytube/pytube'
365
+ )
366
+ )
367
+
368
  return self._title
369
 
370
  @title.setter
pytube/cli.py CHANGED
@@ -17,17 +17,20 @@ from pytube import CaptionQuery, Playlist, Stream, YouTube
17
  from pytube.helpers import safe_filename, setup_logger
18
 
19
 
 
 
 
20
  def main():
21
  """Command line application to download youtube videos."""
22
  # noinspection PyTypeChecker
23
  parser = argparse.ArgumentParser(description=main.__doc__)
24
  args = _parse_args(parser)
25
- if args.verbosity:
26
  log_filename = None
27
- log_level = min(args.verbosity, 4) * 10
28
  if args.logfile:
29
  log_filename = args.logfile
30
- setup_logger(logging.FATAL - log_level, log_filename=log_filename)
 
31
 
32
  if not args.url or "youtu" not in args.url:
33
  parser.print_help()
@@ -113,10 +116,9 @@ def _parse_args(
113
  parser.add_argument(
114
  "-v",
115
  "--verbose",
116
- action="count",
117
- default=0,
118
- dest="verbosity",
119
- help="Verbosity level, use up to 4 to increase logging -vvvv",
120
  )
121
  parser.add_argument(
122
  "--logfile",
 
17
  from pytube.helpers import safe_filename, setup_logger
18
 
19
 
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
  def main():
24
  """Command line application to download youtube videos."""
25
  # noinspection PyTypeChecker
26
  parser = argparse.ArgumentParser(description=main.__doc__)
27
  args = _parse_args(parser)
28
+ if args.verbose:
29
  log_filename = None
 
30
  if args.logfile:
31
  log_filename = args.logfile
32
+ setup_logger(logging.DEBUG, log_filename=log_filename)
33
+ logger.debug(f'Pytube version: {__version__}')
34
 
35
  if not args.url or "youtu" not in args.url:
36
  parser.print_help()
 
116
  parser.add_argument(
117
  "-v",
118
  "--verbose",
119
+ action="store_true",
120
+ dest="verbose",
121
+ help="Set logger output to verbose output.",
 
122
  )
123
  parser.add_argument(
124
  "--logfile",
pytube/extract.py CHANGED
@@ -532,6 +532,7 @@ def apply_descrambler(stream_data: Dict, key: str) -> None:
532
  "fps": format_item["fps"] if 'video' in format_item["mimeType"] else None,
533
  "bitrate": format_item.get("bitrate"),
534
  "is_otf": (format_item.get("type") == otf_type),
 
535
  }
536
  for format_item in formats
537
  ]
@@ -554,6 +555,7 @@ def apply_descrambler(stream_data: Dict, key: str) -> None:
554
  "fps": format_item["fps"] if 'video' in format_item["mimeType"] else None,
555
  "bitrate": format_item.get("bitrate"),
556
  "is_otf": (format_item.get("type") == otf_type),
 
557
  }
558
  for i, format_item in enumerate(formats)
559
  ]
 
532
  "fps": format_item["fps"] if 'video' in format_item["mimeType"] else None,
533
  "bitrate": format_item.get("bitrate"),
534
  "is_otf": (format_item.get("type") == otf_type),
535
+ 'content_length': int(format_item.get('contentLength', 0)),
536
  }
537
  for format_item in formats
538
  ]
 
555
  "fps": format_item["fps"] if 'video' in format_item["mimeType"] else None,
556
  "bitrate": format_item.get("bitrate"),
557
  "is_otf": (format_item.get("type") == otf_type),
558
+ 'content_length': int(format_item.get('contentLength', 0)),
559
  }
560
  for i, format_item in enumerate(formats)
561
  ]
pytube/request.py CHANGED
@@ -1,4 +1,5 @@
1
  """Implements a simple wrapper around urlopen."""
 
2
  import json
3
  import logging
4
  import re
@@ -166,6 +167,9 @@ def stream(
166
  pass
167
  else:
168
  raise
 
 
 
169
  else:
170
  # On a successful request, break from loop
171
  break
 
1
  """Implements a simple wrapper around urlopen."""
2
+ import http.client
3
  import json
4
  import logging
5
  import re
 
167
  pass
168
  else:
169
  raise
170
+ except http.client.IncompleteRead:
171
+ # Allow retries on IncompleteRead errors for unreliable connections
172
+ pass
173
  else:
174
  # On a successful request, break from loop
175
  break
pytube/streams.py CHANGED
@@ -62,7 +62,7 @@ class Stream:
62
  self.is_otf: bool = stream["is_otf"]
63
  self.bitrate: Optional[int] = stream["bitrate"]
64
 
65
- self._filesize: Optional[int] = None # filesize in bytes
66
 
67
  # Additional information about the stream format, such as resolution,
68
  # frame rate, and whether the stream is live (HLS) or 3D.
@@ -147,7 +147,7 @@ class Stream:
147
  :returns:
148
  Filesize (in bytes) of the stream.
149
  """
150
- if self._filesize is None:
151
  try:
152
  self._filesize = request.filesize(self.url)
153
  except HTTPError as e:
 
62
  self.is_otf: bool = stream["is_otf"]
63
  self.bitrate: Optional[int] = stream["bitrate"]
64
 
65
+ self._filesize: Optional[int] = stream['content_length'] # filesize in bytes
66
 
67
  # Additional information about the stream format, such as resolution,
68
  # frame rate, and whether the stream is live (HLS) or 3D.
 
147
  :returns:
148
  Filesize (in bytes) of the stream.
149
  """
150
+ if self._filesize == 0:
151
  try:
152
  self._filesize = request.filesize(self.url)
153
  except HTTPError as e:
tests/test_cli.py CHANGED
@@ -1,4 +1,5 @@
1
  import argparse
 
2
  import os
3
  from unittest import mock
4
  from unittest.mock import MagicMock, patch
@@ -139,7 +140,7 @@ def test_parse_args_falsey():
139
  assert args.build_playback_report is False
140
  assert args.itag is None
141
  assert args.list is False
142
- assert args.verbosity == 0
143
 
144
 
145
  def test_parse_args_truthy():
@@ -160,7 +161,7 @@ def test_parse_args_truthy():
160
  assert args.build_playback_report is True
161
  assert args.itag == 10
162
  assert args.list is True
163
- assert args.verbosity == 3
164
 
165
 
166
  @mock.patch("pytube.cli.setup_logger", return_value=None)
@@ -173,7 +174,7 @@ def test_main_logging_setup(setup_logger):
173
  with pytest.raises(SystemExit):
174
  cli.main()
175
  # Then
176
- setup_logger.assert_called_with(40, log_filename=None)
177
 
178
 
179
  @mock.patch("pytube.cli.YouTube", return_value=None)
 
1
  import argparse
2
+ import logging
3
  import os
4
  from unittest import mock
5
  from unittest.mock import MagicMock, patch
 
140
  assert args.build_playback_report is False
141
  assert args.itag is None
142
  assert args.list is False
143
+ assert args.verbose is False
144
 
145
 
146
  def test_parse_args_truthy():
 
161
  assert args.build_playback_report is True
162
  assert args.itag == 10
163
  assert args.list is True
164
+ assert args.verbose is True
165
 
166
 
167
  @mock.patch("pytube.cli.setup_logger", return_value=None)
 
174
  with pytest.raises(SystemExit):
175
  cli.main()
176
  # Then
177
+ setup_logger.assert_called_with(logging.DEBUG, log_filename=None)
178
 
179
 
180
  @mock.patch("pytube.cli.YouTube", return_value=None)
tests/test_streams.py CHANGED
@@ -27,22 +27,16 @@ def test_stream_to_buffer(mock_request, cipher_signature):
27
  assert buffer.write.call_count == 3
28
 
29
 
30
- @mock.patch(
31
- "pytube.streams.request.head", MagicMock(return_value={"content-length": "6796391"})
32
- )
33
  def test_filesize(cipher_signature):
34
- assert cipher_signature.streams[0].filesize == 6796391
35
 
36
 
37
- @mock.patch(
38
- "pytube.streams.request.head", MagicMock(return_value={"content-length": "6796391"})
39
- )
40
  def test_filesize_approx(cipher_signature):
41
  stream = cipher_signature.streams[0]
42
 
43
  assert stream.filesize_approx == 28309811
44
  stream.bitrate = None
45
- assert stream.filesize_approx == 6796391
46
 
47
 
48
  def test_default_filename(cipher_signature):
@@ -345,14 +339,11 @@ def test_segmented_stream_on_404(cipher_signature):
345
  ]
346
 
347
  # Request order for stream:
348
- # Filesize:
349
- # 1. head(url) -> 404
350
- # 2. get(url&sn=0)
351
- # 3. head(url&sn=[1,2,3])
352
- # Download:
353
- # 4. info(url) -> 404
354
- # 5. get(url&sn=0)
355
- # 6. get(url&sn=[1,2,3])
356
 
357
  # Handle filesize requests
358
  mock_head.side_effect = [
@@ -363,7 +354,6 @@ def test_segmented_stream_on_404(cipher_signature):
363
  # Each response must be followed by None, to break iteration
364
  # in the stream() function
365
  mock_url_open_object.read.side_effect = [
366
- responses[0], None,
367
  responses[0], None,
368
  responses[1], None,
369
  responses[2], None,
@@ -394,5 +384,6 @@ def test_segmented_only_catches_404(cipher_signature):
394
  stream = cipher_signature.streams.filter(adaptive=True)[0]
395
  with mock.patch('pytube.request.head') as mock_head:
396
  mock_head.side_effect = HTTPError('', 403, 'Forbidden', '', '')
397
- with pytest.raises(HTTPError):
398
- stream.download()
 
 
27
  assert buffer.write.call_count == 3
28
 
29
 
 
 
 
30
  def test_filesize(cipher_signature):
31
+ assert cipher_signature.streams[0].filesize == 28282013
32
 
33
 
 
 
 
34
  def test_filesize_approx(cipher_signature):
35
  stream = cipher_signature.streams[0]
36
 
37
  assert stream.filesize_approx == 28309811
38
  stream.bitrate = None
39
+ assert stream.filesize_approx == 28282013
40
 
41
 
42
  def test_default_filename(cipher_signature):
 
339
  ]
340
 
341
  # Request order for stream:
342
+ # 1. get(url&sn=0)
343
+ # 2. head(url&sn=[1,2,3])
344
+ # 3. info(url) -> 404
345
+ # 4. get(url&sn=0)
346
+ # 5. get(url&sn=[1,2,3])
 
 
 
347
 
348
  # Handle filesize requests
349
  mock_head.side_effect = [
 
354
  # Each response must be followed by None, to break iteration
355
  # in the stream() function
356
  mock_url_open_object.read.side_effect = [
 
357
  responses[0], None,
358
  responses[1], None,
359
  responses[2], None,
 
384
  stream = cipher_signature.streams.filter(adaptive=True)[0]
385
  with mock.patch('pytube.request.head') as mock_head:
386
  mock_head.side_effect = HTTPError('', 403, 'Forbidden', '', '')
387
+ with mock.patch("pytube.streams.open", mock.mock_open(), create=True):
388
+ with pytest.raises(HTTPError):
389
+ stream.download()