hbmartin commited on
Commit
3ffc2f4
·
unverified ·
2 Parent(s): 25de36d 6d844dd

Merge branch 'master' into regex-refactor

Browse files
.deepsource.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+
3
+ test_patterns = [
4
+ "*/tests/**"
5
+ ]
6
+
7
+ exclude_patterns = [
8
+ "setup.py"
9
+ ]
10
+
11
+ [[analyzers]]
12
+ name = "python"
13
+ enabled = true
14
+
15
+ [analyzers.meta]
16
+ runtime_version = "3.x.x"
.flake8 CHANGED
@@ -1,3 +1,3 @@
1
  [flake8]
2
- ignore = E231,E203,W605
3
  max-line-length = 88
 
1
  [flake8]
2
+ ignore = E231,E203,W503
3
  max-line-length = 88
.readthedocs.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .readthedocs.yml
2
+ # Read the Docs configuration file
3
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4
+
5
+ # Required
6
+ version: 2
7
+
8
+ # Build documentation in the docs/ directory with Sphinx
9
+ sphinx:
10
+ configuration: docs/conf.py
11
+
12
+ # Optionally build your docs in additional formats such as PDF and ePub
13
+ formats: all
14
+
15
+ # Optionally set the version of Python and requirements required to build your docs
16
+ python:
17
+ version: 3.7
18
+ install:
19
+ - requirements: docs/requirements.txt
README.md CHANGED
@@ -2,10 +2,11 @@
2
  <div align="center">
3
  <p align="center">
4
  <a href="https://pypi.org/project/pytube3/"><img src="https://img.shields.io/pypi/v/pytube3.svg" alt="pypi"></a>
 
5
  <a href="https://travis-ci.com/hbmartin/pytube3/"><img src="https://travis-ci.org/hbmartin/pytube3.svg?branch=master" /></a>
6
  <a href='https://pytube3.readthedocs.io/en/latest/?badge=latest'><img src='https://readthedocs.org/projects/pytube3/badge/?version=latest' alt='Documentation Status' /></a>
7
  <a href="https://coveralls.io/github/hbmartin/pytube3?branch=master"><img src="https://coveralls.io/repos/github/hbmartin/pytube3/badge.svg?branch=master" /></a>
8
- <a href="https://pypi.python.org/pypi/pytube3/"><img src="https://img.shields.io/pypi/pyversions/pytube3.svg" /></a>
9
  <a href="https://github.com/ambv/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg" /></a>
10
  <a href="https://twitter.com/hmartin"><img src="https://img.shields.io/badge/[email protected]?style=flat" /></a>
11
  </p>
@@ -243,6 +244,8 @@ Finally, if you're filing a bug report, the cli contains a switch called ``--bui
243
 
244
  ## Development
245
 
 
 
246
  Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
247
 
248
  To run code checking before a PR use ``make test``
 
2
  <div align="center">
3
  <p align="center">
4
  <a href="https://pypi.org/project/pytube3/"><img src="https://img.shields.io/pypi/v/pytube3.svg" alt="pypi"></a>
5
+ <a href="https://pypi.python.org/pypi/pytube3/"><img src="https://img.shields.io/pypi/pyversions/pytube3.svg" /></a>
6
  <a href="https://travis-ci.com/hbmartin/pytube3/"><img src="https://travis-ci.org/hbmartin/pytube3.svg?branch=master" /></a>
7
  <a href='https://pytube3.readthedocs.io/en/latest/?badge=latest'><img src='https://readthedocs.org/projects/pytube3/badge/?version=latest' alt='Documentation Status' /></a>
8
  <a href="https://coveralls.io/github/hbmartin/pytube3?branch=master"><img src="https://coveralls.io/repos/github/hbmartin/pytube3/badge.svg?branch=master" /></a>
9
+ <a href="https://www.codefactor.io/repository/github/hbmartin/pytube3/overview/master"><img src="https://www.codefactor.io/repository/github/hbmartin/pytube3/badge/master" alt="CodeFactor" /></a>
10
  <a href="https://github.com/ambv/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg" /></a>
11
  <a href="https://twitter.com/hmartin"><img src="https://img.shields.io/badge/[email protected]?style=flat" /></a>
12
  </p>
 
244
 
245
  ## Development
246
 
247
+ <a href="https://deepsource.io/gh/hbmartin/pytube3/?ref=repository-badge" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://static.deepsource.io/deepsource-badge-light-mini.svg"></a>
248
+
249
  Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
250
 
251
  To run code checking before a PR use ``make test``
docs/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ typing_extensions
pytube/__main__.py CHANGED
@@ -23,13 +23,12 @@ from pytube import Stream
23
  from pytube import StreamQuery
24
  from pytube.mixins import install_proxy
25
  from pytube.exceptions import VideoUnavailable
26
- from pytube.helpers import apply_mixin
27
  from pytube.monostate import OnProgress, OnComplete, Monostate
28
 
29
  logger = logging.getLogger(__name__)
30
 
31
 
32
- class YouTube(object):
33
  """Core developer interface for pytube."""
34
 
35
  def __init__(
@@ -91,16 +90,8 @@ class YouTube(object):
91
  install_proxy(proxies)
92
 
93
  if not defer_prefetch_init:
94
- self.prefetch_descramble()
95
-
96
- def prefetch_descramble(self) -> None:
97
- """Download data, descramble it, and build Stream instances.
98
-
99
- :rtype: None
100
-
101
- """
102
- self.prefetch()
103
- self.descramble()
104
 
105
  def descramble(self) -> None:
106
  """Descramble the stream data and build Stream instances.
@@ -161,7 +152,9 @@ class YouTube(object):
161
  self.initialize_stream_objects(fmt)
162
 
163
  # load the player_response object (contains subtitle information)
164
- apply_mixin(self.player_config_args, "player_response", json.loads)
 
 
165
 
166
  self.initialize_caption_objects()
167
  logger.info("init finished successfully")
@@ -283,7 +276,11 @@ class YouTube(object):
283
  :rtype: str
284
 
285
  """
286
- return self.player_config_args["title"]
 
 
 
 
287
 
288
  @property
289
  def description(self) -> str:
@@ -292,7 +289,11 @@ class YouTube(object):
292
  :rtype: str
293
 
294
  """
295
- return self.vid_descr
 
 
 
 
296
 
297
  @property
298
  def rating(self) -> float:
@@ -314,7 +315,7 @@ class YouTube(object):
314
  :rtype: str
315
 
316
  """
317
- return (
318
  self.player_config_args.get("player_response", {})
319
  .get("videoDetails", {})
320
  .get("lengthSeconds")
 
23
  from pytube import StreamQuery
24
  from pytube.mixins import install_proxy
25
  from pytube.exceptions import VideoUnavailable
 
26
  from pytube.monostate import OnProgress, OnComplete, Monostate
27
 
28
  logger = logging.getLogger(__name__)
29
 
30
 
31
+ class YouTube:
32
  """Core developer interface for pytube."""
33
 
34
  def __init__(
 
90
  install_proxy(proxies)
91
 
92
  if not defer_prefetch_init:
93
+ self.prefetch()
94
+ self.descramble()
 
 
 
 
 
 
 
 
95
 
96
  def descramble(self) -> None:
97
  """Descramble the stream data and build Stream instances.
 
152
  self.initialize_stream_objects(fmt)
153
 
154
  # load the player_response object (contains subtitle information)
155
+ self.player_config_args["player_response"] = json.loads(
156
+ self.player_config_args["player_response"]
157
+ )
158
 
159
  self.initialize_caption_objects()
160
  logger.info("init finished successfully")
 
276
  :rtype: str
277
 
278
  """
279
+ return self.player_config_args.get("title") or (
280
+ self.player_config_args.get("player_response", {})
281
+ .get("videoDetails", {})
282
+ .get("title")
283
+ )
284
 
285
  @property
286
  def description(self) -> str:
 
289
  :rtype: str
290
 
291
  """
292
+ return self.vid_descr or (
293
+ self.player_config_args.get("player_response", {})
294
+ .get("videoDetails", {})
295
+ .get("shortDescription")
296
+ )
297
 
298
  @property
299
  def rating(self) -> float:
 
315
  :rtype: str
316
 
317
  """
318
+ return self.player_config_args.get("length_seconds") or (
319
  self.player_config_args.get("player_response", {})
320
  .get("videoDetails", {})
321
  .get("lengthSeconds")
pytube/captions.py CHANGED
@@ -34,7 +34,8 @@ class Caption:
34
  """
35
  return self.xml_caption_to_srt(self.xml_captions)
36
 
37
- def float_to_srt_time_format(self, d: float) -> str:
 
38
  """Convert decimal durations into proper srt format.
39
 
40
  :rtype: str
@@ -56,7 +57,7 @@ class Caption:
56
  """
57
  segments = []
58
  root = ElementTree.fromstring(xml_captions)
59
- for i, child in enumerate(root.getchildren()):
60
  text = child.text or ""
61
  caption = unescape(text.replace("\n", " ").replace(" ", " "),)
62
  duration = float(child.attrib["dur"])
 
34
  """
35
  return self.xml_caption_to_srt(self.xml_captions)
36
 
37
+ @staticmethod
38
+ def float_to_srt_time_format(d: float) -> str:
39
  """Convert decimal durations into proper srt format.
40
 
41
  :rtype: str
 
57
  """
58
  segments = []
59
  root = ElementTree.fromstring(xml_captions)
60
+ for i, child in enumerate(list(root)):
61
  text = child.text or ""
62
  caption = unescape(text.replace("\n", " ").replace(" ", " "),)
63
  duration = float(child.attrib["dur"])
pytube/cipher.py CHANGED
@@ -205,14 +205,14 @@ def map_functions(js_func: str) -> Callable:
205
  """
206
  mapper = (
207
  # function(a){a.reverse()}
208
- ("{\w\.reverse\(\)}", reverse),
209
  # function(a,b){a.splice(0,b)}
210
- ("{\w\.splice\(0,\w\)}", splice),
211
  # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
212
- ("{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\]=\w}", swap),
213
  # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
214
  (
215
- "{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];" "\w\[\w\%\w.length\]=\w}",
216
  swap,
217
  ),
218
  )
 
205
  """
206
  mapper = (
207
  # function(a){a.reverse()}
208
+ (r"{\w\.reverse\(\)}", reverse),
209
  # function(a,b){a.splice(0,b)}
210
+ (r"{\w\.splice\(0,\w\)}", splice),
211
  # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
212
+ (r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\]=\w}", swap),
213
  # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
214
  (
215
+ r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\%\w.length\]=\w}",
216
  swap,
217
  ),
218
  )
pytube/cli.py CHANGED
@@ -8,7 +8,8 @@ import json
8
  import logging
9
  import os
10
  import sys
11
- from typing import Tuple
 
12
 
13
  from pytube import __version__
14
  from pytube import YouTube
@@ -19,6 +20,7 @@ logger = logging.getLogger(__name__)
19
 
20
  def main():
21
  """Command line application to download youtube videos."""
 
22
  parser = argparse.ArgumentParser(description=main.__doc__)
23
  parser.add_argument("url", help="The YouTube /watch url", nargs="?")
24
  parser.add_argument(
@@ -128,14 +130,18 @@ def display_progress_bar(
128
 
129
  filled = int(round(max_width * bytes_received / float(filesize)))
130
  remaining = max_width - filled
131
- bar = ch * filled + " " * remaining
132
  percent = round(100.0 * bytes_received / float(filesize), 1)
133
- text = " ↳ |{bar}| {percent}%\r".format(bar=bar, percent=percent)
 
 
134
  sys.stdout.write(text)
135
  sys.stdout.flush()
136
 
137
 
138
- def on_progress(stream, chunk, file_handle, bytes_remaining):
 
 
139
  filesize = stream.filesize
140
  bytes_received = filesize - bytes_remaining
141
  display_progress_bar(bytes_received, filesize)
 
8
  import logging
9
  import os
10
  import sys
11
+ from io import BufferedWriter
12
+ from typing import Tuple, Any
13
 
14
  from pytube import __version__
15
  from pytube import YouTube
 
20
 
21
  def main():
22
  """Command line application to download youtube videos."""
23
+ # noinspection PyTypeChecker
24
  parser = argparse.ArgumentParser(description=main.__doc__)
25
  parser.add_argument("url", help="The YouTube /watch url", nargs="?")
26
  parser.add_argument(
 
130
 
131
  filled = int(round(max_width * bytes_received / float(filesize)))
132
  remaining = max_width - filled
133
+ progress_bar = ch * filled + " " * remaining
134
  percent = round(100.0 * bytes_received / float(filesize), 1)
135
+ text = " ↳ |{progress_bar}| {percent}%\r".format(
136
+ progress_bar=progress_bar, percent=percent
137
+ )
138
  sys.stdout.write(text)
139
  sys.stdout.flush()
140
 
141
 
142
+ def on_progress(
143
+ stream: Any, chunk: Any, file_handler: BufferedWriter, bytes_remaining: int
144
+ ) -> None:
145
  filesize = stream.filesize
146
  bytes_received = filesize - bytes_remaining
147
  display_progress_bar(bytes_received, filesize)
pytube/contrib/playlist.py CHANGED
@@ -1,7 +1,6 @@
1
  # -*- coding: utf-8 -*-
2
- """
3
- Module to download a complete playlist from a youtube channel
4
- """
5
  import json
6
  import logging
7
  import re
@@ -14,7 +13,7 @@ from pytube.__main__ import YouTube
14
  logger = logging.getLogger(__name__)
15
 
16
 
17
- class Playlist(object):
18
  """Handles all the task of manipulating and downloading a whole YouTube
19
  playlist
20
  """
@@ -40,7 +39,8 @@ class Playlist(object):
40
  # url is already in the desired format, so just return it
41
  return self.playlist_url
42
 
43
- def _load_more_url(self, req):
 
44
  """Given an html page or a fragment thereof, looks for
45
  and returns the "load more" url if found.
46
  """
@@ -70,8 +70,8 @@ class Playlist(object):
70
  # The above only returns 100 or fewer links
71
  # Simulating a browser request for the load more link
72
  load_more_url = self._load_more_url(req)
73
- while len(load_more_url): # there is an url found
74
- logger.debug("load more url: %s" % load_more_url)
75
  req = request.get(load_more_url)
76
  load_more = json.loads(req)
77
  videos = re.findall(
@@ -155,8 +155,6 @@ class Playlist(object):
155
  logger.debug(e)
156
  if not self.suppress_exception:
157
  raise e
158
- else:
159
- logger.debug("Exception suppressed")
160
  else:
161
  # TODO: this should not be hardcoded to a single user's
162
  # preference
@@ -177,26 +175,21 @@ class Playlist(object):
177
  logger.debug("download complete")
178
 
179
  def title(self) -> Optional[str]:
180
- """return playlist title (name)
181
- """
182
- try:
183
- url = self.construct_playlist_url()
184
- req = request.get(url)
185
- open_tag = "<title>"
186
- end_tag = "</title>"
187
- pattern = re.compile(open_tag + "(.+?)" + end_tag)
188
- match = pattern.search(req)
189
-
190
- if match is None:
191
- return None
192
-
193
- return (
194
- match.group()
195
- .replace(open_tag, "")
196
- .replace(end_tag, "")
197
- .replace("- YouTube", "")
198
- .strip()
199
- )
200
- except Exception as e:
201
- logger.debug(e)
202
  return None
 
 
 
 
 
 
 
 
 
1
  # -*- coding: utf-8 -*-
2
+ """Module to download a complete playlist from a youtube channel"""
3
+
 
4
  import json
5
  import logging
6
  import re
 
13
  logger = logging.getLogger(__name__)
14
 
15
 
16
+ class Playlist:
17
  """Handles all the task of manipulating and downloading a whole YouTube
18
  playlist
19
  """
 
39
  # url is already in the desired format, so just return it
40
  return self.playlist_url
41
 
42
+ @staticmethod
43
+ def _load_more_url(req):
44
  """Given an html page or a fragment thereof, looks for
45
  and returns the "load more" url if found.
46
  """
 
70
  # The above only returns 100 or fewer links
71
  # Simulating a browser request for the load more link
72
  load_more_url = self._load_more_url(req)
73
+ while len(load_more_url) > 0: # there is an url found
74
+ logger.debug("load more url: %s", load_more_url)
75
  req = request.get(load_more_url)
76
  load_more = json.loads(req)
77
  videos = re.findall(
 
155
  logger.debug(e)
156
  if not self.suppress_exception:
157
  raise e
 
 
158
  else:
159
  # TODO: this should not be hardcoded to a single user's
160
  # preference
 
175
  logger.debug("download complete")
176
 
177
  def title(self) -> Optional[str]:
178
+ """return playlist title (name)"""
179
+ url = self.construct_playlist_url()
180
+ req = request.get(url)
181
+ open_tag = "<title>"
182
+ end_tag = "</title>"
183
+ pattern = re.compile(open_tag + "(.+?)" + end_tag)
184
+ match = pattern.search(req)
185
+
186
+ if match is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return None
188
+
189
+ return (
190
+ match.group()
191
+ .replace(open_tag, "")
192
+ .replace(end_tag, "")
193
+ .replace("- YouTube", "")
194
+ .strip()
195
+ )
pytube/helpers.py CHANGED
@@ -37,25 +37,6 @@ def regex_search(pattern: str, string: str, group: int) -> str:
37
  return results.group(group)
38
 
39
 
40
- def apply_mixin(dct, key, func, *args, **kwargs):
41
- r"""Apply in-place data mutation to a dictionary.
42
-
43
- :param dict dct:
44
- Dictionary to apply mixin function to.
45
- :param str key:
46
- Key within dictionary to apply mixin function to.
47
- :param callable func:
48
- Transform function to apply to ``dct[key]``.
49
- :param \*args:
50
- (optional) positional arguments that ``func`` takes.
51
- :param \*\*kwargs:
52
- (optional) keyword arguments that ``func`` takes.
53
- :rtype:
54
- None
55
- """
56
- dct[key] = func(dct[key], *args, **kwargs)
57
-
58
-
59
  def safe_filename(s: str, max_length: int = 255) -> str:
60
  """Sanitize a string making it safe to use as a filename.
61
 
@@ -73,26 +54,26 @@ def safe_filename(s: str, max_length: int = 255) -> str:
73
  # Characters in range 0-31 (0x00-0x1F) are not allowed in ntfs filenames.
74
  ntfs_characters = [chr(i) for i in range(0, 31)]
75
  characters = [
76
- '"',
77
- "\#",
78
- "\$",
79
- "\%",
80
- "'",
81
- "\*",
82
- "\,",
83
- "\.",
84
- "\/",
85
- "\:",
86
- '"',
87
- "\;",
88
- "\<",
89
- "\>",
90
- "\?",
91
- "\\",
92
- "\^",
93
- "\|",
94
- "\~",
95
- "\\\\",
96
  ]
97
  pattern = "|".join(ntfs_characters + characters)
98
  regex = re.compile(pattern, re.UNICODE)
 
37
  return results.group(group)
38
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  def safe_filename(s: str, max_length: int = 255) -> str:
41
  """Sanitize a string making it safe to use as a filename.
42
 
 
54
  # Characters in range 0-31 (0x00-0x1F) are not allowed in ntfs filenames.
55
  ntfs_characters = [chr(i) for i in range(0, 31)]
56
  characters = [
57
+ r'"',
58
+ r"\#",
59
+ r"\$",
60
+ r"\%",
61
+ r"'",
62
+ r"\*",
63
+ r"\,",
64
+ r"\.",
65
+ r"\/",
66
+ r"\:",
67
+ r'"',
68
+ r"\;",
69
+ r"\<",
70
+ r"\>",
71
+ r"\?",
72
+ r"\\",
73
+ r"\^",
74
+ r"\|",
75
+ r"\~",
76
+ r"\\\\",
77
  ]
78
  pattern = "|".join(ntfs_characters + characters)
79
  regex = re.compile(pattern, re.UNICODE)
pytube/query.py CHANGED
@@ -167,7 +167,7 @@ class StreamQuery:
167
  return StreamQuery(fmt_streams)
168
 
169
  def order_by(self, attribute_name: str) -> "StreamQuery":
170
- """Apply a sort order to a resultset. Filters out stream the do not have the attribute.
171
 
172
  :param str attribute_name:
173
  The name of the attribute to sort by.
@@ -191,7 +191,6 @@ class StreamQuery:
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:
@@ -201,10 +200,10 @@ class StreamQuery:
201
  key=lambda s: integer_attr_repr[getattr(s, attribute_name)], # type: ignore # noqa: E501
202
  )
203
  )
204
- else:
205
- return StreamQuery(
206
- sorted(has_attribute, key=lambda s: getattr(s, attribute_name))
207
- )
208
 
209
  def desc(self) -> "StreamQuery":
210
  """Sort streams in descending order.
 
167
  return StreamQuery(fmt_streams)
168
 
169
  def order_by(self, attribute_name: str) -> "StreamQuery":
170
+ """Apply a sort order. Filters out stream the do not have the attribute.
171
 
172
  :param str attribute_name:
173
  The name of the attribute to sort by.
 
191
  }
192
  except ValueError:
193
  integer_attr_repr = None
 
194
 
195
  # lookup integer values if we have them
196
  if integer_attr_repr is not None:
 
200
  key=lambda s: integer_attr_repr[getattr(s, attribute_name)], # type: ignore # noqa: E501
201
  )
202
  )
203
+
204
+ return StreamQuery(
205
+ sorted(has_attribute, key=lambda s: getattr(s, attribute_name))
206
+ )
207
 
208
  def desc(self) -> "StreamQuery":
209
  """Sort streams in descending order.
pytube/request.py CHANGED
@@ -17,15 +17,16 @@ def get(url, headers=False, streaming=False, chunk_size=8192):
17
  The size in bytes of each chunk. Defaults to 8*1024
18
  """
19
 
20
- # https://github.com/nficano/pytube/pull/465
21
  req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
22
  response = urlopen(req)
23
 
24
  if streaming:
25
  return stream_response(response, chunk_size)
26
- elif headers:
 
27
  # https://github.com/nficano/pytube/issues/160
28
  return {k.lower(): v for k, v in response.info().items()}
 
29
  return response.read().decode("utf-8")
30
 
31
 
 
17
  The size in bytes of each chunk. Defaults to 8*1024
18
  """
19
 
 
20
  req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
21
  response = urlopen(req)
22
 
23
  if streaming:
24
  return stream_response(response, chunk_size)
25
+
26
+ if headers:
27
  # https://github.com/nficano/pytube/issues/160
28
  return {k.lower(): v for k, v in response.info().items()}
29
+
30
  return response.read().decode("utf-8")
31
 
32
 
pytube/streams.py CHANGED
@@ -23,7 +23,7 @@ from pytube.monostate import Monostate
23
  logger = logging.getLogger(__name__)
24
 
25
 
26
- class Stream(object):
27
  """Container for stream manifest data."""
28
 
29
  def __init__(self, stream: Dict, player_config_args: Dict, monostate: Monostate):
@@ -176,20 +176,16 @@ class Stream(object):
176
  :returns:
177
  Youtube video title
178
  """
179
- player_config_args = self.player_config_args or {}
180
-
181
- if "title" in player_config_args:
182
- return player_config_args["title"]
183
-
184
- details = self.player_config_args.get("player_response", {},).get(
185
- "videoDetails", {}
 
186
  )
187
 
188
- if "title" in details:
189
- return details["title"]
190
-
191
- return "Unknown YouTube Video Title"
192
-
193
  @property
194
  def default_filename(self) -> str:
195
  """Generate filename based on the video title.
 
23
  logger = logging.getLogger(__name__)
24
 
25
 
26
+ class Stream:
27
  """Container for stream manifest data."""
28
 
29
  def __init__(self, stream: Dict, player_config_args: Dict, monostate: Monostate):
 
176
  :returns:
177
  Youtube video title
178
  """
179
+ return (
180
+ self.player_config_args.get("title")
181
+ or (
182
+ self.player_config_args.get("player_response", {})
183
+ .get("videoDetails", {})
184
+ .get("title")
185
+ )
186
+ or "Unknown YouTube Video Title"
187
  )
188
 
 
 
 
 
 
189
  @property
190
  def default_filename(self) -> str:
191
  """Generate filename based on the video title.
tests/test_request.py CHANGED
@@ -18,9 +18,8 @@ def test_get_streaming(mock_urlopen):
18
  response.read.side_effect = fake_stream_binary
19
  mock_urlopen.return_value = response
20
  response = request.get("http://fakeassurl.gov", streaming=True)
21
- call_count = 0
22
- for i in response:
23
- call_count += 1
24
  assert call_count == 3
25
 
26
 
 
18
  response.read.side_effect = fake_stream_binary
19
  mock_urlopen.return_value = response
20
  response = request.get("http://fakeassurl.gov", streaming=True)
21
+ call_count = len(list(response))
22
+
 
23
  assert call_count == 3
24
 
25