yvainyork
commited on
Feature: cli: default download; improved REAME and documentation (#902)
Browse files* Additional CLI documentation in README
* CLI page for readthedocs
* Added default download behavior to download highest resolution progressive stream
* Added `-lc` flag to list available caption codes
* Adds CLI argument for logfile output
* `setup_logger` can now take an argument for file output
- README.md +33 -6
- docs/index.rst +1 -0
- docs/user/cli.rst +54 -0
- pytube/cli.py +49 -9
- pytube/helpers.py +10 -5
- tests/test_cli.py +2 -2
README.md
CHANGED
@@ -234,26 +234,53 @@ Similarly, if your application requires on-download progress logic, pytube expos
|
|
234 |
>>> yt.register_on_progress_callback(show_progress_bar)
|
235 |
```
|
236 |
|
237 |
-
|
238 |
|
239 |
```python
|
240 |
>>> yt = YouTube('https://youtube.com/watch?v=2lAe1cqCOXo')
|
241 |
>>> yt.streams.first().download(output_path="/tmp" ,filename='output')
|
242 |
```
|
243 |
|
244 |
-
## Command-line interface
|
245 |
|
246 |
-
|
247 |
|
248 |
-
|
249 |
|
250 |
```bash
|
251 |
-
$ pytube
|
252 |
```
|
|
|
253 |
To view available streams:
|
254 |
|
255 |
```bash
|
256 |
-
$ pytube
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
```
|
258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
259 |
Finally, if you're filing a bug report, the cli contains a switch called ``--build-playback-report``, which bundles up the state, allowing others to easily replay your issue.
|
|
|
234 |
>>> yt.register_on_progress_callback(show_progress_bar)
|
235 |
```
|
236 |
|
237 |
+
You can also download videos to a specific directory with specific filename:
|
238 |
|
239 |
```python
|
240 |
>>> yt = YouTube('https://youtube.com/watch?v=2lAe1cqCOXo')
|
241 |
>>> yt.streams.first().download(output_path="/tmp" ,filename='output')
|
242 |
```
|
243 |
|
244 |
+
## Command-line interface (CLI)
|
245 |
|
246 |
+
Pytube also ships with a tiny CLI for interacting with videos and playlists.
|
247 |
|
248 |
+
To download the highest resolution progressive stream:
|
249 |
|
250 |
```bash
|
251 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo
|
252 |
```
|
253 |
+
|
254 |
To view available streams:
|
255 |
|
256 |
```bash
|
257 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo --list
|
258 |
+
```
|
259 |
+
|
260 |
+
To download a specific stream, use the itag
|
261 |
+
|
262 |
+
```bash
|
263 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo --itag=22
|
264 |
+
```
|
265 |
+
|
266 |
+
To get a list of all subtitles (caption codes)
|
267 |
+
|
268 |
+
```bash
|
269 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo --list-captions
|
270 |
```
|
271 |
|
272 |
+
To download a specific subtitle (caption code) - in this case the
|
273 |
+
english subtitles (in srt format) - use:
|
274 |
+
|
275 |
+
```bash
|
276 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo -c en
|
277 |
+
```
|
278 |
+
|
279 |
+
It is also possible to just download the audio stream (default AAC/mp4):
|
280 |
+
|
281 |
+
```bash
|
282 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo -a
|
283 |
+
```
|
284 |
+
|
285 |
+
|
286 |
Finally, if you're filing a bug report, the cli contains a switch called ``--build-playback-report``, which bundles up the state, allowing others to easily replay your issue.
|
docs/index.rst
CHANGED
@@ -64,6 +64,7 @@ This part of the documentation begins with some background information about the
|
|
64 |
user/install
|
65 |
user/quickstart
|
66 |
user/playlist
|
|
|
67 |
|
68 |
The API Documentation / Guide
|
69 |
-----------------------------
|
|
|
64 |
user/install
|
65 |
user/quickstart
|
66 |
user/playlist
|
67 |
+
user/cli
|
68 |
|
69 |
The API Documentation / Guide
|
70 |
-----------------------------
|
docs/user/cli.rst
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.. _cli:
|
2 |
+
|
3 |
+
Command-line interface (CLI)
|
4 |
+
=============================
|
5 |
+
|
6 |
+
Pytube also ships with a tiny CLI for interacting with videos and playlists.
|
7 |
+
|
8 |
+
To download the highest resolution progressive stream:
|
9 |
+
|
10 |
+
.. code:: bash
|
11 |
+
|
12 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo
|
13 |
+
|
14 |
+
To view available streams:
|
15 |
+
|
16 |
+
.. code:: bash
|
17 |
+
|
18 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo --list
|
19 |
+
|
20 |
+
To download a specific stream, use the itag
|
21 |
+
|
22 |
+
.. code:: bash
|
23 |
+
|
24 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo --itag=22
|
25 |
+
|
26 |
+
To get a list of all subtitles (caption codes)
|
27 |
+
|
28 |
+
.. code:: bash
|
29 |
+
|
30 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo --list-captions
|
31 |
+
|
32 |
+
To download a specific subtitle (caption code) - in this case the
|
33 |
+
english subtitles (in srt format) - use:
|
34 |
+
|
35 |
+
.. code:: bash
|
36 |
+
|
37 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo -c en
|
38 |
+
|
39 |
+
It is also possible to just download the audio stream (default AAC/mp4):
|
40 |
+
|
41 |
+
.. code:: bash
|
42 |
+
|
43 |
+
$ pytube https://www.youtube.com/watch?v=2lAe1cqCOXo -a
|
44 |
+
|
45 |
+
To list all command line options, simply type
|
46 |
+
|
47 |
+
.. code:: bash
|
48 |
+
|
49 |
+
$ pytube --help
|
50 |
+
|
51 |
+
|
52 |
+
Finally, if you're filing a bug report, the cli contains a switch called
|
53 |
+
``--build-playback-report``, which bundles up the state, allowing others
|
54 |
+
to easily replay your issue.
|
pytube/cli.py
CHANGED
@@ -18,7 +18,7 @@ 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 |
|
@@ -29,8 +29,11 @@ def main():
|
|
29 |
parser = argparse.ArgumentParser(description=main.__doc__)
|
30 |
args = _parse_args(parser)
|
31 |
if args.verbosity:
|
|
|
32 |
log_level = min(args.verbosity, 4) * 10
|
33 |
-
|
|
|
|
|
34 |
|
35 |
if not args.url or "youtu" not in args.url:
|
36 |
parser.print_help()
|
@@ -56,13 +59,19 @@ def main():
|
|
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:
|
62 |
build_playback_report(youtube)
|
63 |
if args.itag:
|
64 |
download_by_itag(youtube=youtube, itag=args.itag, target=args.target)
|
65 |
-
if
|
66 |
download_caption(
|
67 |
youtube=youtube, lang_code=args.caption_code, target=args.target
|
68 |
)
|
@@ -115,6 +124,11 @@ def _parse_args(
|
|
115 |
dest="verbosity",
|
116 |
help="Verbosity level, use up to 4 to increase logging -vvvv",
|
117 |
)
|
|
|
|
|
|
|
|
|
|
|
118 |
parser.add_argument(
|
119 |
"--build-playback-report",
|
120 |
action="store_true",
|
@@ -124,13 +138,19 @@ def _parse_args(
|
|
124 |
"-c",
|
125 |
"--caption-code",
|
126 |
type=str,
|
127 |
-
default=argparse.SUPPRESS,
|
128 |
-
nargs="?",
|
129 |
help=(
|
130 |
"Download srt captions for given language code. "
|
131 |
"Prints available language codes if no argument given"
|
132 |
),
|
133 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
parser.add_argument(
|
135 |
"-t",
|
136 |
"--target",
|
@@ -441,6 +461,30 @@ def download_by_resolution(
|
|
441 |
sys.exit()
|
442 |
|
443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
444 |
def display_streams(youtube: YouTube) -> None:
|
445 |
"""Probe YouTube video and lists its available formats.
|
446 |
|
@@ -472,10 +516,6 @@ def download_caption(
|
|
472 |
:param str target:
|
473 |
Target directory for download
|
474 |
"""
|
475 |
-
if lang_code is None:
|
476 |
-
_print_available_captions(youtube.captions)
|
477 |
-
return
|
478 |
-
|
479 |
try:
|
480 |
caption = youtube.captions[lang_code]
|
481 |
downloaded_path = caption.download(
|
|
|
18 |
from pytube import Playlist
|
19 |
from pytube import Stream
|
20 |
from pytube import YouTube
|
21 |
+
from pytube.exceptions import PytubeError, VideoUnavailable
|
22 |
from pytube.helpers import safe_filename
|
23 |
from pytube.helpers import setup_logger
|
24 |
|
|
|
29 |
parser = argparse.ArgumentParser(description=main.__doc__)
|
30 |
args = _parse_args(parser)
|
31 |
if args.verbosity:
|
32 |
+
log_filename = None
|
33 |
log_level = min(args.verbosity, 4) * 10
|
34 |
+
if args.logfile:
|
35 |
+
log_filename = args.logfile
|
36 |
+
setup_logger(logging.FATAL - log_level, log_filename=log_filename)
|
37 |
|
38 |
if not args.url or "youtu" not in args.url:
|
39 |
parser.print_help()
|
|
|
59 |
def _perform_args_on_youtube(
|
60 |
youtube: YouTube, args: argparse.Namespace
|
61 |
) -> None:
|
62 |
+
if len(sys.argv) == 2 : # no arguments parsed
|
63 |
+
download_highest_resolution_progressive(
|
64 |
+
youtube=youtube, resolution="highest", target=args.target
|
65 |
+
)
|
66 |
+
if args.list_captions:
|
67 |
+
_print_available_captions(youtube.captions)
|
68 |
if args.list:
|
69 |
display_streams(youtube)
|
70 |
if args.build_playback_report:
|
71 |
build_playback_report(youtube)
|
72 |
if args.itag:
|
73 |
download_by_itag(youtube=youtube, itag=args.itag, target=args.target)
|
74 |
+
if args.caption_code:
|
75 |
download_caption(
|
76 |
youtube=youtube, lang_code=args.caption_code, target=args.target
|
77 |
)
|
|
|
124 |
dest="verbosity",
|
125 |
help="Verbosity level, use up to 4 to increase logging -vvvv",
|
126 |
)
|
127 |
+
parser.add_argument(
|
128 |
+
"--logfile",
|
129 |
+
action="store",
|
130 |
+
help="logging debug and error messages into a log file",
|
131 |
+
)
|
132 |
parser.add_argument(
|
133 |
"--build-playback-report",
|
134 |
action="store_true",
|
|
|
138 |
"-c",
|
139 |
"--caption-code",
|
140 |
type=str,
|
|
|
|
|
141 |
help=(
|
142 |
"Download srt captions for given language code. "
|
143 |
"Prints available language codes if no argument given"
|
144 |
),
|
145 |
)
|
146 |
+
parser.add_argument(
|
147 |
+
'-lc',
|
148 |
+
'--list-captions',
|
149 |
+
action='store_true',
|
150 |
+
help=(
|
151 |
+
"List available caption codes for a video"
|
152 |
+
)
|
153 |
+
)
|
154 |
parser.add_argument(
|
155 |
"-t",
|
156 |
"--target",
|
|
|
461 |
sys.exit()
|
462 |
|
463 |
|
464 |
+
def download_highest_resolution_progressive(
|
465 |
+
youtube: YouTube, resolution: str, target: Optional[str] = None
|
466 |
+
) -> None:
|
467 |
+
"""Start downloading the highest resolution progressive stream.
|
468 |
+
|
469 |
+
:param YouTube youtube:
|
470 |
+
A valid YouTube object.
|
471 |
+
:param str resolution:
|
472 |
+
YouTube video resolution.
|
473 |
+
:param str target:
|
474 |
+
Target directory for download
|
475 |
+
"""
|
476 |
+
youtube.register_on_progress_callback(on_progress)
|
477 |
+
try:
|
478 |
+
stream = youtube.streams.get_highest_resolution()
|
479 |
+
except VideoUnavailable as err:
|
480 |
+
print(f"No video streams available: {err}")
|
481 |
+
else:
|
482 |
+
try:
|
483 |
+
_download(stream, target=target)
|
484 |
+
except KeyboardInterrupt:
|
485 |
+
sys.exit()
|
486 |
+
|
487 |
+
|
488 |
def display_streams(youtube: YouTube) -> None:
|
489 |
"""Probe YouTube video and lists its available formats.
|
490 |
|
|
|
516 |
:param str target:
|
517 |
Target directory for download
|
518 |
"""
|
|
|
|
|
|
|
|
|
519 |
try:
|
520 |
caption = youtube.captions[lang_code]
|
521 |
downloaded_path = caption.download(
|
pytube/helpers.py
CHANGED
@@ -88,7 +88,7 @@ def safe_filename(s: str, max_length: int = 255) -> str:
|
|
88 |
return filename[:max_length].rsplit(" ", 0)[0]
|
89 |
|
90 |
|
91 |
-
def setup_logger(level: int = logging.ERROR):
|
92 |
"""Create a configured instance of logger.
|
93 |
|
94 |
:param int level:
|
@@ -98,14 +98,19 @@ def setup_logger(level: int = logging.ERROR):
|
|
98 |
date_fmt = "%H:%M:%S"
|
99 |
formatter = logging.Formatter(fmt, datefmt=date_fmt)
|
100 |
|
101 |
-
handler = logging.StreamHandler()
|
102 |
-
handler.setFormatter(formatter)
|
103 |
-
|
104 |
# https://github.com/nficano/pytube/issues/163
|
105 |
logger = logging.getLogger("pytube")
|
106 |
-
logger.addHandler(handler)
|
107 |
logger.setLevel(level)
|
108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
|
110 |
GenericType = TypeVar("GenericType")
|
111 |
|
|
|
88 |
return filename[:max_length].rsplit(" ", 0)[0]
|
89 |
|
90 |
|
91 |
+
def setup_logger(level: int = logging.ERROR, log_filename: Optional[str] = None) -> None:
|
92 |
"""Create a configured instance of logger.
|
93 |
|
94 |
:param int level:
|
|
|
98 |
date_fmt = "%H:%M:%S"
|
99 |
formatter = logging.Formatter(fmt, datefmt=date_fmt)
|
100 |
|
|
|
|
|
|
|
101 |
# https://github.com/nficano/pytube/issues/163
|
102 |
logger = logging.getLogger("pytube")
|
|
|
103 |
logger.setLevel(level)
|
104 |
|
105 |
+
stream_handler = logging.StreamHandler()
|
106 |
+
stream_handler.setFormatter(formatter)
|
107 |
+
logger.addHandler(stream_handler)
|
108 |
+
|
109 |
+
if log_filename is not None:
|
110 |
+
file_handler = logging.FileHandler(log_filename)
|
111 |
+
file_handler.setFormatter(formatter)
|
112 |
+
logger.addHandler(file_handler)
|
113 |
+
|
114 |
|
115 |
GenericType = TypeVar("GenericType")
|
116 |
|
tests/test_cli.py
CHANGED
@@ -178,7 +178,7 @@ def test_main_logging_setup(setup_logger):
|
|
178 |
with pytest.raises(SystemExit):
|
179 |
cli.main()
|
180 |
# Then
|
181 |
-
setup_logger.assert_called_with(40)
|
182 |
|
183 |
|
184 |
@mock.patch("pytube.cli.YouTube", return_value=None)
|
@@ -222,7 +222,7 @@ def test_main_display_streams(youtube):
|
|
222 |
@mock.patch("pytube.cli.YouTube", return_value=None)
|
223 |
def test_main_download_caption(youtube):
|
224 |
parser = argparse.ArgumentParser()
|
225 |
-
args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-c"])
|
226 |
cli._parse_args = MagicMock(return_value=args)
|
227 |
cli.download_caption = MagicMock()
|
228 |
cli.main()
|
|
|
178 |
with pytest.raises(SystemExit):
|
179 |
cli.main()
|
180 |
# Then
|
181 |
+
setup_logger.assert_called_with(40, log_filename=None)
|
182 |
|
183 |
|
184 |
@mock.patch("pytube.cli.YouTube", return_value=None)
|
|
|
222 |
@mock.patch("pytube.cli.YouTube", return_value=None)
|
223 |
def test_main_download_caption(youtube):
|
224 |
parser = argparse.ArgumentParser()
|
225 |
+
args = parse_args(parser, ["http://youtube.com/watch?v=9bZkp7q19f0", "-c", "en"])
|
226 |
cli._parse_args = MagicMock(return_value=args)
|
227 |
cli.download_caption = MagicMock()
|
228 |
cli.main()
|