Coverage for torrentfile\cli.py: 100%

122 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2023-08-27 21:50 -0700

1#! /usr/bin/python3 

2# -*- coding: utf-8 -*- 

3 

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. 

21 

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. 

25 

26Functions 

27--------- 

28main_script : 

29 process command line arguments and run program. 

30activate_logger : 

31 turns on debug mode and logging facility. 

32 

33Classes 

34------- 

35Config : class 

36 controls logging configuration 

37TorrentFileHelpFormatter : HelpFormatter 

38 the command line help message formatter 

39""" 

40 

41import io 

42import sys 

43import logging 

44from argparse import ArgumentParser, HelpFormatter 

45 

46from torrentfile import commands 

47from torrentfile.utils import toggle_debug_mode 

48from torrentfile.version import __version__ as version 

49 

50 

51class Config: 

52 """ 

53 Class the controls the logging configuration and output settings. 

54 

55 Controls the logging level, or whether to app should operate in quiet mode. 

56 """ 

57 

58 @staticmethod 

59 def activate_quiet(): 

60 """ 

61 Activate quiet mode for the duration of the programs life. 

62 

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() 

69 

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) 

89 

90 

91class TorrentFileHelpFormatter(HelpFormatter): 

92 """ 

93 Formatting class for help tips provided by the CLI. 

94 

95 Subclasses Argparse.HelpFormatter. 

96 """ 

97 

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. 

104 

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) 

117 

118 def _split_lines(self, text: str, _: int) -> list: 

119 """ 

120 Split multiline help messages and remove indentation. 

121 

122 Parameters 

123 ---------- 

124 text : str 

125 text that needs to be split 

126 _ : int 

127 max width for line. 

128 

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] 

136 

137 def _format_text(self, text: str) -> str: 

138 """ 

139 Format text for cli usage messages. 

140 

141 Parameters 

142 ---------- 

143 text : str 

144 Pre-formatted text. 

145 

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" 

154 

155 def _join_parts(self, part_strings: list) -> str: 

156 """ 

157 Combine different sections of the help message. 

158 

159 Parameters 

160 ---------- 

161 part_strings : list 

162 List of argument help messages and headers. 

163 

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) 

171 

172 @staticmethod 

173 def _format_headers(parts: list) -> list: 

174 """ 

175 Format help message section headers. 

176 

177 Parameters 

178 ---------- 

179 parts : list 

180 List of individual lines for help message. 

181 

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 

195 

196 

197def execute(args: list = None) -> list: 

198 """ 

199 Execute program with provided list of arguments. 

200 

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. 

204 

205 Parameters 

206 ---------- 

207 args : list 

208 Commandline arguments. default=None 

209 

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"] 

221 

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 ) 

232 

233 parser.add_argument( 

234 "-q", 

235 "--quiet", 

236 help="Turn off all text output.", 

237 dest="quiet", 

238 action="store_true", 

239 ) 

240 

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 ) 

248 

249 parser.add_argument( 

250 "-v", 

251 "--verbose", 

252 action="store_true", 

253 dest="debug", 

254 help="output debug information", 

255 ) 

256 

257 parser.set_defaults(func=parser.print_help) 

258 

259 subparsers = parser.add_subparsers( 

260 title="Commands", 

261 dest="command", 

262 metavar="create, edit, info, magnet, recheck, rebuild, rename\n", 

263 ) 

264 

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 ) 

272 

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 ) 

284 

285 create_parser.add_argument( 

286 "-p", 

287 "--private", 

288 action="store_true", 

289 dest="private", 

290 help="create private torrent", 

291 ) 

292 

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 ) 

301 

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 ) 

312 

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 ) 

320 

321 create_parser.add_argument( 

322 "-m", 

323 "--magnet", 

324 action="store_true", 

325 dest="magnet", 

326 ) 

327 

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 ) 

336 

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 ) 

345 

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 ) 

361 

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 ) 

377 

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 ) 

389 

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 ) 

398 

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 ) 

407 

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 ) 

414 

415 create_parser.add_argument( 

416 "content", 

417 action="store", 

418 metavar="<content>", 

419 nargs="?", 

420 help="path to content file or directory", 

421 ) 

422 

423 create_parser.set_defaults(func=commands.create) 

424 

425 edit_parser = subparsers.add_parser( 

426 "edit", 

427 help="edit torrent file", 

428 prefix_chars="-", 

429 formatter_class=TorrentFileHelpFormatter, 

430 ) 

431 

432 edit_parser.add_argument( 

433 "metafile", 

434 action="store", 

435 help="path to *.torrent file", 

436 metavar="<*.torrent>", 

437 ) 

438 

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 ) 

447 

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 ) 

456 

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 ) 

465 

466 edit_parser.add_argument( 

467 "--private", 

468 action="store_true", 

469 help="make torrent private", 

470 dest="private", 

471 ) 

472 

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 ) 

480 

481 edit_parser.add_argument( 

482 "--source", 

483 action="store", 

484 dest="source", 

485 metavar="<source>", 

486 help="replaces current source with <source>", 

487 ) 

488 

489 edit_parser.set_defaults(func=commands.edit) 

490 

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 ) 

497 

498 info_parser.add_argument( 

499 "metafile", 

500 action="store", 

501 metavar="<*.torrent>", 

502 help="path to torrent file", 

503 ) 

504 

505 info_parser.set_defaults(func=commands.info) 

506 

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 ) 

514 

515 magnet_parser.add_argument( 

516 "metafile", 

517 action="store", 

518 help="path to torrent file", 

519 metavar="<*.torrent>", 

520 ) 

521 

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 ) 

538 

539 magnet_parser.set_defaults(func=commands.get_magnet) 

540 

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 ) 

548 

549 check_parser.add_argument( 

550 "metafile", 

551 action="store", 

552 metavar="<*.torrent>", 

553 help="path to .torrent file.", 

554 ) 

555 

556 check_parser.add_argument( 

557 "content", 

558 action="store", 

559 metavar="<content>", 

560 help="path to content file or directory", 

561 ) 

562 

563 check_parser.set_defaults(func=commands.recheck) 

564 

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 ) 

574 

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 ) 

585 

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 ) 

596 

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 ) 

606 

607 rebuild_parser.set_defaults(func=commands.rebuild) 

608 

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 ) 

615 

616 rename_parser.add_argument( 

617 "target", 

618 action="store", 

619 metavar="<target>", 

620 help="path to torrent file", 

621 ) 

622 

623 rename_parser.set_defaults(func=commands.rename) 

624 

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") 

644 

645 args = parser.parse_args(args) 

646 

647 if args.quiet: 

648 Config.activate_quiet() 

649 

650 elif args.debug: 

651 Config.activate_logger() 

652 

653 if hasattr(args, "func"): 

654 return args.func(args) 

655 return args # pragma: nocover 

656 

657 

658main_script = execute 

659 

660 

661def main() -> None: 

662 """ 

663 Initiate main function for CLI script. 

664 """ 

665 execute()