fixed cli logging
Browse files- pytube/__main__.py +9 -5
- pytube/cli.py +31 -14
- pytube/helpers.py +1 -1
- pytube/logging.py +1 -1
- pytube/mixins.py +2 -2
- pytube/query.py +32 -0
- pytube/streams.py +17 -8
- tests/conftest.py +1 -1
- tests/mocks/yt-video-9bZkp7q19f0.json +0 -0
pytube/__main__.py
CHANGED
@@ -29,7 +29,7 @@ class YouTube(object):
|
|
29 |
"""Core developer interface for pytube."""
|
30 |
|
31 |
def __init__(
|
32 |
-
self, url=None,
|
33 |
on_complete_callback=None,
|
34 |
):
|
35 |
"""Construct a :class:`YouTube <YouTube>`.
|
@@ -74,11 +74,16 @@ class YouTube(object):
|
|
74 |
'on_complete': on_complete_callback,
|
75 |
}
|
76 |
|
77 |
-
if url and not
|
78 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
def init(self):
|
81 |
-
"""
|
82 |
|
83 |
The initialization process takes advantage of Python's
|
84 |
"call-by-reference evaluation," which allows dictionary transforms to
|
@@ -86,7 +91,6 @@ class YouTube(object):
|
|
86 |
interstitial step.
|
87 |
"""
|
88 |
logger.info('init started')
|
89 |
-
self.prefetch()
|
90 |
|
91 |
self.vid_info = {k: v for k, v in parse_qsl(self.vid_info)}
|
92 |
self.player_config = extract.get_ytplayer_config(self.watch_html)
|
|
|
29 |
"""Core developer interface for pytube."""
|
30 |
|
31 |
def __init__(
|
32 |
+
self, url=None, defer_prefetch_init=False, on_progress_callback=None,
|
33 |
on_complete_callback=None,
|
34 |
):
|
35 |
"""Construct a :class:`YouTube <YouTube>`.
|
|
|
74 |
'on_complete': on_complete_callback,
|
75 |
}
|
76 |
|
77 |
+
if url and not defer_prefetch_init:
|
78 |
+
self.prefetch_init()
|
79 |
+
|
80 |
+
def prefetch_init(self):
|
81 |
+
"""Download data, descramble it, and build Stream instances."""
|
82 |
+
self.prefetch()
|
83 |
+
self.init()
|
84 |
|
85 |
def init(self):
|
86 |
+
"""descramble the stream data and build Stream instances.
|
87 |
|
88 |
The initialization process takes advantage of Python's
|
89 |
"call-by-reference evaluation," which allows dictionary transforms to
|
|
|
91 |
interstitial step.
|
92 |
"""
|
93 |
logger.info('init started')
|
|
|
94 |
|
95 |
self.vid_info = {k: v for k, v in parse_qsl(self.vid_info)}
|
96 |
self.player_config = extract.get_ytplayer_config(self.watch_html)
|
pytube/cli.py
CHANGED
@@ -4,12 +4,15 @@ from __future__ import absolute_import
|
|
4 |
from __future__ import print_function
|
5 |
|
6 |
import argparse
|
|
|
|
|
7 |
import json
|
8 |
import logging
|
9 |
import os
|
10 |
import sys
|
11 |
|
12 |
from pytube import __version__
|
|
|
13 |
from pytube import YouTube
|
14 |
|
15 |
|
@@ -21,7 +24,7 @@ def main():
|
|
21 |
parser = argparse.ArgumentParser(description=main.__doc__)
|
22 |
parser.add_argument('url', help='The YouTube /watch url', nargs='?')
|
23 |
parser.add_argument(
|
24 |
-
'
|
25 |
version='%(prog)s ' + __version__,
|
26 |
)
|
27 |
parser.add_argument(
|
@@ -36,45 +39,59 @@ def main():
|
|
36 |
),
|
37 |
)
|
38 |
parser.add_argument(
|
39 |
-
'-v', '--verbose', action='count', default=0, dest='
|
40 |
help='Verbosity level',
|
41 |
)
|
42 |
parser.add_argument(
|
43 |
-
'--build-
|
44 |
'Save the html and js to disk'
|
45 |
),
|
46 |
)
|
47 |
|
48 |
args = parser.parse_args()
|
49 |
-
|
|
|
50 |
if not args.url:
|
51 |
parser.print_help()
|
52 |
sys.exit(1)
|
|
|
53 |
if args.list:
|
54 |
display_streams(args.url)
|
55 |
-
|
56 |
-
|
|
|
|
|
57 |
elif args.itag:
|
58 |
download(args.url, args.itag)
|
59 |
|
60 |
|
61 |
-
def
|
62 |
"""Serialize the request data to json for offline debugging.
|
63 |
|
64 |
:param str url:
|
65 |
A valid YouTube watch URL.
|
66 |
"""
|
67 |
yt = YouTube(url)
|
|
|
68 |
fp = os.path.join(
|
69 |
os.getcwd(),
|
70 |
-
'yt-video-{yt.video_id}.json'.format(yt=yt),
|
71 |
)
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
|
80 |
def get_terminal_size():
|
|
|
4 |
from __future__ import print_function
|
5 |
|
6 |
import argparse
|
7 |
+
import datetime as dt
|
8 |
+
import gzip
|
9 |
import json
|
10 |
import logging
|
11 |
import os
|
12 |
import sys
|
13 |
|
14 |
from pytube import __version__
|
15 |
+
from pytube import request
|
16 |
from pytube import YouTube
|
17 |
|
18 |
|
|
|
24 |
parser = argparse.ArgumentParser(description=main.__doc__)
|
25 |
parser.add_argument('url', help='The YouTube /watch url', nargs='?')
|
26 |
parser.add_argument(
|
27 |
+
'--version', action='version',
|
28 |
version='%(prog)s ' + __version__,
|
29 |
)
|
30 |
parser.add_argument(
|
|
|
39 |
),
|
40 |
)
|
41 |
parser.add_argument(
|
42 |
+
'-v', '--verbose', action='count', default=0, dest='verbosity',
|
43 |
help='Verbosity level',
|
44 |
)
|
45 |
parser.add_argument(
|
46 |
+
'--build-playback-report', action='store_true', help=(
|
47 |
'Save the html and js to disk'
|
48 |
),
|
49 |
)
|
50 |
|
51 |
args = parser.parse_args()
|
52 |
+
logging.getLogger().setLevel(max(3 - args.verbosity, 0) * 10)
|
53 |
+
|
54 |
if not args.url:
|
55 |
parser.print_help()
|
56 |
sys.exit(1)
|
57 |
+
|
58 |
if args.list:
|
59 |
display_streams(args.url)
|
60 |
+
|
61 |
+
elif args.build_playback_report:
|
62 |
+
build_playback_report(args.url)
|
63 |
+
|
64 |
elif args.itag:
|
65 |
download(args.url, args.itag)
|
66 |
|
67 |
|
68 |
+
def build_playback_report(url):
|
69 |
"""Serialize the request data to json for offline debugging.
|
70 |
|
71 |
:param str url:
|
72 |
A valid YouTube watch URL.
|
73 |
"""
|
74 |
yt = YouTube(url)
|
75 |
+
ts = int(dt.datetime.utcnow().timestamp())
|
76 |
fp = os.path.join(
|
77 |
os.getcwd(),
|
78 |
+
'yt-video-{yt.video_id}-{ts}.json.tar.gz'.format(yt=yt, ts=ts),
|
79 |
)
|
80 |
+
js, watch_html, vid_info = request.get(urls=[
|
81 |
+
yt.js_url,
|
82 |
+
yt.watch_url,
|
83 |
+
yt.vid_info_url,
|
84 |
+
])
|
85 |
+
with gzip.open(fp, 'wb') as fh:
|
86 |
+
fh.write(
|
87 |
+
json.dumps({
|
88 |
+
'url': url,
|
89 |
+
'js': js,
|
90 |
+
'watch_html': watch_html,
|
91 |
+
'video_info': vid_info,
|
92 |
+
})
|
93 |
+
.encode('utf8'),
|
94 |
+
)
|
95 |
|
96 |
|
97 |
def get_terminal_size():
|
pytube/helpers.py
CHANGED
@@ -29,7 +29,7 @@ def regex_search(pattern, string, groups=False, group=None, flags=0):
|
|
29 |
results = regex.search(string)
|
30 |
logger.debug(
|
31 |
'finished regex search: %s',
|
32 |
-
pprint.
|
33 |
{
|
34 |
'pattern': pattern,
|
35 |
'results': results.group(0),
|
|
|
29 |
results = regex.search(string)
|
30 |
logger.debug(
|
31 |
'finished regex search: %s',
|
32 |
+
pprint.pformat(
|
33 |
{
|
34 |
'pattern': pattern,
|
35 |
'results': results.group(0),
|
pytube/logging.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import absolute_import
|
|
5 |
import logging
|
6 |
|
7 |
|
8 |
-
def create_logger(level=logging.
|
9 |
"""Create a configured instance of logger.
|
10 |
|
11 |
:param int level:
|
|
|
5 |
import logging
|
6 |
|
7 |
|
8 |
+
def create_logger(level=logging.ERROR):
|
9 |
"""Create a configured instance of logger.
|
10 |
|
11 |
:param int level:
|
pytube/mixins.py
CHANGED
@@ -40,7 +40,7 @@ def apply_signature(config_args, fmt, js):
|
|
40 |
|
41 |
logger.debug(
|
42 |
'finished descrambling signature for itag=%s\n%s',
|
43 |
-
stream['itag'], pprint.
|
44 |
{
|
45 |
's': stream['s'],
|
46 |
'signature': signature,
|
@@ -63,4 +63,4 @@ def apply_descrambler(stream_data, key):
|
|
63 |
{k: unquote(v) for k, v in parse_qsl(i)}
|
64 |
for i in stream_data[key].split(',')
|
65 |
]
|
66 |
-
logger.debug('applying descrambler\n%s', pprint.
|
|
|
40 |
|
41 |
logger.debug(
|
42 |
'finished descrambling signature for itag=%s\n%s',
|
43 |
+
stream['itag'], pprint.pformat(
|
44 |
{
|
45 |
's': stream['s'],
|
46 |
'signature': signature,
|
|
|
63 |
{k: unquote(v) for k, v in parse_qsl(i)}
|
64 |
for i in stream_data[key].split(',')
|
65 |
]
|
66 |
+
logger.debug('applying descrambler\n%s', pprint.pformat(stream_data[key]))
|
pytube/query.py
CHANGED
@@ -15,6 +15,8 @@ class StreamQuery:
|
|
15 |
self, fps=None, res=None, resolution=None, mime_type=None,
|
16 |
type=None, subtype=None, file_extension=None, abr=None,
|
17 |
bitrate=None, video_codec=None, audio_codec=None,
|
|
|
|
|
18 |
custom_filter_functions=None,
|
19 |
):
|
20 |
"""Apply the given filtering criterion.
|
@@ -43,6 +45,16 @@ class StreamQuery:
|
|
43 |
(optional) Digital video compression format (e.g.: vp9, mp4v.20.3).
|
44 |
:param str audio_codec:
|
45 |
(optional) Digital audio compression format (e.g.: vorbis, mp4).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
:param list custom_filter_functions:
|
47 |
(optional) Interface for defining complex filters without
|
48 |
subclassing.
|
@@ -73,6 +85,26 @@ class StreamQuery:
|
|
73 |
if audio_codec:
|
74 |
filters.append(lambda s: s.audio_codec == audio_codec)
|
75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
if custom_filter_functions:
|
77 |
for fn in custom_filter_functions:
|
78 |
filters.append(fn)
|
|
|
15 |
self, fps=None, res=None, resolution=None, mime_type=None,
|
16 |
type=None, subtype=None, file_extension=None, abr=None,
|
17 |
bitrate=None, video_codec=None, audio_codec=None,
|
18 |
+
only_audio=None, only_video=None,
|
19 |
+
progressive=None, adaptive=None,
|
20 |
custom_filter_functions=None,
|
21 |
):
|
22 |
"""Apply the given filtering criterion.
|
|
|
45 |
(optional) Digital video compression format (e.g.: vp9, mp4v.20.3).
|
46 |
:param str audio_codec:
|
47 |
(optional) Digital audio compression format (e.g.: vorbis, mp4).
|
48 |
+
:param bool progressive:
|
49 |
+
Excludes adaptive streams (one file contains both audio and video
|
50 |
+
tracks).
|
51 |
+
:param bool adaptive:
|
52 |
+
Excludes progressive streams (audio and video are on separate
|
53 |
+
tracks).
|
54 |
+
:param bool only_audio:
|
55 |
+
Excludes streams with video tracks.
|
56 |
+
:param bool only_video:
|
57 |
+
Excludes streams with audio tracks.
|
58 |
:param list custom_filter_functions:
|
59 |
(optional) Interface for defining complex filters without
|
60 |
subclassing.
|
|
|
85 |
if audio_codec:
|
86 |
filters.append(lambda s: s.audio_codec == audio_codec)
|
87 |
|
88 |
+
if only_audio:
|
89 |
+
filters.append(
|
90 |
+
lambda s: (
|
91 |
+
s.includes_audio_track and not s.includes_video_track
|
92 |
+
),
|
93 |
+
)
|
94 |
+
|
95 |
+
if only_video:
|
96 |
+
filters.append(
|
97 |
+
lambda s: (
|
98 |
+
s.includes_video_track and not s.includes_audio_track
|
99 |
+
),
|
100 |
+
)
|
101 |
+
|
102 |
+
if progressive:
|
103 |
+
filters.append(lambda s: s.is_progressive)
|
104 |
+
|
105 |
+
if adaptive:
|
106 |
+
filters.append(lambda s: s.is_progressive)
|
107 |
+
|
108 |
if custom_filter_functions:
|
109 |
for fn in custom_filter_functions:
|
110 |
filters.append(fn)
|
pytube/streams.py
CHANGED
@@ -95,13 +95,22 @@ class Stream(object):
|
|
95 |
return len(self.codecs) % 2
|
96 |
|
97 |
@property
|
98 |
-
def
|
99 |
-
"""Whether the stream
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
return self.type == 'audio'
|
101 |
|
102 |
@property
|
103 |
-
def
|
104 |
-
"""Whether the stream only contains video
|
|
|
|
|
105 |
return self.type == 'video'
|
106 |
|
107 |
def parse_codecs(self):
|
@@ -117,9 +126,9 @@ class Stream(object):
|
|
117 |
audio = None
|
118 |
if not self.is_adaptive:
|
119 |
video, audio = self.codecs
|
120 |
-
elif self.
|
121 |
video = self.codecs[0]
|
122 |
-
elif self.
|
123 |
audio = self.codecs[0]
|
124 |
return video, audio
|
125 |
|
@@ -182,7 +191,7 @@ class Stream(object):
|
|
182 |
file_handler.write(chunk)
|
183 |
logger.debug(
|
184 |
'download progress\n%s',
|
185 |
-
pprint.
|
186 |
{
|
187 |
'chunk_size': len(chunk),
|
188 |
'bytes_remaining': bytes_remaining,
|
@@ -211,7 +220,7 @@ class Stream(object):
|
|
211 |
"""Printable object representation."""
|
212 |
# TODO(nficano): this can probably be written better.
|
213 |
parts = ['itag="{s.itag}"', 'mime_type="{s.mime_type}"']
|
214 |
-
if self.
|
215 |
parts.extend(['res="{s.resolution}"', 'fps="{s.fps}fps"'])
|
216 |
if not self.is_adaptive:
|
217 |
parts.extend([
|
|
|
95 |
return len(self.codecs) % 2
|
96 |
|
97 |
@property
|
98 |
+
def is_progressive(self):
|
99 |
+
"""Whether the stream is progressive."""
|
100 |
+
return not self.is_adaptive
|
101 |
+
|
102 |
+
@property
|
103 |
+
def includes_audio_track(self):
|
104 |
+
"""Whether the stream only contains audio."""
|
105 |
+
if self.is_progressive:
|
106 |
+
return True
|
107 |
return self.type == 'audio'
|
108 |
|
109 |
@property
|
110 |
+
def includes_video_track(self):
|
111 |
+
"""Whether the stream only contains video."""
|
112 |
+
if self.is_progressive:
|
113 |
+
return True
|
114 |
return self.type == 'video'
|
115 |
|
116 |
def parse_codecs(self):
|
|
|
126 |
audio = None
|
127 |
if not self.is_adaptive:
|
128 |
video, audio = self.codecs
|
129 |
+
elif self.includes_video_track:
|
130 |
video = self.codecs[0]
|
131 |
+
elif self.includes_audio_track:
|
132 |
audio = self.codecs[0]
|
133 |
return video, audio
|
134 |
|
|
|
191 |
file_handler.write(chunk)
|
192 |
logger.debug(
|
193 |
'download progress\n%s',
|
194 |
+
pprint.pformat(
|
195 |
{
|
196 |
'chunk_size': len(chunk),
|
197 |
'bytes_remaining': bytes_remaining,
|
|
|
220 |
"""Printable object representation."""
|
221 |
# TODO(nficano): this can probably be written better.
|
222 |
parts = ['itag="{s.itag}"', 'mime_type="{s.mime_type}"']
|
223 |
+
if self.includes_video_track:
|
224 |
parts.extend(['res="{s.resolution}"', 'fps="{s.fps}fps"'])
|
225 |
if not self.is_adaptive:
|
226 |
parts.extend([
|
tests/conftest.py
CHANGED
@@ -18,7 +18,7 @@ def gangnam_style():
|
|
18 |
video = json.loads(fh.read())
|
19 |
yt = YouTube(
|
20 |
url='https://www.youtube.com/watch?v=9bZkp7q19f0',
|
21 |
-
|
22 |
)
|
23 |
yt.watch_html = video['watch_html']
|
24 |
yt.js = video['js']
|
|
|
18 |
video = json.loads(fh.read())
|
19 |
yt = YouTube(
|
20 |
url='https://www.youtube.com/watch?v=9bZkp7q19f0',
|
21 |
+
defer_prefetch_init=True,
|
22 |
)
|
23 |
yt.watch_html = video['watch_html']
|
24 |
yt.js = video['js']
|
tests/mocks/yt-video-9bZkp7q19f0.json
CHANGED
The diff for this file is too large to render.
See raw diff
|
|