yvainyork commited on
Commit
5340b13
·
unverified ·
1 Parent(s): ba89f97

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

Files changed (6) hide show
  1. README.md +33 -6
  2. docs/index.rst +1 -0
  3. docs/user/cli.rst +54 -0
  4. pytube/cli.py +49 -9
  5. pytube/helpers.py +10 -5
  6. 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
- Download video(s) to specific directory with specific name
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
- pytube also ships with a tiny cli interface for downloading and probing videos.
247
 
248
- Let's start with downloading:
249
 
250
  ```bash
251
- $ pytube http://youtube.com/watch?v=2lAe1cqCOXo --itag=22
252
  ```
 
253
  To view available streams:
254
 
255
  ```bash
256
- $ pytube http://youtube.com/watch?v=2lAe1cqCOXo --list
 
 
 
 
 
 
 
 
 
 
 
 
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
- setup_logger(logging.FATAL - log_level)
 
 
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 hasattr(args, "caption_code"):
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()