Coverage for torrentfile\cli.py: 100%
122 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-27 21:50 -0700
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-27 21:50 -0700
1#! /usr/bin/python3
2# -*- coding: utf-8 -*-
4##############################################################################
5# Copyright (C) 2021-current alexpdev
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18##############################################################################
19"""
20Command Line Interface for TorrentFile project.
22This module provides the primary command line argument parser for
23the torrentfile package. The main_script function is automatically
24invoked when called from command line, and parses accompanying arguments.
26Functions
27---------
28main_script :
29 process command line arguments and run program.
30activate_logger :
31 turns on debug mode and logging facility.
33Classes
34-------
35Config : class
36 controls logging configuration
37TorrentFileHelpFormatter : HelpFormatter
38 the command line help message formatter
39"""
41import io
42import sys
43import logging
44from argparse import ArgumentParser, HelpFormatter
46from torrentfile import commands
47from torrentfile.utils import toggle_debug_mode
48from torrentfile.version import __version__ as version
51class Config:
52 """
53 Class the controls the logging configuration and output settings.
55 Controls the logging level, or whether to app should operate in quiet mode.
56 """
58 @staticmethod
59 def activate_quiet():
60 """
61 Activate quiet mode for the duration of the programs life.
63 When quiet mode is enabled, no logging, progress or state information
64 is output to the terminal
65 """
66 if sys.stdout or sys.stderr:
67 sys.stdout = io.StringIO()
68 sys.stderr = io.StringIO()
70 @staticmethod
71 def activate_logger():
72 """
73 Activate the builtin logging mechanism when passed debug flag from CLI.
74 """
75 logging.basicConfig(level=logging.WARNING)
76 logger = logging.getLogger()
77 console_handler = logging.StreamHandler(stream=sys.stderr)
78 stream_formatter = logging.Formatter(
79 "[%(asctime)s] [%(levelno)s] %(message)s",
80 datefmt="%H:%M:%S",
81 style="%",
82 )
83 console_handler.setFormatter(stream_formatter)
84 console_handler.setLevel(logging.DEBUG)
85 logger.setLevel(logging.DEBUG)
86 logger.addHandler(console_handler)
87 logger.debug("Debug: ON")
88 toggle_debug_mode(True)
91class TorrentFileHelpFormatter(HelpFormatter):
92 """
93 Formatting class for help tips provided by the CLI.
95 Subclasses Argparse.HelpFormatter.
96 """
98 def __init__(self,
99 prog: str,
100 width: int = 45,
101 max_help_positions: int = 45):
102 """
103 Construct HelpFormat class for usage output.
105 Parameters
106 ----------
107 prog : str
108 Name of the program.
109 width : int
110 Max width of help message output.
111 max_help_positions : int
112 max length until line wrap.
113 """
114 super().__init__(prog,
115 width=width,
116 max_help_position=max_help_positions)
118 def _split_lines(self, text: str, _: int) -> list:
119 """
120 Split multiline help messages and remove indentation.
122 Parameters
123 ----------
124 text : str
125 text that needs to be split
126 _ : int
127 max width for line.
129 Returns
130 -------
131 list :
132 split into multiline text list
133 """
134 lines = text.split("\n")
135 return [line.strip() for line in lines if line]
137 def _format_text(self, text: str) -> str:
138 """
139 Format text for cli usage messages.
141 Parameters
142 ----------
143 text : str
144 Pre-formatted text.
146 Returns
147 -------
148 str
149 Formatted text from input.
150 """
151 text = text % {"prog": self._prog} if "%(prog)" in text else text
152 text = self._whitespace_matcher.sub(" ", text).strip()
153 return text + "\n\n"
155 def _join_parts(self, part_strings: list) -> str:
156 """
157 Combine different sections of the help message.
159 Parameters
160 ----------
161 part_strings : list
162 List of argument help messages and headers.
164 Returns
165 -------
166 str
167 Fully formatted help message for CLI.
168 """
169 parts = self._format_headers(part_strings)
170 return super()._join_parts(parts)
172 @staticmethod
173 def _format_headers(parts: list) -> list:
174 """
175 Format help message section headers.
177 Parameters
178 ----------
179 parts : list
180 List of individual lines for help message.
182 Returns
183 -------
184 list
185 Input list with formatted section headers.
186 """
187 if parts and parts[0].startswith("usage:"):
188 parts[0] = "Usage\n=====\n " + parts[0][6:]
189 headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
190 for i in headings[::-1]:
191 parts[i] = parts[i][:-2].title()
192 underline = "".join(["\n", "-" * len(parts[i]), "\n"])
193 parts.insert(i + 1, underline)
194 return parts
197def execute(args: list = None) -> list:
198 """
199 Execute program with provided list of arguments.
201 If no arguments are given then it defaults to using
202 sys.argv. This is the main entrypoint for the program
203 and command line interface.
205 Parameters
206 ----------
207 args : list
208 Commandline arguments. default=None
210 Returns
211 -------
212 list
213 Depends on what the command line args were.
214 """
215 toggle_debug_mode(False)
216 if not args:
217 if sys.argv[1:]:
218 args = sys.argv[1:]
219 else:
220 args = ["-h"]
222 parser = ArgumentParser(
223 "torrentfile",
224 usage="torrentfile <options>",
225 description=(
226 "Command line tool for creating, editing, validating, building "
227 "and interacting with all versions of Bittorrent files"),
228 prefix_chars="-",
229 formatter_class=TorrentFileHelpFormatter,
230 conflict_handler="resolve",
231 )
233 parser.add_argument(
234 "-q",
235 "--quiet",
236 help="Turn off all text output.",
237 dest="quiet",
238 action="store_true",
239 )
241 parser.add_argument(
242 "-V",
243 "--version",
244 action="version",
245 version=f"torrentfile v{version}",
246 help="show program version and exit",
247 )
249 parser.add_argument(
250 "-v",
251 "--verbose",
252 action="store_true",
253 dest="debug",
254 help="output debug information",
255 )
257 parser.set_defaults(func=parser.print_help)
259 subparsers = parser.add_subparsers(
260 title="Commands",
261 dest="command",
262 metavar="create, edit, info, magnet, recheck, rebuild, rename\n",
263 )
265 create_parser = subparsers.add_parser(
266 "create",
267 help="Create a new Bittorrent file.",
268 prefix_chars="-",
269 aliases=["new"],
270 formatter_class=TorrentFileHelpFormatter,
271 )
273 create_parser.add_argument(
274 "-a",
275 "--announce",
276 "--tracker",
277 action="store",
278 dest="announce",
279 metavar="<url>",
280 nargs="+",
281 default=[],
282 help="one or more space-seperated tracker url(s)",
283 )
285 create_parser.add_argument(
286 "-p",
287 "--private",
288 action="store_true",
289 dest="private",
290 help="create private torrent",
291 )
293 create_parser.add_argument(
294 "-s",
295 "--source",
296 action="store",
297 dest="source",
298 metavar="<source>",
299 help="add source field to the metadata",
300 )
302 create_parser.add_argument(
303 "--config",
304 action="store_true",
305 dest="config",
306 help="""
307 Parse torrent information from a config file. Looks in the current
308 working directory, or the directory named .torrentfile in the users
309 home directory for a torrentfile.ini file. See --config-path option.
310 """,
311 )
313 create_parser.add_argument(
314 "--config-path",
315 action="store",
316 metavar="<path>",
317 dest="config_path",
318 help="use in combination with --config to provide config file path",
319 )
321 create_parser.add_argument(
322 "-m",
323 "--magnet",
324 action="store_true",
325 dest="magnet",
326 )
328 create_parser.add_argument(
329 "-c",
330 "--comment",
331 action="store",
332 dest="comment",
333 metavar="<comment>",
334 help="include a comment in the torrent file metadata",
335 )
337 create_parser.add_argument(
338 "-o",
339 "--out",
340 action="store",
341 dest="outfile",
342 metavar="<path>",
343 help="path to write torrent file",
344 )
346 create_parser.add_argument(
347 "--prog",
348 "--progress",
349 default="1",
350 action="store",
351 dest="progress",
352 metavar="<int>",
353 help="""
354 set the progress bar level
355 Options = 0, 1, 2
356 (0) = Do not display progress bar.
357 (1) = Display progress bar for each file.(default)
358 (2) = Display one progress bar for full torrent.
359 """,
360 )
362 create_parser.add_argument(
363 "--meta-version",
364 default="1",
365 choices=["1", "2", "3"],
366 action="store",
367 dest="meta_version",
368 metavar="<int>",
369 help="""
370 bittorrent metafile version
371 options = 1, 2, 3
372 (1) = Bittorrent v1 (Default)
373 (2) = Bittorrent v2
374 (3) = Bittorrent v1 & v2 hybrid
375 """,
376 )
378 create_parser.add_argument(
379 "--piece-length",
380 action="store",
381 dest="piece_length",
382 metavar="<int>",
383 help="""
384 (Default: auto calculated based on total size of content)
385 acceptable values include numbers 14-26
386 14 = 16KiB, 20 = 1MiB, 21 = 2MiB etc. Examples:[--piece-length 14]
387 """,
388 )
390 create_parser.add_argument(
391 "--web-seed",
392 action="store",
393 dest="url_list",
394 metavar="<url>",
395 nargs="+",
396 help="list of web addresses where torrent data exists (GetRight)",
397 )
399 create_parser.add_argument(
400 "--http-seed",
401 action="store",
402 dest="httpseeds",
403 metavar="<url>",
404 nargs="+",
405 help="list of URLs, addresses where content can be found (Hoffman)",
406 )
408 create_parser.add_argument(
409 "--align",
410 action="store_true",
411 help=("Align pieces to file boundaries. "
412 "This option is ignored when not used with V1 torrents."),
413 )
415 create_parser.add_argument(
416 "content",
417 action="store",
418 metavar="<content>",
419 nargs="?",
420 help="path to content file or directory",
421 )
423 create_parser.set_defaults(func=commands.create)
425 edit_parser = subparsers.add_parser(
426 "edit",
427 help="edit torrent file",
428 prefix_chars="-",
429 formatter_class=TorrentFileHelpFormatter,
430 )
432 edit_parser.add_argument(
433 "metafile",
434 action="store",
435 help="path to *.torrent file",
436 metavar="<*.torrent>",
437 )
439 edit_parser.add_argument(
440 "--tracker",
441 action="store",
442 dest="announce",
443 metavar="<url>",
444 nargs="+",
445 help="replace current trackers with one or more urls",
446 )
448 edit_parser.add_argument(
449 "--web-seed",
450 action="store",
451 dest="url_list",
452 metavar="<url>",
453 nargs="+",
454 help="replace current web-seed with one or more url(s)",
455 )
457 edit_parser.add_argument(
458 "--http-seed",
459 action="store",
460 dest="httpseeds",
461 metavar="<url>",
462 nargs="+",
463 help="replace current http-seed urls with new ones (Hoffman)",
464 )
466 edit_parser.add_argument(
467 "--private",
468 action="store_true",
469 help="make torrent private",
470 dest="private",
471 )
473 edit_parser.add_argument(
474 "--comment",
475 help="replaces any existing comment with <comment>",
476 metavar="<comment>",
477 dest="comment",
478 action="store",
479 )
481 edit_parser.add_argument(
482 "--source",
483 action="store",
484 dest="source",
485 metavar="<source>",
486 help="replaces current source with <source>",
487 )
489 edit_parser.set_defaults(func=commands.edit)
491 info_parser = subparsers.add_parser(
492 "info",
493 help="show detailed information about a torrent file",
494 prefix_chars="-",
495 formatter_class=TorrentFileHelpFormatter,
496 )
498 info_parser.add_argument(
499 "metafile",
500 action="store",
501 metavar="<*.torrent>",
502 help="path to torrent file",
503 )
505 info_parser.set_defaults(func=commands.info)
507 magnet_parser = subparsers.add_parser(
508 "magnet",
509 help="generate magnet url from an existing torrent file",
510 aliases=["m"],
511 prefix_chars="-",
512 formatter_class=TorrentFileHelpFormatter,
513 )
515 magnet_parser.add_argument(
516 "metafile",
517 action="store",
518 help="path to torrent file",
519 metavar="<*.torrent>",
520 )
522 magnet_parser.add_argument(
523 "--meta-version",
524 action="store",
525 choices=["0", "1", "2", "3"],
526 default="0",
527 help="""
528 This option is only relevant for hybrid torrent files.
529 Options = 0, 1, 2, 3
530 (0) = [default] version is determined automatically
531 (1) = create V1 magnet link only
532 (2) = create V2 magnet link only
533 (3) = create a hybrid magnet link
534 """,
535 dest="meta_version",
536 metavar="<int>",
537 )
539 magnet_parser.set_defaults(func=commands.get_magnet)
541 check_parser = subparsers.add_parser(
542 "recheck",
543 help="gives a detailed look at how much of the torrent is available",
544 aliases=["check"],
545 prefix_chars="-",
546 formatter_class=TorrentFileHelpFormatter,
547 )
549 check_parser.add_argument(
550 "metafile",
551 action="store",
552 metavar="<*.torrent>",
553 help="path to .torrent file.",
554 )
556 check_parser.add_argument(
557 "content",
558 action="store",
559 metavar="<content>",
560 help="path to content file or directory",
561 )
563 check_parser.set_defaults(func=commands.recheck)
565 rebuild_parser = subparsers.add_parser(
566 "rebuild",
567 help="""
568 Re-assemble files obtained from a bittorrent file into the
569 appropriate file structure for re-seeding. Read documentation
570 for more information, or use cases.
571 """,
572 formatter_class=TorrentFileHelpFormatter,
573 )
575 rebuild_parser.add_argument(
576 "-m",
577 "--metafiles",
578 action="store",
579 metavar="<*.torrent>",
580 nargs="+",
581 dest="metafiles",
582 required=True,
583 help="path(s) to .torrent file(s)/folder(s) containing .torrent files",
584 )
586 rebuild_parser.add_argument(
587 "-c"
588 "--contents",
589 action="store",
590 dest="contents",
591 nargs="+",
592 required=True,
593 metavar="<contents>",
594 help="folders that might contain the source contents needed to rebuld",
595 )
597 rebuild_parser.add_argument(
598 "-d",
599 "--destination",
600 action="store",
601 dest="destination",
602 required=True,
603 metavar="<destination>",
604 help="path to where torrents will be re-assembled",
605 )
607 rebuild_parser.set_defaults(func=commands.rebuild)
609 rename_parser = subparsers.add_parser(
610 "rename",
611 help="""Rename a torrent file to it's original name provided in the
612 metadata/the same name you see in your torrent client.""",
613 formatter_class=TorrentFileHelpFormatter,
614 )
616 rename_parser.add_argument(
617 "target",
618 action="store",
619 metavar="<target>",
620 help="path to torrent file",
621 )
623 rename_parser.set_defaults(func=commands.rename)
625 all_commands = [
626 "m",
627 "-h",
628 "-V",
629 "new",
630 "edit",
631 "info",
632 "check",
633 "create",
634 "magnet",
635 "rename",
636 "rebuild",
637 "recheck",
638 ]
639 if not any(i for i in all_commands if i in args):
640 start = 0
641 while args[start] in ["-v", "-q"]:
642 start += 1
643 args.insert(start, "create")
645 args = parser.parse_args(args)
647 if args.quiet:
648 Config.activate_quiet()
650 elif args.debug:
651 Config.activate_logger()
653 if hasattr(args, "func"):
654 return args.func(args)
655 return args # pragma: nocover
658main_script = execute
661def main() -> None:
662 """
663 Initiate main function for CLI script.
664 """
665 execute()