|
10 | 10 |
|
11 | 11 | import click |
12 | 12 | import spotipy |
| 13 | +from click_option_group import OptionGroup, optgroup |
13 | 14 | from spotipy.oauth2 import SpotifyOAuth |
14 | 15 | from tabulate import tabulate |
15 | 16 | from tqdm.auto import tqdm |
16 | 17 |
|
17 | 18 | # Default options for the CLI (used in [exportify-cli] section) |
18 | 19 | CLI_DEFAULTS = { |
| 20 | + "format": "csv", |
| 21 | + "output": "./playlists", |
19 | 22 | "uris": "false", |
20 | 23 | "external_ids": "false", |
21 | | - "with_bar": "true", |
| 24 | + "no_bar": "false", |
22 | 25 | } |
23 | 26 |
|
24 | 27 | # Default bar format for progress bars |
@@ -182,14 +185,14 @@ def __init__( |
182 | 185 | file_format: str, |
183 | 186 | include_uris: bool, |
184 | 187 | external_ids: bool, |
185 | | - with_bar_flag: bool, |
| 188 | + with_bar: bool, |
186 | 189 | ) -> None: |
187 | 190 | """Initialize the exporter with a Spotify client.""" |
188 | 191 | self.spotify = spotify_client |
189 | 192 | self.file_format = file_format |
190 | 193 | self.include_uris = include_uris |
191 | 194 | self.external_ids = external_ids |
192 | | - self.with_bar_flag = with_bar_flag |
| 195 | + self.with_bar = with_bar |
193 | 196 | self.exported_playlists = 0 |
194 | 197 | self.exported_tracks = 0 |
195 | 198 |
|
@@ -223,7 +226,7 @@ def _fetch_all_items( |
223 | 226 |
|
224 | 227 | pbar = ( |
225 | 228 | tqdm(total=total, desc=desc_text, unit="album", bar_format=bar_format) |
226 | | - if show_bar and self.with_bar_flag |
| 229 | + if show_bar and self.with_bar |
227 | 230 | else None |
228 | 231 | ) |
229 | 232 |
|
@@ -268,7 +271,7 @@ def _fetch_all_items( |
268 | 271 |
|
269 | 272 | pbar = ( |
270 | 273 | tqdm(total=total, desc=desc_text, unit="track", bar_format=bar_format) |
271 | | - if show_bar and self.with_bar_flag |
| 274 | + if show_bar and self.with_bar |
272 | 275 | else None |
273 | 276 | ) |
274 | 277 | if pbar: |
@@ -429,109 +432,134 @@ def export_playlist(self, playlist: dict, output_dir: Path) -> None: |
429 | 432 | ) |
430 | 433 |
|
431 | 434 |
|
432 | | -@click.command() |
433 | | -@click.help_option("-h", "--help") |
434 | | -@click.option( |
435 | | - "--config", |
| 435 | +# Custom command class to override usage line |
| 436 | +class CustomCommand(click.Command): |
| 437 | + def format_usage(self, ctx, formatter) -> None: |
| 438 | + # Override the usage display |
| 439 | + formatter.write_text( |
| 440 | + "Usage: exportify-cli.py (-a | -p NAME|ID|URL|URI [-p ...] | -l) [OPTIONS]\n", |
| 441 | + ) |
| 442 | + |
| 443 | + |
| 444 | +@click.command(cls=CustomCommand) |
| 445 | +@optgroup.group(cls=OptionGroup) |
| 446 | +@optgroup.option("-a", "--all", "export_all", is_flag=True, help="Export all playlists") |
| 447 | +@optgroup.option( |
| 448 | + "-p", |
| 449 | + "--playlist", |
| 450 | + "playlist", |
| 451 | + multiple=True, |
| 452 | + metavar="NAME|ID|URL|URI", |
| 453 | + help="Spotify playlist name, ID, URL, or URI; repeatable.", |
| 454 | +) |
| 455 | +@optgroup.option( |
| 456 | + "-l", |
| 457 | + "--list", |
| 458 | + "list_only", |
| 459 | + is_flag=True, |
| 460 | + help="List available playlists.", |
| 461 | +) |
| 462 | +@optgroup.option( |
436 | 463 | "-c", |
| 464 | + "--config", |
| 465 | + "config", |
437 | 466 | default="config.cfg", |
| 467 | + show_default=True, |
438 | 468 | type=click.Path(), |
439 | | - help="Path to configuration file", |
| 469 | + help="Path to configuration file.", |
440 | 470 | ) |
441 | | -@click.option( |
442 | | - "--output", |
| 471 | +@optgroup.option( |
443 | 472 | "-o", |
| 473 | + "--output", |
| 474 | + "output_param", |
444 | 475 | default="./playlists", |
| 476 | + show_default=True, |
445 | 477 | type=click.Path(), |
446 | | - help="Directory to save files", |
| 478 | + help="Directory to save exported files.", |
447 | 479 | ) |
448 | | -@click.option( |
449 | | - "--format", |
| 480 | +@optgroup.option( |
450 | 481 | "-f", |
451 | | - "file_format", |
| 482 | + "--format", |
| 483 | + "format_param", |
452 | 484 | type=click.Choice(["csv", "json"]), |
453 | 485 | default="csv", |
454 | | - help="Output file format", |
| 486 | + show_default=True, |
| 487 | + help="Output file format.", |
455 | 488 | ) |
456 | | -@click.option( |
457 | | - "--all", |
458 | | - "-a", |
459 | | - "export_all", |
460 | | - is_flag=True, |
461 | | - help="Export all playlists", |
462 | | -) |
463 | | -@click.option( |
464 | | - "--playlist", |
465 | | - "-p", |
466 | | - "playlist", |
467 | | - multiple=True, |
468 | | - help="Names, URLs or IDs of playlists to export", |
469 | | -) |
470 | | -@click.option( |
471 | | - "--list", |
472 | | - "-l", |
473 | | - "list_only", |
| 489 | +@optgroup.option( |
| 490 | + "--uris", |
| 491 | + "uris_flag", |
| 492 | + default=None, |
474 | 493 | is_flag=True, |
475 | | - help="List available playlists", |
| 494 | + help="Include album and artist URIs.", |
476 | 495 | ) |
477 | | -@click.option( |
478 | | - "--uris/--no-uris", |
479 | | - "include_uris", |
| 496 | +@optgroup.option( |
| 497 | + "--external-ids", |
| 498 | + "external_ids_flag", |
480 | 499 | default=None, |
481 | | - help="Include album and artist URIs (overrides config)", |
| 500 | + is_flag=True, |
| 501 | + help="Include track ISRC and album UPC.", |
482 | 502 | ) |
483 | | -@click.option( |
484 | | - "--external-ids/--no-external-ids", |
485 | | - "external_ids", |
| 503 | +@optgroup.option( |
| 504 | + "--no-bar", |
| 505 | + "no_bar_flag", |
486 | 506 | default=None, |
487 | | - help="Include track ISRC and album UPC (overrides config)", |
| 507 | + is_flag=True, |
| 508 | + help="Hide progress bar.", |
488 | 509 | ) |
489 | | -@click.option( |
490 | | - "--with-bar/--no-bar", |
491 | | - "with_bar_flag", |
492 | | - default=None, |
493 | | - help="Show or hide progress bar (overrides config)", |
| 510 | +@click.help_option("-h", "--help") |
| 511 | +@click.version_option( |
| 512 | + "0.2", |
| 513 | + "-v", |
| 514 | + "--version", |
| 515 | + prog_name="exportify-cli", |
| 516 | + message="%(prog)s v%(version)s", |
494 | 517 | ) |
495 | 518 | def main( |
496 | | - config: str, |
497 | | - output: str, |
498 | | - file_format: str, |
499 | 519 | export_all: bool, |
500 | 520 | playlist: tuple[str, ...], |
501 | 521 | list_only: bool, |
502 | | - include_uris: bool, |
503 | | - external_ids: bool, |
504 | | - with_bar_flag: bool, |
| 522 | + config: str, |
| 523 | + output_param: str, |
| 524 | + format_param: str, |
| 525 | + uris_flag: bool, |
| 526 | + external_ids_flag: bool, |
| 527 | + no_bar_flag: bool, |
505 | 528 | ) -> None: |
506 | | - """CLI entrypoint for exporting Spotify playlists.""" |
| 529 | + """Export Spotify playlists to CSV or JSON.""" |
| 530 | + # If no --all, --playlist or --list options are given, show help |
| 531 | + if not export_all and not playlist and not list_only: |
| 532 | + click.echo(main.get_help(ctx=click.get_current_context())) |
| 533 | + sys.exit(1) |
| 534 | + |
507 | 535 | cfg_path = Path(config) |
508 | 536 | cfg = load_config(cfg_path) |
509 | 537 |
|
510 | 538 | # Resolve config vs CLI |
511 | | - uris_flag = ( |
512 | | - include_uris |
513 | | - if include_uris is not None |
514 | | - else cfg.getboolean("exportify-cli", "uris") |
| 539 | + file_format = format_param if format_param else cfg.get("exportify-cli", "format") |
| 540 | + output = output_param if output_param else cfg.get("exportify-cli", "output") |
| 541 | + include_uris = ( |
| 542 | + uris_flag if uris_flag is not None else cfg.getboolean("exportify-cli", "uris") |
515 | 543 | ) |
516 | | - ext_flag = ( |
517 | | - external_ids |
518 | | - if external_ids is not None |
| 544 | + external_ids = ( |
| 545 | + external_ids_flag |
| 546 | + if external_ids_flag is not None |
519 | 547 | else cfg.getboolean("exportify-cli", "external_ids") |
520 | 548 | ) |
521 | | - bar_flag = ( |
522 | | - with_bar_flag |
523 | | - if with_bar_flag is not None |
524 | | - else cfg.getboolean("exportify-cli", "with_bar") |
| 549 | + with_bar = not ( |
| 550 | + no_bar_flag |
| 551 | + if no_bar_flag is not None |
| 552 | + else cfg.getboolean("exportify-cli", "no_bar") |
525 | 553 | ) |
526 | 554 |
|
527 | 555 | client = init_spotify_client(cfg) |
528 | 556 |
|
529 | 557 | exporter = SpotifyExporter( |
530 | 558 | spotify_client=client, |
531 | 559 | file_format=file_format, |
532 | | - include_uris=uris_flag, |
533 | | - external_ids=ext_flag, |
534 | | - with_bar_flag=bar_flag, |
| 560 | + include_uris=include_uris, |
| 561 | + external_ids=external_ids, |
| 562 | + with_bar=with_bar, |
535 | 563 | ) |
536 | 564 |
|
537 | 565 | playlist = list(playlist) |
@@ -608,6 +636,7 @@ def main( |
608 | 636 | f"Successfully exported {exporter.exported_tracks} tracks " |
609 | 637 | f"from {exporter.exported_playlists} playlists.", |
610 | 638 | ) |
| 639 | + click.echo() |
611 | 640 | sys.exit(0) |
612 | 641 |
|
613 | 642 |
|
|
0 commit comments