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