nficano commited on
Commit
ee0deb6
·
unverified ·
1 Parent(s): f1cd745
.deepsource.toml CHANGED
@@ -13,4 +13,4 @@ name = "python"
13
  enabled = true
14
 
15
  [analyzers.meta]
16
- runtime_version = "3.x.x"
 
13
  enabled = true
14
 
15
  [analyzers.meta]
16
+ runtime_version = "3.x.x"
.idea/dictionaries/haroldmartin.xml CHANGED
@@ -46,4 +46,4 @@
46
  <w>ytplayer</w>
47
  </words>
48
  </dictionary>
49
- </component>
 
46
  <w>ytplayer</w>
47
  </words>
48
  </dictionary>
49
+ </component>
.idea/inspectionProfiles/profiles_settings.xml CHANGED
@@ -3,4 +3,4 @@
3
  <option name="USE_PROJECT_PROFILE" value="false" />
4
  <version value="1.0" />
5
  </settings>
6
- </component>
 
3
  <option name="USE_PROJECT_PROFILE" value="false" />
4
  <version value="1.0" />
5
  </settings>
6
+ </component>
.idea/misc.xml CHANGED
@@ -1,4 +1,4 @@
1
  <?xml version="1.0" encoding="UTF-8"?>
2
  <project version="4">
3
  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
4
- </project>
 
1
  <?xml version="1.0" encoding="UTF-8"?>
2
  <project version="4">
3
  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
4
+ </project>
.idea/vcs.xml CHANGED
@@ -3,4 +3,4 @@
3
  <component name="VcsDirectoryMappings">
4
  <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
  </component>
6
- </project>
 
3
  <component name="VcsDirectoryMappings">
4
  <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
  </component>
6
+ </project>
docs/conf.py CHANGED
@@ -5,38 +5,39 @@ import os
5
  import sys
6
 
7
  import sphinx_rtd_theme
8
- sys.path.insert(0, os.path.abspath('../'))
 
9
 
10
  from pytube import __version__ # noqa
11
 
12
  # -- General configuration ------------------------------------------------
13
 
14
  extensions = [
15
- 'sphinx.ext.autodoc',
16
- 'sphinx.ext.autosummary',
17
- 'sphinx.ext.todo',
18
- 'sphinx.ext.intersphinx',
19
- 'sphinx.ext.viewcode',
20
  ]
21
 
22
  autosummary_generate = True
23
 
24
  # Add any paths that contain templates here, relative to this directory.
25
- templates_path = ['_templates']
26
 
27
  # The suffix(es) of source filenames.
28
  # You can specify multiple suffix as a list of string:
29
  #
30
  # source_suffix = ['.rst', '.md']
31
- source_suffix = '.rst'
32
 
33
  # The master toctree document.
34
- master_doc = 'index'
35
 
36
  # General information about the project.
37
- project = 'pytube3'
38
- copyright = '2019, Nick Ficano'
39
- author = 'Nick Ficano, Harold Martin'
40
 
41
  # The version info for the project you're documenting, acts as replacement for
42
  # |version| and |release|, also used in various other places throughout the
@@ -57,16 +58,16 @@ language = None
57
  # List of patterns, relative to source directory, that match files and
58
  # directories to ignore when looking for source files.
59
  # This patterns also effect to html_static_path and html_extra_path
60
- exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
61
 
62
  # The name of the Pygments (syntax highlighting) style to use.
63
- pygments_style = 'sphinx'
64
 
65
  # If true, `todo` and `todoList` produce output, else they produce nothing.
66
  todo_include_todos = True
67
 
68
  intersphinx_mapping = {
69
- 'python': ('https://docs.python.org/3/', None),
70
  }
71
 
72
 
@@ -75,7 +76,7 @@ intersphinx_mapping = {
75
  # The theme to use for HTML and HTML Help pages. See the documentation for
76
  # a list of builtin themes.
77
  #
78
- html_theme = 'sphinx_rtd_theme'
79
  html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
80
 
81
  # Theme options are theme-specific and customize the look and feel of a theme
@@ -87,7 +88,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
87
  # Add any paths that contain custom static files (such as style sheets) here,
88
  # relative to this directory. They are copied after the builtin static files,
89
  # so a file named "default.css" will overwrite the builtin "default.css".
90
- html_static_path = ['_static']
91
 
92
  # Custom sidebar templates, must be a dictionary that maps document names
93
  # to template names.
@@ -95,12 +96,12 @@ html_static_path = ['_static']
95
  # This is required for the alabaster theme
96
  # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
97
  html_sidebars = {
98
- '**': [
99
- 'about.html',
100
- 'navigation.html',
101
- 'relations.html', # needs 'show_related': True theme option to display
102
- 'searchbox.html',
103
- 'donate.html',
104
  ],
105
  }
106
 
@@ -108,7 +109,7 @@ html_sidebars = {
108
  # -- Options for HTMLHelp output ------------------------------------------
109
 
110
  # Output file base name for HTML help builder.
111
- htmlhelp_basename = 'pytube3doc'
112
 
113
 
114
  # -- Options for LaTeX output ---------------------------------------------
@@ -120,8 +121,11 @@ latex_elements = {}
120
  # author, documentclass [howto, manual, or own class]).
121
  latex_documents = [
122
  (
123
- master_doc, 'pytube3.tex', 'pytube3 Documentation',
124
- 'Nick Ficano', 'manual',
 
 
 
125
  ),
126
  ]
127
 
@@ -131,10 +135,7 @@ latex_documents = [
131
  # One entry per manual page. List of tuples
132
  # (source start file, name, description, authors, manual section).
133
  man_pages = [
134
- (
135
- master_doc, 'pytube3', 'pytube3 Documentation',
136
- [author], 1,
137
- ),
138
  ]
139
 
140
 
@@ -145,8 +146,12 @@ man_pages = [
145
  # dir menu entry, description, category)
146
  texinfo_documents = [
147
  (
148
- master_doc, 'pytube3', 'pytube3 Documentation',
149
- author, 'pytube3', 'One line description of project.',
150
- 'Miscellaneous',
 
 
 
 
151
  ),
152
  ]
 
5
  import sys
6
 
7
  import sphinx_rtd_theme
8
+
9
+ sys.path.insert(0, os.path.abspath("../"))
10
 
11
  from pytube import __version__ # noqa
12
 
13
  # -- General configuration ------------------------------------------------
14
 
15
  extensions = [
16
+ "sphinx.ext.autodoc",
17
+ "sphinx.ext.autosummary",
18
+ "sphinx.ext.todo",
19
+ "sphinx.ext.intersphinx",
20
+ "sphinx.ext.viewcode",
21
  ]
22
 
23
  autosummary_generate = True
24
 
25
  # Add any paths that contain templates here, relative to this directory.
26
+ templates_path = ["_templates"]
27
 
28
  # The suffix(es) of source filenames.
29
  # You can specify multiple suffix as a list of string:
30
  #
31
  # source_suffix = ['.rst', '.md']
32
+ source_suffix = ".rst"
33
 
34
  # The master toctree document.
35
+ master_doc = "index"
36
 
37
  # General information about the project.
38
+ project = "pytube3"
39
+ copyright = "2019, Nick Ficano"
40
+ author = "Nick Ficano, Harold Martin"
41
 
42
  # The version info for the project you're documenting, acts as replacement for
43
  # |version| and |release|, also used in various other places throughout the
 
58
  # List of patterns, relative to source directory, that match files and
59
  # directories to ignore when looking for source files.
60
  # This patterns also effect to html_static_path and html_extra_path
61
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
62
 
63
  # The name of the Pygments (syntax highlighting) style to use.
64
+ pygments_style = "sphinx"
65
 
66
  # If true, `todo` and `todoList` produce output, else they produce nothing.
67
  todo_include_todos = True
68
 
69
  intersphinx_mapping = {
70
+ "python": ("https://docs.python.org/3/", None),
71
  }
72
 
73
 
 
76
  # The theme to use for HTML and HTML Help pages. See the documentation for
77
  # a list of builtin themes.
78
  #
79
+ html_theme = "sphinx_rtd_theme"
80
  html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
81
 
82
  # Theme options are theme-specific and customize the look and feel of a theme
 
88
  # Add any paths that contain custom static files (such as style sheets) here,
89
  # relative to this directory. They are copied after the builtin static files,
90
  # so a file named "default.css" will overwrite the builtin "default.css".
91
+ html_static_path = ["_static"]
92
 
93
  # Custom sidebar templates, must be a dictionary that maps document names
94
  # to template names.
 
96
  # This is required for the alabaster theme
97
  # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
98
  html_sidebars = {
99
+ "**": [
100
+ "about.html",
101
+ "navigation.html",
102
+ "relations.html", # needs 'show_related': True theme option to display
103
+ "searchbox.html",
104
+ "donate.html",
105
  ],
106
  }
107
 
 
109
  # -- Options for HTMLHelp output ------------------------------------------
110
 
111
  # Output file base name for HTML help builder.
112
+ htmlhelp_basename = "pytube3doc"
113
 
114
 
115
  # -- Options for LaTeX output ---------------------------------------------
 
121
  # author, documentclass [howto, manual, or own class]).
122
  latex_documents = [
123
  (
124
+ master_doc,
125
+ "pytube3.tex",
126
+ "pytube3 Documentation",
127
+ "Nick Ficano",
128
+ "manual",
129
  ),
130
  ]
131
 
 
135
  # One entry per manual page. List of tuples
136
  # (source start file, name, description, authors, manual section).
137
  man_pages = [
138
+ (master_doc, "pytube3", "pytube3 Documentation", [author], 1,),
 
 
 
139
  ]
140
 
141
 
 
146
  # dir menu entry, description, category)
147
  texinfo_documents = [
148
  (
149
+ master_doc,
150
+ "pytube3",
151
+ "pytube3 Documentation",
152
+ author,
153
+ "pytube3",
154
+ "One line description of project.",
155
+ "Miscellaneous",
156
  ),
157
  ]
pytube/__main__.py CHANGED
@@ -7,12 +7,13 @@ exclusively on the developer interface. Pytube offloads the heavy lifting to
7
  smaller peripheral modules and functions.
8
 
9
  """
10
-
11
  import json
12
  import logging
13
- from typing import Optional, Dict, List
14
- from urllib.parse import parse_qsl
15
  from html import unescape
 
 
 
 
16
 
17
  from pytube import Caption
18
  from pytube import CaptionQuery
@@ -20,10 +21,14 @@ 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, get_ytplayer_config
24
- from pytube.helpers import install_proxy
25
  from pytube.exceptions import VideoUnavailable
26
- from pytube.monostate import OnProgress, OnComplete, Monostate
 
 
 
 
 
 
27
 
28
  logger = logging.getLogger(__name__)
29
 
@@ -54,17 +59,23 @@ class YouTube:
54
 
55
  """
56
  self.js: Optional[str] = None # js fetched by js_url
57
- self.js_url: Optional[str] = None # the url to the js, parsed from watch html
 
 
58
 
59
  # note: vid_info may eventually be removed. It sounds like it once had
60
  # additional formats, but that doesn't appear to still be the case.
61
 
62
  # the url to vid info, parsed from watch html
63
  self.vid_info_url: Optional[str] = None
64
- self.vid_info_raw: Optional[str] = None # content fetched by vid_info_url
 
 
65
  self.vid_info: Optional[Dict] = None # parsed content of vid_info_raw
66
 
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 = {}
@@ -109,11 +120,15 @@ class YouTube:
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
116
- i_start = self.watch_html.lower().index("<title>") + len("<title>")
 
 
117
  i_end = self.watch_html.lower().index("</title>")
118
  title = self.watch_html[i_start:i_end].strip()
119
  index = title.lower().rfind(" - youtube")
@@ -143,7 +158,9 @@ class YouTube:
143
  self.initialize_stream_objects(fmt)
144
 
145
  # load the player_response object (contains subtitle information)
146
- self.player_response = json.loads(self.player_config_args["player_response"])
 
 
147
  del self.player_config_args["player_response"]
148
  self.stream_monostate.title = self.title
149
  self.stream_monostate.duration = self.length
@@ -164,7 +181,10 @@ class YouTube:
164
  raise VideoUnavailable(video_id=self.video_id)
165
  self.age_restricted = extract.is_age_restricted(self.watch_html)
166
 
167
- if not self.age_restricted and "This video is private" in self.watch_html:
 
 
 
168
  raise VideoUnavailable(video_id=self.video_id)
169
 
170
  if self.age_restricted:
@@ -282,7 +302,9 @@ class YouTube:
282
  :rtype: float
283
 
284
  """
285
- return self.player_response.get("videoDetails", {}).get("averageRating")
 
 
286
 
287
  @property
288
  def length(self) -> int:
@@ -293,7 +315,11 @@ class YouTube:
293
  """
294
  return int(
295
  self.player_config_args.get("length_seconds")
296
- or (self.player_response.get("videoDetails", {}).get("lengthSeconds"))
 
 
 
 
297
  )
298
 
299
  @property
@@ -303,14 +329,18 @@ class YouTube:
303
  :rtype: str
304
 
305
  """
306
- return int(self.player_response.get("videoDetails", {}).get("viewCount"))
 
 
307
 
308
  @property
309
  def author(self) -> str:
310
  """Get the video author.
311
  :rtype: str
312
  """
313
- return self.player_response.get("videoDetails", {}).get("author", "unknown")
 
 
314
 
315
  def register_on_progress_callback(self, func: OnProgress):
316
  """Register a download progress callback function post initialization.
 
7
  smaller peripheral modules and functions.
8
 
9
  """
 
10
  import json
11
  import logging
 
 
12
  from html import unescape
13
+ from typing import Dict
14
+ from typing import List
15
+ from typing import Optional
16
+ from urllib.parse import parse_qsl
17
 
18
  from pytube import Caption
19
  from pytube import CaptionQuery
 
21
  from pytube import request
22
  from pytube import Stream
23
  from pytube import StreamQuery
 
 
24
  from pytube.exceptions import VideoUnavailable
25
+ from pytube.extract import apply_descrambler
26
+ from pytube.extract import apply_signature
27
+ from pytube.extract import get_ytplayer_config
28
+ from pytube.helpers import install_proxy
29
+ from pytube.monostate import Monostate
30
+ from pytube.monostate import OnComplete
31
+ from pytube.monostate import OnProgress
32
 
33
  logger = logging.getLogger(__name__)
34
 
 
59
 
60
  """
61
  self.js: Optional[str] = None # js fetched by js_url
62
+ self.js_url: Optional[
63
+ str
64
+ ] = None # the url to the js, parsed from watch html
65
 
66
  # note: vid_info may eventually be removed. It sounds like it once had
67
  # additional formats, but that doesn't appear to still be the case.
68
 
69
  # the url to vid info, parsed from watch html
70
  self.vid_info_url: Optional[str] = None
71
+ self.vid_info_raw: Optional[
72
+ str
73
+ ] = None # content fetched by vid_info_url
74
  self.vid_info: Optional[Dict] = None # parsed content of vid_info_raw
75
 
76
+ self.watch_html: Optional[
77
+ str
78
+ ] = None # the html of /watch?v=<video_id>
79
  self.embed_html: Optional[str] = None
80
  self.player_config_args: Dict = {} # inline js in the html containing
81
  self.player_response: Dict = {}
 
120
  self.player_config_args = self.vid_info
121
  else:
122
  assert self.watch_html is not None
123
+ self.player_config_args = get_ytplayer_config(self.watch_html)[
124
+ "args"
125
+ ]
126
 
127
  # Fix for KeyError: 'title' issue #434
128
  if "title" not in self.player_config_args: # type: ignore
129
+ i_start = self.watch_html.lower().index("<title>") + len(
130
+ "<title>"
131
+ )
132
  i_end = self.watch_html.lower().index("</title>")
133
  title = self.watch_html[i_start:i_end].strip()
134
  index = title.lower().rfind(" - youtube")
 
158
  self.initialize_stream_objects(fmt)
159
 
160
  # load the player_response object (contains subtitle information)
161
+ self.player_response = json.loads(
162
+ self.player_config_args["player_response"]
163
+ )
164
  del self.player_config_args["player_response"]
165
  self.stream_monostate.title = self.title
166
  self.stream_monostate.duration = self.length
 
181
  raise VideoUnavailable(video_id=self.video_id)
182
  self.age_restricted = extract.is_age_restricted(self.watch_html)
183
 
184
+ if (
185
+ not self.age_restricted
186
+ and "This video is private" in self.watch_html
187
+ ):
188
  raise VideoUnavailable(video_id=self.video_id)
189
 
190
  if self.age_restricted:
 
302
  :rtype: float
303
 
304
  """
305
+ return self.player_response.get("videoDetails", {}).get(
306
+ "averageRating"
307
+ )
308
 
309
  @property
310
  def length(self) -> int:
 
315
  """
316
  return int(
317
  self.player_config_args.get("length_seconds")
318
+ or (
319
+ self.player_response.get("videoDetails", {}).get(
320
+ "lengthSeconds"
321
+ )
322
+ )
323
  )
324
 
325
  @property
 
329
  :rtype: str
330
 
331
  """
332
+ return int(
333
+ self.player_response.get("videoDetails", {}).get("viewCount")
334
+ )
335
 
336
  @property
337
  def author(self) -> str:
338
  """Get the video author.
339
  :rtype: str
340
  """
341
+ return self.player_response.get("videoDetails", {}).get(
342
+ "author", "unknown"
343
+ )
344
 
345
  def register_on_progress_callback(self, func: OnProgress):
346
  """Register a download progress callback function post initialization.
pytube/captions.py CHANGED
@@ -3,10 +3,13 @@ import math
3
  import os
4
  import time
5
  import xml.etree.ElementTree as ElementTree
6
- from typing import Dict, Optional
7
- from pytube import request
8
  from html import unescape
9
- from pytube.helpers import safe_filename, target_directory
 
 
 
 
 
10
 
11
 
12
  class Caption:
 
3
  import os
4
  import time
5
  import xml.etree.ElementTree as ElementTree
 
 
6
  from html import unescape
7
+ from typing import Dict
8
+ from typing import Optional
9
+
10
+ from pytube import request
11
+ from pytube.helpers import safe_filename
12
+ from pytube.helpers import target_directory
13
 
14
 
15
  class Caption:
pytube/cipher.py CHANGED
@@ -1,5 +1,4 @@
1
  # -*- coding: utf-8 -*-
2
-
3
  """
4
  This module contains all logic necessary to decipher the signature.
5
 
@@ -17,10 +16,16 @@ signature and decoding it.
17
  import logging
18
  import re
19
  from itertools import chain
20
- from typing import List, Tuple, Dict, Callable, Any, Optional
 
 
 
 
 
21
 
22
  from pytube.exceptions import RegexMatchError
23
- from pytube.helpers import regex_search, cache
 
24
 
25
  logger = logging.getLogger(__name__)
26
 
@@ -84,7 +89,9 @@ class Cipher:
84
  logger.debug("parsing transform function")
85
  parse_match = self.js_func_regex.search(js_func)
86
  if not parse_match:
87
- raise RegexMatchError(caller="parse_function", pattern="js_func_regex")
 
 
88
  fn_name, fn_arg = parse_match.groups()
89
  return fn_name, int(fn_arg)
90
 
@@ -120,7 +127,9 @@ def get_initial_function_name(js: str) -> str:
120
  logger.debug("finished regex search, matched: %s", pattern)
121
  return function_match.group(1)
122
 
123
- raise RegexMatchError(caller="get_initial_function_name", pattern="multiple")
 
 
124
 
125
 
126
  def get_transform_plan(js: str) -> List[str]:
 
1
  # -*- coding: utf-8 -*-
 
2
  """
3
  This module contains all logic necessary to decipher the signature.
4
 
 
16
  import logging
17
  import re
18
  from itertools import chain
19
+ from typing import Any
20
+ from typing import Callable
21
+ from typing import Dict
22
+ from typing import List
23
+ from typing import Optional
24
+ from typing import Tuple
25
 
26
  from pytube.exceptions import RegexMatchError
27
+ from pytube.helpers import cache
28
+ from pytube.helpers import regex_search
29
 
30
  logger = logging.getLogger(__name__)
31
 
 
89
  logger.debug("parsing transform function")
90
  parse_match = self.js_func_regex.search(js_func)
91
  if not parse_match:
92
+ raise RegexMatchError(
93
+ caller="parse_function", pattern="js_func_regex"
94
+ )
95
  fn_name, fn_arg = parse_match.groups()
96
  return fn_name, int(fn_arg)
97
 
 
127
  logger.debug("finished regex search, matched: %s", pattern)
128
  return function_match.group(1)
129
 
130
+ raise RegexMatchError(
131
+ caller="get_initial_function_name", pattern="multiple"
132
+ )
133
 
134
 
135
  def get_transform_plan(js: str) -> List[str]:
pytube/cli.py CHANGED
@@ -1,7 +1,6 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
  """A simple command line application to download youtube videos."""
4
-
5
  import argparse
6
  import datetime as dt
7
  import gzip
@@ -9,14 +8,19 @@ import json
9
  import logging
10
  import os
11
  import shutil
12
- import sys
13
  import subprocess # nosec
14
- from typing import List, Optional
 
 
15
 
16
- from pytube import __version__, CaptionQuery, Stream, Playlist
 
 
 
17
  from pytube import YouTube
18
  from pytube.exceptions import PytubeError
19
- from pytube.helpers import safe_filename, setup_logger
 
20
 
21
 
22
  def main():
@@ -49,7 +53,9 @@ def main():
49
  _perform_args_on_youtube(youtube, args)
50
 
51
 
52
- def _perform_args_on_youtube(youtube: YouTube, args: argparse.Namespace) -> None:
 
 
53
  if args.list:
54
  display_streams(youtube)
55
  if args.build_playback_report:
@@ -65,15 +71,21 @@ def _perform_args_on_youtube(youtube: YouTube, args: argparse.Namespace) -> None
65
  youtube=youtube, resolution=args.resolution, target=args.target
66
  )
67
  if args.audio:
68
- download_audio(youtube=youtube, filetype=args.audio, target=args.target)
 
 
69
  if args.ffmpeg:
70
- ffmpeg_process(youtube=youtube, resolution=args.ffmpeg, target=args.target)
 
 
71
 
72
 
73
  def _parse_args(
74
  parser: argparse.ArgumentParser, args: Optional[List] = None
75
  ) -> argparse.Namespace:
76
- parser.add_argument("url", help="The YouTube /watch or /playlist url", nargs="?")
 
 
77
  parser.add_argument(
78
  "--version", action="version", version="%(prog)s " + __version__,
79
  )
@@ -81,7 +93,10 @@ def _parse_args(
81
  "--itag", type=int, help="The itag for the desired stream",
82
  )
83
  parser.add_argument(
84
- "-r", "--resolution", type=str, help="The resolution for the desired stream",
 
 
 
85
  )
86
  parser.add_argument(
87
  "-l",
@@ -218,7 +233,9 @@ def on_progress(
218
 
219
 
220
  def _download(
221
- stream: Stream, target: Optional[str] = None, filename: Optional[str] = None
 
 
222
  ) -> None:
223
  filesize_megabytes = stream.filesize // 1048576
224
  print(f"{filename or stream.default_filename} | {filesize_megabytes} MB")
@@ -271,7 +288,9 @@ def ffmpeg_process(
271
 
272
  if resolution == "best":
273
  highest_quality_stream = (
274
- youtube.streams.filter(progressive=False).order_by("resolution").last()
 
 
275
  )
276
  mp4_stream = (
277
  youtube.streams.filter(progressive=False, subtype="mp4")
@@ -298,7 +317,9 @@ def ffmpeg_process(
298
 
299
  audio_stream = youtube.streams.get_audio_only(video_stream.subtype)
300
  if not audio_stream:
301
- audio_stream = youtube.streams.filter(only_audio=True).order_by("abr").last()
 
 
302
  if not audio_stream:
303
  print("Could not find an audio only stream")
304
  sys.exit()
@@ -307,7 +328,9 @@ def ffmpeg_process(
307
  )
308
 
309
 
310
- def _ffmpeg_downloader(audio_stream: Stream, video_stream: Stream, target: str) -> None:
 
 
311
  """
312
  Given a YouTube Stream object, finds the correct audio stream, downloads them both
313
  giving them a unique name, them uses ffmpeg to create a new file with the audio
@@ -322,29 +345,50 @@ def _ffmpeg_downloader(audio_stream: Stream, video_stream: Stream, target: str)
322
  A valid Path object
323
  """
324
  video_unique_name = _unique_name(
325
- safe_filename(video_stream.title), video_stream.subtype, "video", target=target
 
 
 
326
  )
327
  audio_unique_name = _unique_name(
328
- safe_filename(video_stream.title), audio_stream.subtype, "audio", target=target
 
 
 
329
  )
330
  _download(stream=video_stream, target=target, filename=video_unique_name)
331
  print("Loading audio...")
332
  _download(stream=audio_stream, target=target, filename=audio_unique_name)
333
 
334
- video_path = os.path.join(target, f"{video_unique_name}.{video_stream.subtype}")
335
- audio_path = os.path.join(target, f"{audio_unique_name}.{audio_stream.subtype}")
 
 
 
 
336
  final_path = os.path.join(
337
  target, f"{safe_filename(video_stream.title)}.{video_stream.subtype}"
338
  )
339
 
340
  subprocess.run( # nosec
341
- ["ffmpeg", "-i", video_path, "-i", audio_path, "-codec", "copy", final_path,]
 
 
 
 
 
 
 
 
 
342
  )
343
  os.unlink(video_path)
344
  os.unlink(audio_path)
345
 
346
 
347
- def download_by_itag(youtube: YouTube, itag: int, target: Optional[str] = None) -> None:
 
 
348
  """Start downloading a YouTube video.
349
 
350
  :param YouTube youtube:
@@ -409,7 +453,9 @@ def display_streams(youtube: YouTube) -> None:
409
 
410
 
411
  def _print_available_captions(captions: CaptionQuery) -> None:
412
- print(f"Available caption codes are: {', '.join(c.code for c in captions)}")
 
 
413
 
414
 
415
  def download_caption(
@@ -432,7 +478,9 @@ def download_caption(
432
 
433
  try:
434
  caption = youtube.captions[lang_code]
435
- downloaded_path = caption.download(title=youtube.title, output_path=target)
 
 
436
  print(f"Saved caption file to: {downloaded_path}")
437
  except KeyError:
438
  print(f"Unable to find caption with code: {lang_code}")
@@ -454,7 +502,9 @@ def download_audio(
454
  Target directory for download
455
  """
456
  audio = (
457
- youtube.streams.filter(only_audio=True, subtype=filetype).order_by("abr").last()
 
 
458
  )
459
 
460
  if audio is None:
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
  """A simple command line application to download youtube videos."""
 
4
  import argparse
5
  import datetime as dt
6
  import gzip
 
8
  import logging
9
  import os
10
  import shutil
 
11
  import subprocess # nosec
12
+ import sys
13
+ from typing import List
14
+ from typing import Optional
15
 
16
+ from pytube import __version__
17
+ from pytube import CaptionQuery
18
+ from pytube import Playlist
19
+ from pytube import Stream
20
  from pytube import YouTube
21
  from pytube.exceptions import PytubeError
22
+ from pytube.helpers import safe_filename
23
+ from pytube.helpers import setup_logger
24
 
25
 
26
  def main():
 
53
  _perform_args_on_youtube(youtube, args)
54
 
55
 
56
+ def _perform_args_on_youtube(
57
+ youtube: YouTube, args: argparse.Namespace
58
+ ) -> None:
59
  if args.list:
60
  display_streams(youtube)
61
  if args.build_playback_report:
 
71
  youtube=youtube, resolution=args.resolution, target=args.target
72
  )
73
  if args.audio:
74
+ download_audio(
75
+ youtube=youtube, filetype=args.audio, target=args.target
76
+ )
77
  if args.ffmpeg:
78
+ ffmpeg_process(
79
+ youtube=youtube, resolution=args.ffmpeg, target=args.target
80
+ )
81
 
82
 
83
  def _parse_args(
84
  parser: argparse.ArgumentParser, args: Optional[List] = None
85
  ) -> argparse.Namespace:
86
+ parser.add_argument(
87
+ "url", help="The YouTube /watch or /playlist url", nargs="?"
88
+ )
89
  parser.add_argument(
90
  "--version", action="version", version="%(prog)s " + __version__,
91
  )
 
93
  "--itag", type=int, help="The itag for the desired stream",
94
  )
95
  parser.add_argument(
96
+ "-r",
97
+ "--resolution",
98
+ type=str,
99
+ help="The resolution for the desired stream",
100
  )
101
  parser.add_argument(
102
  "-l",
 
233
 
234
 
235
  def _download(
236
+ stream: Stream,
237
+ target: Optional[str] = None,
238
+ filename: Optional[str] = None,
239
  ) -> None:
240
  filesize_megabytes = stream.filesize // 1048576
241
  print(f"{filename or stream.default_filename} | {filesize_megabytes} MB")
 
288
 
289
  if resolution == "best":
290
  highest_quality_stream = (
291
+ youtube.streams.filter(progressive=False)
292
+ .order_by("resolution")
293
+ .last()
294
  )
295
  mp4_stream = (
296
  youtube.streams.filter(progressive=False, subtype="mp4")
 
317
 
318
  audio_stream = youtube.streams.get_audio_only(video_stream.subtype)
319
  if not audio_stream:
320
+ audio_stream = (
321
+ youtube.streams.filter(only_audio=True).order_by("abr").last()
322
+ )
323
  if not audio_stream:
324
  print("Could not find an audio only stream")
325
  sys.exit()
 
328
  )
329
 
330
 
331
+ def _ffmpeg_downloader(
332
+ audio_stream: Stream, video_stream: Stream, target: str
333
+ ) -> None:
334
  """
335
  Given a YouTube Stream object, finds the correct audio stream, downloads them both
336
  giving them a unique name, them uses ffmpeg to create a new file with the audio
 
345
  A valid Path object
346
  """
347
  video_unique_name = _unique_name(
348
+ safe_filename(video_stream.title),
349
+ video_stream.subtype,
350
+ "video",
351
+ target=target,
352
  )
353
  audio_unique_name = _unique_name(
354
+ safe_filename(video_stream.title),
355
+ audio_stream.subtype,
356
+ "audio",
357
+ target=target,
358
  )
359
  _download(stream=video_stream, target=target, filename=video_unique_name)
360
  print("Loading audio...")
361
  _download(stream=audio_stream, target=target, filename=audio_unique_name)
362
 
363
+ video_path = os.path.join(
364
+ target, f"{video_unique_name}.{video_stream.subtype}"
365
+ )
366
+ audio_path = os.path.join(
367
+ target, f"{audio_unique_name}.{audio_stream.subtype}"
368
+ )
369
  final_path = os.path.join(
370
  target, f"{safe_filename(video_stream.title)}.{video_stream.subtype}"
371
  )
372
 
373
  subprocess.run( # nosec
374
+ [
375
+ "ffmpeg",
376
+ "-i",
377
+ video_path,
378
+ "-i",
379
+ audio_path,
380
+ "-codec",
381
+ "copy",
382
+ final_path,
383
+ ]
384
  )
385
  os.unlink(video_path)
386
  os.unlink(audio_path)
387
 
388
 
389
+ def download_by_itag(
390
+ youtube: YouTube, itag: int, target: Optional[str] = None
391
+ ) -> None:
392
  """Start downloading a YouTube video.
393
 
394
  :param YouTube youtube:
 
453
 
454
 
455
  def _print_available_captions(captions: CaptionQuery) -> None:
456
+ print(
457
+ f"Available caption codes are: {', '.join(c.code for c in captions)}"
458
+ )
459
 
460
 
461
  def download_caption(
 
478
 
479
  try:
480
  caption = youtube.captions[lang_code]
481
+ downloaded_path = caption.download(
482
+ title=youtube.title, output_path=target
483
+ )
484
  print(f"Saved caption file to: {downloaded_path}")
485
  except KeyError:
486
  print(f"Unable to find caption with code: {lang_code}")
 
502
  Target directory for download
503
  """
504
  audio = (
505
+ youtube.streams.filter(only_audio=True, subtype=filetype)
506
+ .order_by("abr")
507
+ .last()
508
  )
509
 
510
  if audio is None:
pytube/contrib/playlist.py CHANGED
@@ -1,17 +1,24 @@
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
8
- from datetime import date, datetime
9
- from typing import List, Optional, Iterable, Dict, Union
10
- from urllib.parse import parse_qs
11
  from collections.abc import Sequence
 
 
 
 
 
 
 
 
12
 
13
- from pytube import request, YouTube
14
- from pytube.helpers import cache, deprecated, install_proxy, uniqueify
 
 
 
 
15
 
16
  logger = logging.getLogger(__name__)
17
 
@@ -28,7 +35,9 @@ class Playlist(Sequence):
28
  except IndexError: # assume that url is just the id
29
  self.playlist_id = url
30
 
31
- self.playlist_url = f"https://www.youtube.com/playlist?list={self.playlist_id}"
 
 
32
  self.html = request.get(self.playlist_url)
33
 
34
  # Needs testing with non-English
@@ -48,7 +57,8 @@ class Playlist(Sequence):
48
  def _find_load_more_url(req: str) -> Optional[str]:
49
  """Given an html page or fragment, returns the "load more" url if found."""
50
  match = re.search(
51
- r"data-uix-load-more-href=\"(/browse_ajax\?" 'action_continuation=.*?)"',
 
52
  req,
53
  )
54
  if match:
@@ -56,7 +66,9 @@ class Playlist(Sequence):
56
 
57
  return None
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
  """ Deprecated function for returning list of URLs
62
 
@@ -64,7 +76,9 @@ class Playlist(Sequence):
64
  """
65
  return self.video_urls
66
 
67
- def _paginate(self, until_watch_id: Optional[str] = None) -> Iterable[List[str]]:
 
 
68
  """Parse the video links from the page source, yields the /watch?v= part from video link
69
  """
70
  req = self.html
@@ -94,7 +108,9 @@ class Playlist(Sequence):
94
  videos_urls = self._extract_videos(html)
95
  if until_watch_id:
96
  try:
97
- trim_index = videos_urls.index(f"/watch?v={until_watch_id}")
 
 
98
  yield videos_urls[:trim_index]
99
  return
100
  except ValueError:
@@ -132,7 +148,9 @@ class Playlist(Sequence):
132
  :returns: List of video URLs
133
  """
134
  return [
135
- self._video_url(video) for page in list(self._paginate()) for video in page
 
 
136
  ]
137
 
138
  @property
 
1
  # -*- coding: utf-8 -*-
 
2
  """Module to download a complete playlist from a youtube channel."""
 
3
  import json
4
  import logging
5
  import re
 
 
 
6
  from collections.abc import Sequence
7
+ from datetime import date
8
+ from datetime import datetime
9
+ from typing import Dict
10
+ from typing import Iterable
11
+ from typing import List
12
+ from typing import Optional
13
+ from typing import Union
14
+ from urllib.parse import parse_qs
15
 
16
+ from pytube import request
17
+ from pytube import YouTube
18
+ from pytube.helpers import cache
19
+ from pytube.helpers import deprecated
20
+ from pytube.helpers import install_proxy
21
+ from pytube.helpers import uniqueify
22
 
23
  logger = logging.getLogger(__name__)
24
 
 
35
  except IndexError: # assume that url is just the id
36
  self.playlist_id = url
37
 
38
+ self.playlist_url = (
39
+ f"https://www.youtube.com/playlist?list={self.playlist_id}"
40
+ )
41
  self.html = request.get(self.playlist_url)
42
 
43
  # Needs testing with non-English
 
57
  def _find_load_more_url(req: str) -> Optional[str]:
58
  """Given an html page or fragment, returns the "load more" url if found."""
59
  match = re.search(
60
+ r"data-uix-load-more-href=\"(/browse_ajax\?"
61
+ 'action_continuation=.*?)"',
62
  req,
63
  )
64
  if match:
 
66
 
67
  return None
68
 
69
+ @deprecated(
70
+ "This function will be removed in the future, please use .video_urls"
71
+ )
72
  def parse_links(self) -> List[str]: # pragma: no cover
73
  """ Deprecated function for returning list of URLs
74
 
 
76
  """
77
  return self.video_urls
78
 
79
+ def _paginate(
80
+ self, until_watch_id: Optional[str] = None
81
+ ) -> Iterable[List[str]]:
82
  """Parse the video links from the page source, yields the /watch?v= part from video link
83
  """
84
  req = self.html
 
108
  videos_urls = self._extract_videos(html)
109
  if until_watch_id:
110
  try:
111
+ trim_index = videos_urls.index(
112
+ f"/watch?v={until_watch_id}"
113
+ )
114
  yield videos_urls[:trim_index]
115
  return
116
  except ValueError:
 
148
  :returns: List of video URLs
149
  """
150
  return [
151
+ self._video_url(video)
152
+ for page in list(self._paginate())
153
+ for video in page
154
  ]
155
 
156
  @property
pytube/exceptions.py CHANGED
@@ -1,7 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
-
3
  """Library specific exception definitions."""
4
- from typing import Union, Pattern
 
5
 
6
 
7
  class PytubeError(Exception):
 
1
  # -*- coding: utf-8 -*-
 
2
  """Library specific exception definitions."""
3
+ from typing import Pattern
4
+ from typing import Union
5
 
6
 
7
  class PytubeError(Exception):
pytube/extract.py CHANGED
@@ -5,12 +5,21 @@ import logging
5
  import re
6
  from collections import OrderedDict
7
  from html.parser import HTMLParser
8
- from typing import Any, Optional, Tuple, List, Dict
9
- from urllib.parse import quote, parse_qs, unquote, parse_qsl
 
 
 
 
 
 
 
10
  from urllib.parse import urlencode
11
 
12
  from pytube.cipher import Cipher
13
- from pytube.exceptions import RegexMatchError, HTMLParseError, LiveStreamError
 
 
14
  from pytube.helpers import regex_search
15
 
16
  logger = logging.getLogger(__name__)
@@ -123,7 +132,9 @@ def video_info_url_age_restricted(video_id: str, embed_html: str) -> str:
123
  # Here we use ``OrderedDict`` so that the output is consistent between
124
  # Python 2.7+.
125
  eurl = f"https://youtube.googleapis.com/v/{video_id}"
126
- params = OrderedDict([("video_id", video_id), ("eurl", eurl), ("sts", sts),])
 
 
127
  return _video_info_url(params)
128
 
129
 
@@ -199,7 +210,9 @@ def get_ytplayer_config(html: str) -> Any:
199
  yt_player_config = function_match.group(1)
200
  return json.loads(yt_player_config)
201
 
202
- raise RegexMatchError(caller="get_ytplayer_config", pattern="config_patterns")
 
 
203
 
204
 
205
  def _get_vid_descr(html: Optional[str]) -> str:
@@ -248,7 +261,9 @@ def apply_signature(config_args: Dict, fmt: str, js: str) -> None:
248
 
249
  signature = cipher.get_signature(ciphered_signature=stream["s"])
250
 
251
- logger.debug("finished descrambling signature for itag=%s", stream["itag"])
 
 
252
  # 403 forbidden fix
253
  stream_manifest[i]["url"] = url + "&sig=" + signature
254
 
@@ -278,7 +293,9 @@ def apply_descrambler(stream_data: Dict, key: str) -> None:
278
  if key == "url_encoded_fmt_stream_map" and not stream_data.get(
279
  "url_encoded_fmt_stream_map"
280
  ):
281
- formats = json.loads(stream_data["player_response"])["streamingData"]["formats"]
 
 
282
  formats.extend(
283
  json.loads(stream_data["player_response"])["streamingData"][
284
  "adaptiveFormats"
@@ -298,7 +315,8 @@ def apply_descrambler(stream_data: Dict, key: str) -> None:
298
  ]
299
  except KeyError:
300
  cipher_url = [
301
- parse_qs(formats[i]["cipher"]) for i, data in enumerate(formats)
 
302
  ]
303
  stream_data[key] = [
304
  {
 
5
  import re
6
  from collections import OrderedDict
7
  from html.parser import HTMLParser
8
+ from typing import Any
9
+ from typing import Dict
10
+ from typing import List
11
+ from typing import Optional
12
+ from typing import Tuple
13
+ from urllib.parse import parse_qs
14
+ from urllib.parse import parse_qsl
15
+ from urllib.parse import quote
16
+ from urllib.parse import unquote
17
  from urllib.parse import urlencode
18
 
19
  from pytube.cipher import Cipher
20
+ from pytube.exceptions import HTMLParseError
21
+ from pytube.exceptions import LiveStreamError
22
+ from pytube.exceptions import RegexMatchError
23
  from pytube.helpers import regex_search
24
 
25
  logger = logging.getLogger(__name__)
 
132
  # Here we use ``OrderedDict`` so that the output is consistent between
133
  # Python 2.7+.
134
  eurl = f"https://youtube.googleapis.com/v/{video_id}"
135
+ params = OrderedDict(
136
+ [("video_id", video_id), ("eurl", eurl), ("sts", sts),]
137
+ )
138
  return _video_info_url(params)
139
 
140
 
 
210
  yt_player_config = function_match.group(1)
211
  return json.loads(yt_player_config)
212
 
213
+ raise RegexMatchError(
214
+ caller="get_ytplayer_config", pattern="config_patterns"
215
+ )
216
 
217
 
218
  def _get_vid_descr(html: Optional[str]) -> str:
 
261
 
262
  signature = cipher.get_signature(ciphered_signature=stream["s"])
263
 
264
+ logger.debug(
265
+ "finished descrambling signature for itag=%s", stream["itag"]
266
+ )
267
  # 403 forbidden fix
268
  stream_manifest[i]["url"] = url + "&sig=" + signature
269
 
 
293
  if key == "url_encoded_fmt_stream_map" and not stream_data.get(
294
  "url_encoded_fmt_stream_map"
295
  ):
296
+ formats = json.loads(stream_data["player_response"])["streamingData"][
297
+ "formats"
298
+ ]
299
  formats.extend(
300
  json.loads(stream_data["player_response"])["streamingData"][
301
  "adaptiveFormats"
 
315
  ]
316
  except KeyError:
317
  cipher_url = [
318
+ parse_qs(formats[i]["cipher"])
319
+ for i, data in enumerate(formats)
320
  ]
321
  stream_data[key] = [
322
  {
pytube/helpers.py CHANGED
@@ -1,12 +1,16 @@
1
  # -*- coding: utf-8 -*-
2
-
3
  """Various helper functions implemented by pytube."""
4
  import functools
5
  import logging
6
  import os
7
  import re
8
  import warnings
9
- from typing import TypeVar, Callable, Optional, Dict, List, Any
 
 
 
 
 
10
  from urllib import request
11
 
12
  from pytube.exceptions import RegexMatchError
 
1
  # -*- coding: utf-8 -*-
 
2
  """Various helper functions implemented by pytube."""
3
  import functools
4
  import logging
5
  import os
6
  import re
7
  import warnings
8
+ from typing import Any
9
+ from typing import Callable
10
+ from typing import Dict
11
+ from typing import List
12
+ from typing import Optional
13
+ from typing import TypeVar
14
  from urllib import request
15
 
16
  from pytube.exceptions import RegexMatchError
pytube/monostate.py CHANGED
@@ -1,11 +1,14 @@
1
  # -*- coding: utf-8 -*-
 
 
2
 
3
- from typing import Any, Optional
4
  from typing_extensions import Protocol
5
 
6
 
7
  class OnProgress(Protocol):
8
- def __call__(self, stream: Any, chunk: bytes, bytes_remaining: int) -> None:
 
 
9
  """On download progress callback function.
10
 
11
  :param stream:
 
1
  # -*- coding: utf-8 -*-
2
+ from typing import Any
3
+ from typing import Optional
4
 
 
5
  from typing_extensions import Protocol
6
 
7
 
8
  class OnProgress(Protocol):
9
+ def __call__(
10
+ self, stream: Any, chunk: bytes, bytes_remaining: int
11
+ ) -> None:
12
  """On download progress callback function.
13
 
14
  :param stream:
pytube/query.py CHANGED
@@ -1,10 +1,14 @@
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 Mapping, Sequence
6
-
7
- from pytube import Stream, Caption
 
 
 
 
 
8
  from pytube.helpers import deprecated
9
 
10
 
@@ -150,12 +154,16 @@ class StreamQuery(Sequence):
150
 
151
  if only_audio:
152
  filters.append(
153
- lambda s: (s.includes_audio_track and not s.includes_video_track),
 
 
154
  )
155
 
156
  if only_video:
157
  filters.append(
158
- lambda s: (s.includes_video_track and not s.includes_audio_track),
 
 
159
  )
160
 
161
  if progressive:
@@ -185,10 +193,14 @@ class StreamQuery(Sequence):
185
  The name of the attribute to sort by.
186
  """
187
  has_attribute = [
188
- s for s in self.fmt_streams if getattr(s, attribute_name) is not None
 
 
189
  ]
190
  # Check that the attributes have string values.
191
- if has_attribute and isinstance(getattr(has_attribute[0], attribute_name), str):
 
 
192
  # Try to return a StreamQuery sorted by the integer representations
193
  # of the values.
194
  try:
@@ -196,7 +208,9 @@ class StreamQuery(Sequence):
196
  sorted(
197
  has_attribute,
198
  key=lambda s: int(
199
- "".join(filter(str.isdigit, getattr(s, attribute_name)))
 
 
200
  ), # type: ignore # noqa: E501
201
  )
202
  )
@@ -263,7 +277,9 @@ class StreamQuery(Sequence):
263
 
264
  """
265
  return (
266
- self.filter(progressive=True, subtype="mp4").order_by("resolution").first()
 
 
267
  )
268
 
269
  def get_highest_resolution(self) -> Optional[Stream]:
@@ -287,7 +303,11 @@ class StreamQuery(Sequence):
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
@@ -368,7 +388,9 @@ class CaptionQuery(Mapping):
368
  """
369
  self.lang_code_index = {c.code: c for c in captions}
370
 
371
- @deprecated("This object can be treated as a dictionary, i.e. captions['en']")
 
 
372
  def get_by_language_code(
373
  self, lang_code: str
374
  ) -> Optional[Caption]: # pragma: no cover
 
1
  # -*- coding: utf-8 -*-
 
2
  """This module provides a query interface for media streams and captions."""
3
+ from collections.abc import Mapping
4
+ from collections.abc import Sequence
5
+ from typing import Callable
6
+ from typing import List
7
+ from typing import Optional
8
+ from typing import Union
9
+
10
+ from pytube import Caption
11
+ from pytube import Stream
12
  from pytube.helpers import deprecated
13
 
14
 
 
154
 
155
  if only_audio:
156
  filters.append(
157
+ lambda s: (
158
+ s.includes_audio_track and not s.includes_video_track
159
+ ),
160
  )
161
 
162
  if only_video:
163
  filters.append(
164
+ lambda s: (
165
+ s.includes_video_track and not s.includes_audio_track
166
+ ),
167
  )
168
 
169
  if progressive:
 
193
  The name of the attribute to sort by.
194
  """
195
  has_attribute = [
196
+ s
197
+ for s in self.fmt_streams
198
+ if getattr(s, attribute_name) is not None
199
  ]
200
  # Check that the attributes have string values.
201
+ if has_attribute and isinstance(
202
+ getattr(has_attribute[0], attribute_name), str
203
+ ):
204
  # Try to return a StreamQuery sorted by the integer representations
205
  # of the values.
206
  try:
 
208
  sorted(
209
  has_attribute,
210
  key=lambda s: int(
211
+ "".join(
212
+ filter(str.isdigit, getattr(s, attribute_name))
213
+ )
214
  ), # type: ignore # noqa: E501
215
  )
216
  )
 
277
 
278
  """
279
  return (
280
+ self.filter(progressive=True, subtype="mp4")
281
+ .order_by("resolution")
282
+ .first()
283
  )
284
 
285
  def get_highest_resolution(self) -> Optional[Stream]:
 
303
  The :class:`Stream <Stream>` matching the given itag or None if
304
  not found.
305
  """
306
+ return (
307
+ self.filter(only_audio=True, subtype=subtype)
308
+ .order_by("abr")
309
+ .last()
310
+ )
311
 
312
  def otf(self, is_otf: bool = False) -> "StreamQuery":
313
  """Filter stream by OTF, useful if some streams have 404 URLs
 
388
  """
389
  self.lang_code_index = {c.code: c for c in captions}
390
 
391
+ @deprecated(
392
+ "This object can be treated as a dictionary, i.e. captions['en']"
393
+ )
394
  def get_by_language_code(
395
  self, lang_code: str
396
  ) -> Optional[Caption]: # pragma: no cover
pytube/request.py CHANGED
@@ -1,10 +1,11 @@
1
  # -*- coding: utf-8 -*-
2
-
3
  """Implements a simple wrapper around urlopen."""
4
  import logging
5
  from functools import lru_cache
6
  from http.client import HTTPResponse
7
- from typing import Iterable, Dict, Optional
 
 
8
  from urllib.request import Request
9
  from urllib.request import urlopen
10
 
@@ -12,7 +13,9 @@ logger = logging.getLogger(__name__)
12
 
13
 
14
  def _execute_request(
15
- url: str, method: Optional[str] = None, headers: Optional[Dict[str, str]] = None
 
 
16
  ) -> HTTPResponse:
17
  base_headers = {"User-Agent": "Mozilla/5.0"}
18
  if headers:
@@ -50,7 +53,9 @@ def stream(
50
  while downloaded < file_size:
51
  stop_pos = min(downloaded + range_size, file_size) - 1
52
  range_header = f"bytes={downloaded}-{stop_pos}"
53
- response = _execute_request(url, method="GET", headers={"Range": range_header})
 
 
54
  if file_size == range_size:
55
  try:
56
  content_range = response.info()["Content-Range"]
 
1
  # -*- coding: utf-8 -*-
 
2
  """Implements a simple wrapper around urlopen."""
3
  import logging
4
  from functools import lru_cache
5
  from http.client import HTTPResponse
6
+ from typing import Dict
7
+ from typing import Iterable
8
+ from typing import Optional
9
  from urllib.request import Request
10
  from urllib.request import urlopen
11
 
 
13
 
14
 
15
  def _execute_request(
16
+ url: str,
17
+ method: Optional[str] = None,
18
+ headers: Optional[Dict[str, str]] = None,
19
  ) -> HTTPResponse:
20
  base_headers = {"User-Agent": "Mozilla/5.0"}
21
  if headers:
 
53
  while downloaded < file_size:
54
  stop_pos = min(downloaded + range_size, file_size) - 1
55
  range_header = f"bytes={downloaded}-{stop_pos}"
56
+ response = _execute_request(
57
+ url, method="GET", headers={"Range": range_header}
58
+ )
59
  if file_size == range_size:
60
  try:
61
  content_range = response.info()["Content-Range"]
pytube/streams.py CHANGED
@@ -1,5 +1,4 @@
1
  # -*- coding: utf-8 -*-
2
-
3
  """
4
  This module contains a container for stream manifest data.
5
 
@@ -8,16 +7,19 @@ combined). This was referred to as ``Video`` in the legacy pytube version, but
8
  has been renamed to accommodate DASH (which serves the audio and video
9
  separately).
10
  """
11
-
12
- from datetime import datetime
13
  import logging
14
  import os
15
- from typing import Dict, Tuple, Optional, BinaryIO
 
 
 
 
16
  from urllib.parse import parse_qs
17
 
18
  from pytube import extract
19
  from pytube import request
20
- from pytube.helpers import safe_filename, target_directory
 
21
  from pytube.itags import get_format_profile
22
  from pytube.monostate import Monostate
23
 
@@ -27,7 +29,9 @@ logger = logging.getLogger(__name__)
27
  class Stream:
28
  """Container for stream manifest data."""
29
 
30
- def __init__(self, stream: Dict, player_config_args: Dict, monostate: Monostate):
 
 
31
  """Construct a :class:`Stream <Stream>`.
32
 
33
  :param dict stream:
@@ -44,7 +48,9 @@ class Stream:
44
  self._monostate = monostate
45
 
46
  self.url = stream["url"] # signed download url
47
- self.itag = int(stream["itag"]) # stream format id (youtube nomenclature)
 
 
48
 
49
  # set type and codec info
50
 
@@ -68,8 +74,12 @@ class Stream:
68
  itag_profile = get_format_profile(self.itag)
69
  self.is_dash = itag_profile["is_dash"]
70
  self.abr = itag_profile["abr"] # average bitrate (audio streams only)
71
- self.fps = itag_profile["fps"] # frames per second (video streams only)
72
- self.resolution = itag_profile["resolution"] # resolution (e.g.: "480p")
 
 
 
 
73
  self.is_3d = itag_profile["is_3d"]
74
  self.is_hdr = itag_profile["is_hdr"]
75
  self.is_live = itag_profile["is_live"]
@@ -167,7 +177,9 @@ class Stream:
167
  """
168
  if self._monostate.duration and self.bitrate:
169
  bits_in_byte = 8
170
- return int((self._monostate.duration * self.bitrate) / bits_in_byte)
 
 
171
 
172
  return self.filesize
173
 
@@ -220,7 +232,9 @@ class Stream:
220
 
221
  """
222
  file_path = self.get_file_path(
223
- filename=filename, output_path=output_path, filename_prefix=filename_prefix
 
 
224
  )
225
 
226
  if skip_existing and self.exists_at_path(file_path):
@@ -230,7 +244,9 @@ class Stream:
230
 
231
  bytes_remaining = self.filesize
232
  logger.debug(
233
- "downloading (%s total bytes) file to %s", self.filesize, file_path,
 
 
234
  )
235
 
236
  with open(file_path, "wb") as fh:
@@ -257,7 +273,10 @@ class Stream:
257
  return os.path.join(target_directory(output_path), filename)
258
 
259
  def exists_at_path(self, file_path: str) -> bool:
260
- return os.path.isfile(file_path) and os.path.getsize(file_path) == self.filesize
 
 
 
261
 
262
  def stream_to_buffer(self, buffer: BinaryIO) -> None:
263
  """Write the media stream to buffer
@@ -276,7 +295,9 @@ class Stream:
276
  self.on_progress(chunk, buffer, bytes_remaining)
277
  self.on_complete(None)
278
 
279
- def on_progress(self, chunk: bytes, file_handler: BinaryIO, bytes_remaining: int):
 
 
280
  """On progress callback function.
281
 
282
  This function writes the binary data to the file, then checks if an
 
1
  # -*- coding: utf-8 -*-
 
2
  """
3
  This module contains a container for stream manifest data.
4
 
 
7
  has been renamed to accommodate DASH (which serves the audio and video
8
  separately).
9
  """
 
 
10
  import logging
11
  import os
12
+ from datetime import datetime
13
+ from typing import BinaryIO
14
+ from typing import Dict
15
+ from typing import Optional
16
+ from typing import Tuple
17
  from urllib.parse import parse_qs
18
 
19
  from pytube import extract
20
  from pytube import request
21
+ from pytube.helpers import safe_filename
22
+ from pytube.helpers import target_directory
23
  from pytube.itags import get_format_profile
24
  from pytube.monostate import Monostate
25
 
 
29
  class Stream:
30
  """Container for stream manifest data."""
31
 
32
+ def __init__(
33
+ self, stream: Dict, player_config_args: Dict, monostate: Monostate
34
+ ):
35
  """Construct a :class:`Stream <Stream>`.
36
 
37
  :param dict stream:
 
48
  self._monostate = monostate
49
 
50
  self.url = stream["url"] # signed download url
51
+ self.itag = int(
52
+ stream["itag"]
53
+ ) # stream format id (youtube nomenclature)
54
 
55
  # set type and codec info
56
 
 
74
  itag_profile = get_format_profile(self.itag)
75
  self.is_dash = itag_profile["is_dash"]
76
  self.abr = itag_profile["abr"] # average bitrate (audio streams only)
77
+ self.fps = itag_profile[
78
+ "fps"
79
+ ] # frames per second (video streams only)
80
+ self.resolution = itag_profile[
81
+ "resolution"
82
+ ] # resolution (e.g.: "480p")
83
  self.is_3d = itag_profile["is_3d"]
84
  self.is_hdr = itag_profile["is_hdr"]
85
  self.is_live = itag_profile["is_live"]
 
177
  """
178
  if self._monostate.duration and self.bitrate:
179
  bits_in_byte = 8
180
+ return int(
181
+ (self._monostate.duration * self.bitrate) / bits_in_byte
182
+ )
183
 
184
  return self.filesize
185
 
 
232
 
233
  """
234
  file_path = self.get_file_path(
235
+ filename=filename,
236
+ output_path=output_path,
237
+ filename_prefix=filename_prefix,
238
  )
239
 
240
  if skip_existing and self.exists_at_path(file_path):
 
244
 
245
  bytes_remaining = self.filesize
246
  logger.debug(
247
+ "downloading (%s total bytes) file to %s",
248
+ self.filesize,
249
+ file_path,
250
  )
251
 
252
  with open(file_path, "wb") as fh:
 
273
  return os.path.join(target_directory(output_path), filename)
274
 
275
  def exists_at_path(self, file_path: str) -> bool:
276
+ return (
277
+ os.path.isfile(file_path)
278
+ and os.path.getsize(file_path) == self.filesize
279
+ )
280
 
281
  def stream_to_buffer(self, buffer: BinaryIO) -> None:
282
  """Write the media stream to buffer
 
295
  self.on_progress(chunk, buffer, bytes_remaining)
296
  self.on_complete(None)
297
 
298
+ def on_progress(
299
+ self, chunk: bytes, file_handler: BinaryIO, bytes_remaining: int
300
+ ):
301
  """On progress callback function.
302
 
303
  This function writes the binary data to the file, then checks if an
setup.py CHANGED
@@ -3,6 +3,7 @@
3
  """This module contains setup instructions for pytube3."""
4
  import codecs
5
  import os
 
6
  from setuptools import setup
7
 
8
  here = os.path.abspath(os.path.dirname(__file__))
 
3
  """This module contains setup instructions for pytube3."""
4
  import codecs
5
  import os
6
+
7
  from setuptools import setup
8
 
9
  here = os.path.abspath(os.path.dirname(__file__))
tests/conftest.py CHANGED
@@ -1,6 +1,5 @@
1
  # -*- coding: utf-8 -*-
2
  """Reusable dependency injected testing components."""
3
-
4
  import gzip
5
  import json
6
  import os
@@ -57,7 +56,9 @@ def playlist_html():
57
  """Youtube playlist HTML loaded on 2020-01-25 from
58
  https://www.youtube.com/playlist?list=PLzMcBGfZo4-mP7qA9cagf68V06sko5otr"""
59
  file_path = os.path.join(
60
- os.path.dirname(os.path.realpath(__file__)), "mocks", "playlist.html.gz"
 
 
61
  )
62
  with gzip.open(file_path, "rb") as f:
63
  return f.read().decode("utf-8")
@@ -68,7 +69,9 @@ def playlist_long_html():
68
  """Youtube playlist HTML loaded on 2020-01-25 from
69
  https://www.youtube.com/playlist?list=PLzMcBGfZo4-mP7qA9cagf68V06sko5otr"""
70
  file_path = os.path.join(
71
- os.path.dirname(os.path.realpath(__file__)), "mocks", "playlist_long.html.gz"
 
 
72
  )
73
  with gzip.open(file_path, "rb") as f:
74
  return f.read().decode("utf-8")
 
1
  # -*- coding: utf-8 -*-
2
  """Reusable dependency injected testing components."""
 
3
  import gzip
4
  import json
5
  import os
 
56
  """Youtube playlist HTML loaded on 2020-01-25 from
57
  https://www.youtube.com/playlist?list=PLzMcBGfZo4-mP7qA9cagf68V06sko5otr"""
58
  file_path = os.path.join(
59
+ os.path.dirname(os.path.realpath(__file__)),
60
+ "mocks",
61
+ "playlist.html.gz",
62
  )
63
  with gzip.open(file_path, "rb") as f:
64
  return f.read().decode("utf-8")
 
69
  """Youtube playlist HTML loaded on 2020-01-25 from
70
  https://www.youtube.com/playlist?list=PLzMcBGfZo4-mP7qA9cagf68V06sko5otr"""
71
  file_path = os.path.join(
72
+ os.path.dirname(os.path.realpath(__file__)),
73
+ "mocks",
74
+ "playlist_long.html.gz",
75
  )
76
  with gzip.open(file_path, "rb") as f:
77
  return f.read().decode("utf-8")
tests/contrib/test_playlist.py CHANGED
@@ -15,7 +15,10 @@ def test_title(request_get):
15
  url = "https://www.fakeurl.com/playlist?list=PLS1QulWo1RIaJECMeUT4LFwJ-ghgoSH6n"
16
  pl = Playlist(url)
17
  pl_title = pl.title()
18
- assert pl_title == "(149) Python Tutorial for Beginners (For Absolute Beginners)"
 
 
 
19
 
20
 
21
  @mock.patch("pytube.contrib.playlist.request.get")
@@ -208,7 +211,9 @@ def test_trimmed_pagination(request_get, playlist_html, playlist_long_html):
208
 
209
 
210
  @mock.patch("pytube.contrib.playlist.request.get")
211
- def test_trimmed_pagination_not_found(request_get, playlist_html, playlist_long_html):
 
 
212
  url = "https://www.fakeurl.com/playlist?list=whatever"
213
  request_get.side_effect = [
214
  playlist_long_html,
 
15
  url = "https://www.fakeurl.com/playlist?list=PLS1QulWo1RIaJECMeUT4LFwJ-ghgoSH6n"
16
  pl = Playlist(url)
17
  pl_title = pl.title()
18
+ assert (
19
+ pl_title
20
+ == "(149) Python Tutorial for Beginners (For Absolute Beginners)"
21
+ )
22
 
23
 
24
  @mock.patch("pytube.contrib.playlist.request.get")
 
211
 
212
 
213
  @mock.patch("pytube.contrib.playlist.request.get")
214
+ def test_trimmed_pagination_not_found(
215
+ request_get, playlist_html, playlist_long_html
216
+ ):
217
  url = "https://www.fakeurl.com/playlist?list=whatever"
218
  request_get.side_effect = [
219
  playlist_long_html,
tests/generate_fixture.py CHANGED
@@ -1,16 +1,15 @@
1
  #!/usr/bin/env python3
2
-
3
  # flake8: noqa: E402
4
-
5
- from os import path
6
- import sys
7
  import json
 
 
 
 
8
 
9
  currentdir = path.dirname(path.realpath(__file__))
10
  parentdir = path.dirname(currentdir)
11
  sys.path.append(parentdir)
12
 
13
- from pytube import YouTube
14
 
15
  yt = YouTube(sys.argv[1], defer_prefetch_init=True)
16
  yt.prefetch()
 
1
  #!/usr/bin/env python3
 
2
  # flake8: noqa: E402
 
 
 
3
  import json
4
+ import sys
5
+ from os import path
6
+
7
+ from pytube import YouTube
8
 
9
  currentdir = path.dirname(path.realpath(__file__))
10
  parentdir = path.dirname(currentdir)
11
  sys.path.append(parentdir)
12
 
 
13
 
14
  yt = YouTube(sys.argv[1], defer_prefetch_init=True)
15
  yt.prefetch()
tests/test_captions.py CHANGED
@@ -1,10 +1,14 @@
1
  # -*- coding: utf-8 -*-
2
  from unittest import mock
3
- from unittest.mock import patch, mock_open, MagicMock
 
 
4
 
5
  import pytest
6
 
7
- from pytube import Caption, CaptionQuery, captions
 
 
8
 
9
 
10
  def test_float_to_srt_time_format():
@@ -60,10 +64,17 @@ def test_download(srt):
60
  with patch("builtins.open", open_mock):
61
  srt.return_value = ""
62
  caption = Caption(
63
- {"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
 
 
 
 
64
  )
65
  caption.download("title")
66
- assert open_mock.call_args_list[0][0][0].split("/")[-1] == "title (en).srt"
 
 
 
67
 
68
 
69
  @mock.patch("pytube.captions.Caption.generate_srt_captions")
@@ -72,10 +83,17 @@ def test_download_with_prefix(srt):
72
  with patch("builtins.open", open_mock):
73
  srt.return_value = ""
74
  caption = Caption(
75
- {"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
 
 
 
 
76
  )
77
  caption.download("title", filename_prefix="1 ")
78
- assert open_mock.call_args_list[0][0][0].split("/")[-1] == "1 title (en).srt"
 
 
 
79
 
80
 
81
  @mock.patch("pytube.captions.Caption.generate_srt_captions")
@@ -85,7 +103,11 @@ def test_download_with_output_path(srt):
85
  with patch("builtins.open", open_mock):
86
  srt.return_value = ""
87
  caption = Caption(
88
- {"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
 
 
 
 
89
  )
90
  file_path = caption.download("title", output_path="blah")
91
  assert file_path == "/target/title (en).srt"
@@ -98,10 +120,17 @@ def test_download_xml_and_trim_extension(xml):
98
  with patch("builtins.open", open_mock):
99
  xml.return_value = ""
100
  caption = Caption(
101
- {"url": "url1", "name": {"simpleText": "name1"}, "languageCode": "en"}
 
 
 
 
102
  )
103
  caption.download("title.xml", srt=False)
104
- assert open_mock.call_args_list[0][0][0].split("/")[-1] == "title (en).xml"
 
 
 
105
 
106
 
107
  def test_repr():
 
1
  # -*- coding: utf-8 -*-
2
  from unittest import mock
3
+ from unittest.mock import MagicMock
4
+ from unittest.mock import mock_open
5
+ from unittest.mock import patch
6
 
7
  import pytest
8
 
9
+ from pytube import Caption
10
+ from pytube import CaptionQuery
11
+ from pytube import captions
12
 
13
 
14
  def test_float_to_srt_time_format():
 
64
  with patch("builtins.open", open_mock):
65
  srt.return_value = ""
66
  caption = Caption(
67
+ {
68
+ "url": "url1",
69
+ "name": {"simpleText": "name1"},
70
+ "languageCode": "en",
71
+ }
72
  )
73
  caption.download("title")
74
+ assert (
75
+ open_mock.call_args_list[0][0][0].split("/")[-1]
76
+ == "title (en).srt"
77
+ )
78
 
79
 
80
  @mock.patch("pytube.captions.Caption.generate_srt_captions")
 
83
  with patch("builtins.open", open_mock):
84
  srt.return_value = ""
85
  caption = Caption(
86
+ {
87
+ "url": "url1",
88
+ "name": {"simpleText": "name1"},
89
+ "languageCode": "en",
90
+ }
91
  )
92
  caption.download("title", filename_prefix="1 ")
93
+ assert (
94
+ open_mock.call_args_list[0][0][0].split("/")[-1]
95
+ == "1 title (en).srt"
96
+ )
97
 
98
 
99
  @mock.patch("pytube.captions.Caption.generate_srt_captions")
 
103
  with patch("builtins.open", open_mock):
104
  srt.return_value = ""
105
  caption = Caption(
106
+ {
107
+ "url": "url1",
108
+ "name": {"simpleText": "name1"},
109
+ "languageCode": "en",
110
+ }
111
  )
112
  file_path = caption.download("title", output_path="blah")
113
  assert file_path == "/target/title (en).srt"
 
120
  with patch("builtins.open", open_mock):
121
  xml.return_value = ""
122
  caption = Caption(
123
+ {
124
+ "url": "url1",
125
+ "name": {"simpleText": "name1"},
126
+ "languageCode": "en",
127
+ }
128
  )
129
  caption.download("title.xml", srt=False)
130
+ assert (
131
+ open_mock.call_args_list[0][0][0].split("/")[-1]
132
+ == "title (en).xml"
133
+ )
134
 
135
 
136
  def test_repr():
tests/test_cli.py CHANGED
@@ -1,11 +1,15 @@
1
  # -*- coding: utf-8 -*-
2
  import argparse
3
  from unittest import mock
4
- from unittest.mock import MagicMock, patch
 
5
 
6
  import pytest
7
 
8
- from pytube import cli, StreamQuery, Caption, CaptionQuery
 
 
 
9
  from pytube.exceptions import PytubeError
10
 
11
  parse_args = cli._parse_args
@@ -179,7 +183,9 @@ def test_main_logging_setup(setup_logger):
179
  @mock.patch("pytube.cli.YouTube", return_value=None)
180
  def test_main_download_by_itag(youtube):
181
  parser = argparse.ArgumentParser()
182
- args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "--itag=10"])
 
 
183
  cli._parse_args = MagicMock(return_value=args)
184
  cli.download_by_itag = MagicMock()
185
  cli.main()
@@ -191,7 +197,8 @@ def test_main_download_by_itag(youtube):
191
  def test_main_build_playback_report(youtube):
192
  parser = argparse.ArgumentParser()
193
  args = parse_args(
194
- parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "--build-playback-report"]
 
195
  )
196
  cli._parse_args = MagicMock(return_value=args)
197
  cli.build_playback_report = MagicMock()
@@ -226,7 +233,9 @@ def test_main_download_caption(youtube):
226
  @mock.patch("pytube.cli.download_by_resolution")
227
  def test_download_by_resolution_flag(youtube, download_by_resolution):
228
  parser = argparse.ArgumentParser()
229
- args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-r", "320p"])
 
 
230
  cli._parse_args = MagicMock(return_value=args)
231
  cli.main()
232
  youtube.assert_called()
@@ -284,7 +293,9 @@ def test_download_by_resolution(download, stream, stream_query, youtube):
284
  stream_query.get_by_resolution.return_value = stream
285
  youtube.streams = stream_query
286
  # When
287
- cli.download_by_resolution(youtube=youtube, resolution="320p", target="test_target")
 
 
288
  # Then
289
  download.assert_called_with(stream, target="test_target")
290
 
@@ -319,12 +330,16 @@ def test_download_stream_file_exists(stream, capsys):
319
  def test_perform_args_should_ffmpeg_process(ffmpeg_process, youtube):
320
  # Given
321
  parser = argparse.ArgumentParser()
322
- args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-f", "best"])
 
 
323
  cli._parse_args = MagicMock(return_value=args)
324
  # When
325
  cli._perform_args_on_youtube(youtube, args)
326
  # Then
327
- ffmpeg_process.assert_called_with(youtube=youtube, resolution="best", target=None)
 
 
328
 
329
 
330
  @mock.patch("pytube.cli.YouTube")
@@ -335,7 +350,9 @@ def test_ffmpeg_process_best_should_download(_ffmpeg_downloader, youtube):
335
  streams = MagicMock()
336
  youtube.streams = streams
337
  video_stream = MagicMock()
338
- streams.filter.return_value.order_by.return_value.last.return_value = video_stream
 
 
339
  audio_stream = MagicMock()
340
  streams.get_audio_only.return_value = audio_stream
341
  # When
@@ -367,7 +384,9 @@ def test_ffmpeg_process_res_should_download(_ffmpeg_downloader, youtube):
367
 
368
  @mock.patch("pytube.cli.YouTube")
369
  @mock.patch("pytube.cli._ffmpeg_downloader")
370
- def test_ffmpeg_process_res_none_should_not_download(_ffmpeg_downloader, youtube):
 
 
371
  # Given
372
  target = "/target"
373
  streams = MagicMock()
@@ -392,7 +411,9 @@ def test_ffmpeg_process_audio_none_should_fallback_download(
392
  streams = MagicMock()
393
  youtube.streams = streams
394
  stream = MagicMock()
395
- streams.filter.return_value.order_by.return_value.last.return_value = stream
 
 
396
  streams.get_audio_only.return_value = None
397
  # When
398
  cli.ffmpeg_process(youtube, "best", target)
@@ -404,7 +425,9 @@ def test_ffmpeg_process_audio_none_should_fallback_download(
404
 
405
  @mock.patch("pytube.cli.YouTube")
406
  @mock.patch("pytube.cli._ffmpeg_downloader")
407
- def test_ffmpeg_process_audio_fallback_none_should_exit(_ffmpeg_downloader, youtube):
 
 
408
  # Given
409
  target = "/target"
410
  streams = MagicMock()
@@ -463,7 +486,9 @@ def test_ffmpeg_downloader(unique_name, download, run, unlink):
463
  def test_download_audio_args(youtube, download_audio):
464
  # Given
465
  parser = argparse.ArgumentParser()
466
- args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-a", "mp4"])
 
 
467
  cli._parse_args = MagicMock(return_value=args)
468
  # When
469
  cli.main()
@@ -515,10 +540,16 @@ def test_perform_args_on_youtube(youtube):
515
 
516
  @mock.patch("pytube.cli.os.path.exists", return_value=False)
517
  def test_unique_name(path_exists):
518
- assert cli._unique_name("base", "subtype", "video", "target") == "base_video_0"
 
 
 
519
 
520
 
521
  @mock.patch("pytube.cli.os.path.exists")
522
  def test_unique_name_counter(path_exists):
523
  path_exists.side_effect = [True, False]
524
- assert cli._unique_name("base", "subtype", "video", "target") == "base_video_1"
 
 
 
 
1
  # -*- coding: utf-8 -*-
2
  import argparse
3
  from unittest import mock
4
+ from unittest.mock import MagicMock
5
+ from unittest.mock import patch
6
 
7
  import pytest
8
 
9
+ from pytube import Caption
10
+ from pytube import CaptionQuery
11
+ from pytube import cli
12
+ from pytube import StreamQuery
13
  from pytube.exceptions import PytubeError
14
 
15
  parse_args = cli._parse_args
 
183
  @mock.patch("pytube.cli.YouTube", return_value=None)
184
  def test_main_download_by_itag(youtube):
185
  parser = argparse.ArgumentParser()
186
+ args = parse_args(
187
+ parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "--itag=10"]
188
+ )
189
  cli._parse_args = MagicMock(return_value=args)
190
  cli.download_by_itag = MagicMock()
191
  cli.main()
 
197
  def test_main_build_playback_report(youtube):
198
  parser = argparse.ArgumentParser()
199
  args = parse_args(
200
+ parser,
201
+ ["http://youtube.com/watch?v=9bZkp7q19f0", "--build-playback-report"],
202
  )
203
  cli._parse_args = MagicMock(return_value=args)
204
  cli.build_playback_report = MagicMock()
 
233
  @mock.patch("pytube.cli.download_by_resolution")
234
  def test_download_by_resolution_flag(youtube, download_by_resolution):
235
  parser = argparse.ArgumentParser()
236
+ args = parse_args(
237
+ parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-r", "320p"]
238
+ )
239
  cli._parse_args = MagicMock(return_value=args)
240
  cli.main()
241
  youtube.assert_called()
 
293
  stream_query.get_by_resolution.return_value = stream
294
  youtube.streams = stream_query
295
  # When
296
+ cli.download_by_resolution(
297
+ youtube=youtube, resolution="320p", target="test_target"
298
+ )
299
  # Then
300
  download.assert_called_with(stream, target="test_target")
301
 
 
330
  def test_perform_args_should_ffmpeg_process(ffmpeg_process, youtube):
331
  # Given
332
  parser = argparse.ArgumentParser()
333
+ args = parse_args(
334
+ parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-f", "best"]
335
+ )
336
  cli._parse_args = MagicMock(return_value=args)
337
  # When
338
  cli._perform_args_on_youtube(youtube, args)
339
  # Then
340
+ ffmpeg_process.assert_called_with(
341
+ youtube=youtube, resolution="best", target=None
342
+ )
343
 
344
 
345
  @mock.patch("pytube.cli.YouTube")
 
350
  streams = MagicMock()
351
  youtube.streams = streams
352
  video_stream = MagicMock()
353
+ streams.filter.return_value.order_by.return_value.last.return_value = (
354
+ video_stream
355
+ )
356
  audio_stream = MagicMock()
357
  streams.get_audio_only.return_value = audio_stream
358
  # When
 
384
 
385
  @mock.patch("pytube.cli.YouTube")
386
  @mock.patch("pytube.cli._ffmpeg_downloader")
387
+ def test_ffmpeg_process_res_none_should_not_download(
388
+ _ffmpeg_downloader, youtube
389
+ ):
390
  # Given
391
  target = "/target"
392
  streams = MagicMock()
 
411
  streams = MagicMock()
412
  youtube.streams = streams
413
  stream = MagicMock()
414
+ streams.filter.return_value.order_by.return_value.last.return_value = (
415
+ stream
416
+ )
417
  streams.get_audio_only.return_value = None
418
  # When
419
  cli.ffmpeg_process(youtube, "best", target)
 
425
 
426
  @mock.patch("pytube.cli.YouTube")
427
  @mock.patch("pytube.cli._ffmpeg_downloader")
428
+ def test_ffmpeg_process_audio_fallback_none_should_exit(
429
+ _ffmpeg_downloader, youtube
430
+ ):
431
  # Given
432
  target = "/target"
433
  streams = MagicMock()
 
486
  def test_download_audio_args(youtube, download_audio):
487
  # Given
488
  parser = argparse.ArgumentParser()
489
+ args = parse_args(
490
+ parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-a", "mp4"]
491
+ )
492
  cli._parse_args = MagicMock(return_value=args)
493
  # When
494
  cli.main()
 
540
 
541
  @mock.patch("pytube.cli.os.path.exists", return_value=False)
542
  def test_unique_name(path_exists):
543
+ assert (
544
+ cli._unique_name("base", "subtype", "video", "target")
545
+ == "base_video_0"
546
+ )
547
 
548
 
549
  @mock.patch("pytube.cli.os.path.exists")
550
  def test_unique_name_counter(path_exists):
551
  path_exists.side_effect = [True, False]
552
+ assert (
553
+ cli._unique_name("base", "subtype", "video", "target")
554
+ == "base_video_1"
555
+ )
tests/test_exceptions.py CHANGED
@@ -1,5 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
- from pytube.exceptions import VideoUnavailable, RegexMatchError, LiveStreamError
 
 
3
 
4
 
5
  def test_video_unavailable():
 
1
  # -*- coding: utf-8 -*-
2
+ from pytube.exceptions import LiveStreamError
3
+ from pytube.exceptions import RegexMatchError
4
+ from pytube.exceptions import VideoUnavailable
5
 
6
 
7
  def test_video_unavailable():
tests/test_extract.py CHANGED
@@ -2,8 +2,8 @@
2
  """Unit tests for the :module:`extract <extract>` module."""
3
  import pytest
4
 
5
- from pytube.exceptions import RegexMatchError
6
  from pytube import extract
 
7
 
8
 
9
  def test_extract_video_id():
@@ -25,7 +25,8 @@ def test_info_url(age_restricted):
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"
@@ -36,7 +37,9 @@ def test_info_url_age_restricted(cipher_signature):
36
 
37
 
38
  def test_js_url(cipher_signature):
39
- expected = "https://youtube.com/yts/jsbin/player_ias-vflWQEEag/en_US/base.js"
 
 
40
  result = extract.js_url(cipher_signature.watch_html)
41
  assert expected == result
42
 
@@ -69,7 +72,9 @@ def test_get_vid_desc(cipher_signature):
69
 
70
 
71
  def test_mime_type_codec():
72
- mime_type, mime_subtype = extract.mime_type_codec('audio/webm; codecs="opus"')
 
 
73
  assert mime_type == "audio/webm"
74
  assert mime_subtype == ["opus"]
75
 
 
2
  """Unit tests for the :module:`extract <extract>` module."""
3
  import pytest
4
 
 
5
  from pytube import extract
6
+ from pytube.exceptions import RegexMatchError
7
 
8
 
9
  def test_extract_video_id():
 
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,
29
+ watch_url=cipher_signature.watch_url,
30
  )
31
  expected = (
32
  "https://youtube.com/get_video_info?video_id=9bZkp7q19f0&el=%24el"
 
37
 
38
 
39
  def test_js_url(cipher_signature):
40
+ expected = (
41
+ "https://youtube.com/yts/jsbin/player_ias-vflWQEEag/en_US/base.js"
42
+ )
43
  result = extract.js_url(cipher_signature.watch_html)
44
  assert expected == result
45
 
 
72
 
73
 
74
  def test_mime_type_codec():
75
+ mime_type, mime_subtype = extract.mime_type_codec(
76
+ 'audio/webm; codecs="opus"'
77
+ )
78
  assert mime_type == "audio/webm"
79
  assert mime_subtype == ["opus"]
80
 
tests/test_helpers.py CHANGED
@@ -5,7 +5,10 @@ import pytest
5
 
6
  from pytube import helpers
7
  from pytube.exceptions import RegexMatchError
8
- from pytube.helpers import deprecated, cache, target_directory, setup_logger
 
 
 
9
 
10
 
11
  def test_regex_search_no_match():
 
5
 
6
  from pytube import helpers
7
  from pytube.exceptions import RegexMatchError
8
+ from pytube.helpers import cache
9
+ from pytube.helpers import deprecated
10
+ from pytube.helpers import setup_logger
11
+ from pytube.helpers import target_directory
12
 
13
 
14
  def test_regex_search_no_match():
tests/test_query.py CHANGED
@@ -59,7 +59,8 @@ def test_order_by(cipher_signature):
59
  :class:`Stream <Stream>` instances in the expected order.
60
  """
61
  itags = [
62
- s.itag for s in cipher_signature.streams.filter(type="audio").order_by("itag")
 
63
  ]
64
  assert itags == [140, 249, 250, 251]
65
 
@@ -71,7 +72,9 @@ def test_order_by_descending(cipher_signature):
71
  # numerical values
72
  itags = [
73
  s.itag
74
- for s in cipher_signature.streams.filter(type="audio").order_by("itag").desc()
 
 
75
  ]
76
  assert itags == [251, 250, 249, 140]
77
 
@@ -93,7 +96,9 @@ def test_order_by_ascending(cipher_signature):
93
  # numerical values
94
  itags = [
95
  s.itag
96
- for s in cipher_signature.streams.filter(type="audio").order_by("itag").asc()
 
 
97
  ]
98
  assert itags == [140, 249, 250, 251]
99
 
@@ -101,7 +106,9 @@ def test_order_by_ascending(cipher_signature):
101
  def test_order_by_non_numerical_ascending(cipher_signature):
102
  mime_types = [
103
  s.mime_type
104
- for s in cipher_signature.streams.filter(res="360p").order_by("mime_type").asc()
 
 
105
  ]
106
  assert mime_types == ["video/mp4", "video/mp4", "video/webm"]
107
 
 
59
  :class:`Stream <Stream>` instances in the expected order.
60
  """
61
  itags = [
62
+ s.itag
63
+ for s in cipher_signature.streams.filter(type="audio").order_by("itag")
64
  ]
65
  assert itags == [140, 249, 250, 251]
66
 
 
72
  # numerical values
73
  itags = [
74
  s.itag
75
+ for s in cipher_signature.streams.filter(type="audio")
76
+ .order_by("itag")
77
+ .desc()
78
  ]
79
  assert itags == [251, 250, 249, 140]
80
 
 
96
  # numerical values
97
  itags = [
98
  s.itag
99
+ for s in cipher_signature.streams.filter(type="audio")
100
+ .order_by("itag")
101
+ .asc()
102
  ]
103
  assert itags == [140, 249, 250, 251]
104
 
 
106
  def test_order_by_non_numerical_ascending(cipher_signature):
107
  mime_types = [
108
  s.mime_type
109
+ for s in cipher_signature.streams.filter(res="360p")
110
+ .order_by("mime_type")
111
+ .asc()
112
  ]
113
  assert mime_types == ["video/mp4", "video/mp4", "video/webm"]
114
 
tests/test_streams.py CHANGED
@@ -6,7 +6,8 @@ from unittest import mock
6
  from unittest.mock import MagicMock
7
 
8
  from pytube import request
9
- from pytube import Stream, streams
 
10
 
11
 
12
  @mock.patch("pytube.streams.request")
@@ -61,7 +62,9 @@ def test_title(cipher_signature):
61
 
62
 
63
  def test_expiration(cipher_signature):
64
- assert cipher_signature.streams[0].expiration == datetime(2020, 1, 16, 5, 12, 5)
 
 
65
 
66
 
67
  def test_caption_tracks(presigned_video):
 
6
  from unittest.mock import MagicMock
7
 
8
  from pytube import request
9
+ from pytube import Stream
10
+ from pytube import streams
11
 
12
 
13
  @mock.patch("pytube.streams.request")
 
62
 
63
 
64
  def test_expiration(cipher_signature):
65
+ assert cipher_signature.streams[0].expiration == datetime(
66
+ 2020, 1, 16, 5, 12, 5
67
+ )
68
 
69
 
70
  def test_caption_tracks(presigned_video):