more type checking
Browse files- README.md +2 -0
- pytube/__main__.py +10 -10
- pytube/captions.py +3 -2
- pytube/cli.py +12 -6
- pytube/exceptions.py +1 -1
- pytube/extract.py +2 -2
- pytube/itags.py +1 -0
- pytube/mixins.py +2 -2
- pytube/query.py +6 -9
- pytube/streams.py +8 -3
README.md
CHANGED
@@ -229,6 +229,8 @@ Finally, if you're filing a bug report, the cli contains a switch called ``--bui
|
|
229 |
|
230 |
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
231 |
|
|
|
|
|
232 |
#### Virtual environment
|
233 |
|
234 |
Virtual environment is setup with [pipenv](https://pipenv-fork.readthedocs.io/en/latest/) and can be automatically activated with [direnv](https://direnv.net/docs/installation.html)
|
|
|
229 |
|
230 |
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
231 |
|
232 |
+
To run code checking before a PR use ``make test``
|
233 |
+
|
234 |
#### Virtual environment
|
235 |
|
236 |
Virtual environment is setup with [pipenv](https://pipenv-fork.readthedocs.io/en/latest/) and can be automatically activated with [direnv](https://direnv.net/docs/installation.html)
|
pytube/__main__.py
CHANGED
@@ -91,7 +91,7 @@ class YouTube(object):
|
|
91 |
if not defer_prefetch_init:
|
92 |
self.prefetch_descramble()
|
93 |
|
94 |
-
def prefetch_descramble(self):
|
95 |
"""Download data, descramble it, and build Stream instances.
|
96 |
|
97 |
:rtype: None
|
@@ -100,7 +100,7 @@ class YouTube(object):
|
|
100 |
self.prefetch()
|
101 |
self.descramble()
|
102 |
|
103 |
-
def descramble(self):
|
104 |
"""Descramble the stream data and build Stream instances.
|
105 |
|
106 |
The initialization process takes advantage of Python's
|
@@ -158,7 +158,7 @@ class YouTube(object):
|
|
158 |
self.initialize_caption_objects()
|
159 |
logger.info("init finished successfully")
|
160 |
|
161 |
-
def prefetch(self):
|
162 |
"""Eagerly download all necessary data.
|
163 |
|
164 |
Eagerly executes all necessary network requests so all other
|
@@ -185,7 +185,7 @@ class YouTube(object):
|
|
185 |
self.js_url = extract.js_url(self.watch_html, self.age_restricted)
|
186 |
self.js = request.get(self.js_url)
|
187 |
|
188 |
-
def initialize_stream_objects(self, fmt: str):
|
189 |
"""Convert manifest data to instances of :class:`Stream <Stream>`.
|
190 |
|
191 |
Take the unscrambled stream data and uses it to initialize
|
@@ -208,7 +208,7 @@ class YouTube(object):
|
|
208 |
)
|
209 |
self.fmt_streams.append(video)
|
210 |
|
211 |
-
def initialize_caption_objects(self):
|
212 |
"""Populate instances of :class:`Caption <Caption>`.
|
213 |
|
214 |
Take the unscrambled player response data, and use it to initialize
|
@@ -230,7 +230,7 @@ class YouTube(object):
|
|
230 |
self.caption_tracks.append(Caption(caption_track))
|
231 |
|
232 |
@property
|
233 |
-
def captions(self):
|
234 |
"""Interface to query caption tracks.
|
235 |
|
236 |
:rtype: :class:`CaptionQuery <CaptionQuery>`.
|
@@ -238,7 +238,7 @@ class YouTube(object):
|
|
238 |
return CaptionQuery([c for c in self.caption_tracks])
|
239 |
|
240 |
@property
|
241 |
-
def streams(self):
|
242 |
"""Interface to query both adaptive (DASH) and progressive streams.
|
243 |
|
244 |
:rtype: :class:`StreamQuery <StreamQuery>`.
|
@@ -246,7 +246,7 @@ class YouTube(object):
|
|
246 |
return StreamQuery([s for s in self.fmt_streams])
|
247 |
|
248 |
@property
|
249 |
-
def thumbnail_url(self):
|
250 |
"""Get the thumbnail url image.
|
251 |
|
252 |
:rtype: str
|
@@ -255,7 +255,7 @@ class YouTube(object):
|
|
255 |
return self.player_config_args["thumbnail_url"]
|
256 |
|
257 |
@property
|
258 |
-
def title(self):
|
259 |
"""Get the video title.
|
260 |
|
261 |
:rtype: str
|
@@ -264,7 +264,7 @@ class YouTube(object):
|
|
264 |
return self.player_config_args["title"]
|
265 |
|
266 |
@property
|
267 |
-
def description(self):
|
268 |
"""Get the video description.
|
269 |
|
270 |
:rtype: str
|
|
|
91 |
if not defer_prefetch_init:
|
92 |
self.prefetch_descramble()
|
93 |
|
94 |
+
def prefetch_descramble(self) -> None:
|
95 |
"""Download data, descramble it, and build Stream instances.
|
96 |
|
97 |
:rtype: None
|
|
|
100 |
self.prefetch()
|
101 |
self.descramble()
|
102 |
|
103 |
+
def descramble(self) -> None:
|
104 |
"""Descramble the stream data and build Stream instances.
|
105 |
|
106 |
The initialization process takes advantage of Python's
|
|
|
158 |
self.initialize_caption_objects()
|
159 |
logger.info("init finished successfully")
|
160 |
|
161 |
+
def prefetch(self) -> None:
|
162 |
"""Eagerly download all necessary data.
|
163 |
|
164 |
Eagerly executes all necessary network requests so all other
|
|
|
185 |
self.js_url = extract.js_url(self.watch_html, self.age_restricted)
|
186 |
self.js = request.get(self.js_url)
|
187 |
|
188 |
+
def initialize_stream_objects(self, fmt: str) -> None:
|
189 |
"""Convert manifest data to instances of :class:`Stream <Stream>`.
|
190 |
|
191 |
Take the unscrambled stream data and uses it to initialize
|
|
|
208 |
)
|
209 |
self.fmt_streams.append(video)
|
210 |
|
211 |
+
def initialize_caption_objects(self) -> None:
|
212 |
"""Populate instances of :class:`Caption <Caption>`.
|
213 |
|
214 |
Take the unscrambled player response data, and use it to initialize
|
|
|
230 |
self.caption_tracks.append(Caption(caption_track))
|
231 |
|
232 |
@property
|
233 |
+
def captions(self) -> CaptionQuery:
|
234 |
"""Interface to query caption tracks.
|
235 |
|
236 |
:rtype: :class:`CaptionQuery <CaptionQuery>`.
|
|
|
238 |
return CaptionQuery([c for c in self.caption_tracks])
|
239 |
|
240 |
@property
|
241 |
+
def streams(self) -> StreamQuery:
|
242 |
"""Interface to query both adaptive (DASH) and progressive streams.
|
243 |
|
244 |
:rtype: :class:`StreamQuery <StreamQuery>`.
|
|
|
246 |
return StreamQuery([s for s in self.fmt_streams])
|
247 |
|
248 |
@property
|
249 |
+
def thumbnail_url(self) -> str:
|
250 |
"""Get the thumbnail url image.
|
251 |
|
252 |
:rtype: str
|
|
|
255 |
return self.player_config_args["thumbnail_url"]
|
256 |
|
257 |
@property
|
258 |
+
def title(self) -> str:
|
259 |
"""Get the video title.
|
260 |
|
261 |
:rtype: str
|
|
|
264 |
return self.player_config_args["title"]
|
265 |
|
266 |
@property
|
267 |
+
def description(self) -> str:
|
268 |
"""Get the video description.
|
269 |
|
270 |
:rtype: str
|
pytube/captions.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2 |
import math
|
3 |
import time
|
4 |
import xml.etree.ElementTree as ElementTree
|
|
|
5 |
|
6 |
from pytube import request
|
7 |
from html import unescape
|
@@ -10,7 +11,7 @@ from html import unescape
|
|
10 |
class Caption:
|
11 |
"""Container for caption tracks."""
|
12 |
|
13 |
-
def __init__(self, caption_track):
|
14 |
"""Construct a :class:`Caption <Caption>`.
|
15 |
|
16 |
:param dict caption_track:
|
@@ -33,7 +34,7 @@ class Caption:
|
|
33 |
"""
|
34 |
return self.xml_caption_to_srt(self.xml_captions)
|
35 |
|
36 |
-
def float_to_srt_time_format(self, d):
|
37 |
"""Convert decimal durations into proper srt format.
|
38 |
|
39 |
:rtype: str
|
|
|
2 |
import math
|
3 |
import time
|
4 |
import xml.etree.ElementTree as ElementTree
|
5 |
+
from typing import Dict
|
6 |
|
7 |
from pytube import request
|
8 |
from html import unescape
|
|
|
11 |
class Caption:
|
12 |
"""Container for caption tracks."""
|
13 |
|
14 |
+
def __init__(self, caption_track: Dict):
|
15 |
"""Construct a :class:`Caption <Caption>`.
|
16 |
|
17 |
:param dict caption_track:
|
|
|
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
|
pytube/cli.py
CHANGED
@@ -8,6 +8,7 @@ import json
|
|
8 |
import logging
|
9 |
import os
|
10 |
import sys
|
|
|
11 |
|
12 |
from pytube import __version__
|
13 |
from pytube import YouTube
|
@@ -66,7 +67,7 @@ def main():
|
|
66 |
download(args.url, args.itag)
|
67 |
|
68 |
|
69 |
-
def build_playback_report(url: str):
|
70 |
"""Serialize the request data to json for offline debugging.
|
71 |
|
72 |
:param str url:
|
@@ -95,13 +96,15 @@ def build_playback_report(url: str):
|
|
95 |
)
|
96 |
|
97 |
|
98 |
-
def get_terminal_size():
|
99 |
"""Return the terminal size in rows and columns."""
|
100 |
rows, columns = os.popen("stty size", "r").read().split()
|
101 |
return int(rows), int(columns)
|
102 |
|
103 |
|
104 |
-
def display_progress_bar(
|
|
|
|
|
105 |
"""Display a simple, pretty progress bar.
|
106 |
|
107 |
Example:
|
@@ -150,7 +153,7 @@ def on_progress(stream, chunk, file_handle, bytes_remaining):
|
|
150 |
display_progress_bar(bytes_received, filesize)
|
151 |
|
152 |
|
153 |
-
def download(url: str, itag: str):
|
154 |
"""Start downloading a YouTube video.
|
155 |
|
156 |
:param str url:
|
@@ -162,7 +165,10 @@ def download(url: str, itag: str):
|
|
162 |
# TODO(nficano): allow download target to be specified
|
163 |
# TODO(nficano): allow dash itags to be selected
|
164 |
yt = YouTube(url, on_progress_callback=on_progress)
|
165 |
-
stream = yt.streams.get_by_itag(itag)
|
|
|
|
|
|
|
166 |
print("\n{fn} | {fs} bytes".format(fn=stream.default_filename, fs=stream.filesize,))
|
167 |
try:
|
168 |
stream.download()
|
@@ -171,7 +177,7 @@ def download(url: str, itag: str):
|
|
171 |
sys.exit()
|
172 |
|
173 |
|
174 |
-
def display_streams(url: str):
|
175 |
"""Probe YouTube video and lists its available formats.
|
176 |
|
177 |
:param str url:
|
|
|
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
|
|
|
67 |
download(args.url, args.itag)
|
68 |
|
69 |
|
70 |
+
def build_playback_report(url: str) -> None:
|
71 |
"""Serialize the request data to json for offline debugging.
|
72 |
|
73 |
:param str url:
|
|
|
96 |
)
|
97 |
|
98 |
|
99 |
+
def get_terminal_size() -> Tuple[int, int]:
|
100 |
"""Return the terminal size in rows and columns."""
|
101 |
rows, columns = os.popen("stty size", "r").read().split()
|
102 |
return int(rows), int(columns)
|
103 |
|
104 |
|
105 |
+
def display_progress_bar(
|
106 |
+
bytes_received: int, filesize: int, ch: str = "█", scale: float = 0.55
|
107 |
+
) -> None:
|
108 |
"""Display a simple, pretty progress bar.
|
109 |
|
110 |
Example:
|
|
|
153 |
display_progress_bar(bytes_received, filesize)
|
154 |
|
155 |
|
156 |
+
def download(url: str, itag: str) -> None:
|
157 |
"""Start downloading a YouTube video.
|
158 |
|
159 |
:param str url:
|
|
|
165 |
# TODO(nficano): allow download target to be specified
|
166 |
# TODO(nficano): allow dash itags to be selected
|
167 |
yt = YouTube(url, on_progress_callback=on_progress)
|
168 |
+
stream = yt.streams.get_by_itag(int(itag))
|
169 |
+
if stream is None:
|
170 |
+
print("Could not find a stream with itag: " + itag)
|
171 |
+
sys.exit()
|
172 |
print("\n{fn} | {fs} bytes".format(fn=stream.default_filename, fs=stream.filesize,))
|
173 |
try:
|
174 |
stream.download()
|
|
|
177 |
sys.exit()
|
178 |
|
179 |
|
180 |
+
def display_streams(url: str) -> None:
|
181 |
"""Probe YouTube video and lists its available formats.
|
182 |
|
183 |
:param str url:
|
pytube/exceptions.py
CHANGED
@@ -15,7 +15,7 @@ class PytubeError(Exception):
|
|
15 |
class ExtractError(PytubeError):
|
16 |
"""Data extraction based exception."""
|
17 |
|
18 |
-
def __init__(self, msg: str, video_id: str =
|
19 |
"""Construct an instance of a :class:`ExtractError <ExtractError>`.
|
20 |
|
21 |
:param str msg:
|
|
|
15 |
class ExtractError(PytubeError):
|
16 |
"""Data extraction based exception."""
|
17 |
|
18 |
+
def __init__(self, msg: str, video_id: str = "unknown id"):
|
19 |
"""Construct an instance of a :class:`ExtractError <ExtractError>`.
|
20 |
|
21 |
:param str msg:
|
pytube/extract.py
CHANGED
@@ -4,7 +4,7 @@ import json
|
|
4 |
from collections import OrderedDict
|
5 |
|
6 |
from html.parser import HTMLParser
|
7 |
-
from typing import Any
|
8 |
from urllib.parse import quote
|
9 |
from urllib.parse import urlencode
|
10 |
from pytube.exceptions import RegexMatchError
|
@@ -95,7 +95,7 @@ def eurl(video_id: str) -> str:
|
|
95 |
def video_info_url(
|
96 |
video_id: str,
|
97 |
watch_url: str,
|
98 |
-
watch_html: str,
|
99 |
embed_html: str,
|
100 |
age_restricted: bool,
|
101 |
) -> str:
|
|
|
4 |
from collections import OrderedDict
|
5 |
|
6 |
from html.parser import HTMLParser
|
7 |
+
from typing import Any, Optional
|
8 |
from urllib.parse import quote
|
9 |
from urllib.parse import urlencode
|
10 |
from pytube.exceptions import RegexMatchError
|
|
|
95 |
def video_info_url(
|
96 |
video_id: str,
|
97 |
watch_url: str,
|
98 |
+
watch_html: Optional[str],
|
99 |
embed_html: str,
|
100 |
age_restricted: bool,
|
101 |
) -> str:
|
pytube/itags.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""This module contains a lookup table of YouTube's itag values."""
|
|
|
3 |
|
4 |
ITAGS = {
|
5 |
5: ("240p", "64kbps"),
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""This module contains a lookup table of YouTube's itag values."""
|
3 |
+
from typing import Dict
|
4 |
|
5 |
ITAGS = {
|
6 |
5: ("240p", "64kbps"),
|
pytube/mixins.py
CHANGED
@@ -17,7 +17,7 @@ from pytube.exceptions import LiveStreamError
|
|
17 |
logger = logging.getLogger(__name__)
|
18 |
|
19 |
|
20 |
-
def apply_signature(config_args: Dict, fmt: str, js: str):
|
21 |
"""Apply the decrypted signature to the stream manifest.
|
22 |
|
23 |
:param dict config_args:
|
@@ -67,7 +67,7 @@ def apply_signature(config_args: Dict, fmt: str, js: str):
|
|
67 |
stream_manifest[i]["url"] = url + "&sig=" + signature
|
68 |
|
69 |
|
70 |
-
def apply_descrambler(stream_data: Dict, key: str):
|
71 |
"""Apply various in-place transforms to YouTube's media stream data.
|
72 |
|
73 |
Creates a ``list`` of dictionaries by string splitting on commas, then
|
|
|
17 |
logger = logging.getLogger(__name__)
|
18 |
|
19 |
|
20 |
+
def apply_signature(config_args: Dict, fmt: str, js: str) -> None:
|
21 |
"""Apply the decrypted signature to the stream manifest.
|
22 |
|
23 |
:param dict config_args:
|
|
|
67 |
stream_manifest[i]["url"] = url + "&sig=" + signature
|
68 |
|
69 |
|
70 |
+
def apply_descrambler(stream_data: Dict, key: str) -> None:
|
71 |
"""Apply various in-place transforms to YouTube's media stream data.
|
72 |
|
73 |
Creates a ``list`` of dictionaries by string splitting on commas, then
|
pytube/query.py
CHANGED
@@ -206,7 +206,7 @@ class StreamQuery:
|
|
206 |
sorted(has_attribute, key=lambda s: getattr(s, attribute_name))
|
207 |
)
|
208 |
|
209 |
-
def desc(self):
|
210 |
"""Sort streams in descending order.
|
211 |
|
212 |
:rtype: :class:`StreamQuery <StreamQuery>`
|
@@ -214,7 +214,7 @@ class StreamQuery:
|
|
214 |
"""
|
215 |
return StreamQuery(self.fmt_streams[::-1])
|
216 |
|
217 |
-
def asc(self):
|
218 |
"""Sort streams in ascending order.
|
219 |
|
220 |
:rtype: :class:`StreamQuery <StreamQuery>`
|
@@ -222,7 +222,7 @@ class StreamQuery:
|
|
222 |
"""
|
223 |
return self
|
224 |
|
225 |
-
def get_by_itag(self, itag):
|
226 |
"""Get the corresponding :class:`Stream <Stream>` for a given itag.
|
227 |
|
228 |
:param int itag:
|
@@ -233,12 +233,9 @@ class StreamQuery:
|
|
233 |
not found.
|
234 |
|
235 |
"""
|
236 |
-
|
237 |
-
return self.itag_index[int(itag)]
|
238 |
-
except KeyError:
|
239 |
-
pass
|
240 |
|
241 |
-
def first(self):
|
242 |
"""Get the first :class:`Stream <Stream>` in the results.
|
243 |
|
244 |
:rtype: :class:`Stream <Stream>` or None
|
@@ -250,7 +247,7 @@ class StreamQuery:
|
|
250 |
try:
|
251 |
return self.fmt_streams[0]
|
252 |
except IndexError:
|
253 |
-
|
254 |
|
255 |
def last(self):
|
256 |
"""Get the last :class:`Stream <Stream>` in the results.
|
|
|
206 |
sorted(has_attribute, key=lambda s: getattr(s, attribute_name))
|
207 |
)
|
208 |
|
209 |
+
def desc(self) -> "StreamQuery":
|
210 |
"""Sort streams in descending order.
|
211 |
|
212 |
:rtype: :class:`StreamQuery <StreamQuery>`
|
|
|
214 |
"""
|
215 |
return StreamQuery(self.fmt_streams[::-1])
|
216 |
|
217 |
+
def asc(self) -> "StreamQuery":
|
218 |
"""Sort streams in ascending order.
|
219 |
|
220 |
:rtype: :class:`StreamQuery <StreamQuery>`
|
|
|
222 |
"""
|
223 |
return self
|
224 |
|
225 |
+
def get_by_itag(self, itag) -> Optional[Stream]:
|
226 |
"""Get the corresponding :class:`Stream <Stream>` for a given itag.
|
227 |
|
228 |
:param int itag:
|
|
|
233 |
not found.
|
234 |
|
235 |
"""
|
236 |
+
return self.itag_index.get(int(itag))
|
|
|
|
|
|
|
237 |
|
238 |
+
def first(self) -> Optional[Stream]:
|
239 |
"""Get the first :class:`Stream <Stream>` in the results.
|
240 |
|
241 |
:rtype: :class:`Stream <Stream>` or None
|
|
|
247 |
try:
|
248 |
return self.fmt_streams[0]
|
249 |
except IndexError:
|
250 |
+
return None
|
251 |
|
252 |
def last(self):
|
253 |
"""Get the last :class:`Stream <Stream>` in the results.
|
pytube/streams.py
CHANGED
@@ -83,7 +83,7 @@ class Stream(object):
|
|
83 |
# streams return NoneType for audio/video depending.
|
84 |
self.video_codec, self.audio_codec = self.parse_codecs()
|
85 |
|
86 |
-
def set_attributes_from_dict(self, dct: Dict):
|
87 |
"""Set class attributes from dictionary items.
|
88 |
|
89 |
:rtype: None
|
@@ -199,7 +199,12 @@ class Stream(object):
|
|
199 |
filename = safe_filename(self.title)
|
200 |
return "{filename}.{s.subtype}".format(filename=filename, s=self)
|
201 |
|
202 |
-
def download(
|
|
|
|
|
|
|
|
|
|
|
203 |
"""Write the media stream to disk.
|
204 |
|
205 |
:param output_path:
|
@@ -248,7 +253,7 @@ class Stream(object):
|
|
248 |
self.on_complete(fh)
|
249 |
return fp
|
250 |
|
251 |
-
def stream_to_buffer(self):
|
252 |
"""Write the media stream to buffer
|
253 |
|
254 |
:rtype: io.BytesIO buffer
|
|
|
83 |
# streams return NoneType for audio/video depending.
|
84 |
self.video_codec, self.audio_codec = self.parse_codecs()
|
85 |
|
86 |
+
def set_attributes_from_dict(self, dct: Dict) -> None:
|
87 |
"""Set class attributes from dictionary items.
|
88 |
|
89 |
:rtype: None
|
|
|
199 |
filename = safe_filename(self.title)
|
200 |
return "{filename}.{s.subtype}".format(filename=filename, s=self)
|
201 |
|
202 |
+
def download(
|
203 |
+
self,
|
204 |
+
output_path: Optional[str] = None,
|
205 |
+
filename: Optional[str] = None,
|
206 |
+
filename_prefix: Optional[str] = None,
|
207 |
+
) -> str:
|
208 |
"""Write the media stream to disk.
|
209 |
|
210 |
:param output_path:
|
|
|
253 |
self.on_complete(fh)
|
254 |
return fp
|
255 |
|
256 |
+
def stream_to_buffer(self) -> io.BytesIO:
|
257 |
"""Write the media stream to buffer
|
258 |
|
259 |
:rtype: io.BytesIO buffer
|