Skip to content

Commit a277165

Browse files
committed
nicer interface
1 parent fce8beb commit a277165

4 files changed

Lines changed: 264 additions & 161 deletions

File tree

exportify-cli.py

Lines changed: 99 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010

1111
import click
1212
import spotipy
13+
from click_option_group import OptionGroup, optgroup
1314
from spotipy.oauth2 import SpotifyOAuth
1415
from tabulate import tabulate
1516
from tqdm.auto import tqdm
1617

1718
# Default options for the CLI (used in [exportify-cli] section)
1819
CLI_DEFAULTS = {
20+
"format": "csv",
21+
"output": "./playlists",
1922
"uris": "false",
2023
"external_ids": "false",
21-
"with_bar": "true",
24+
"no_bar": "false",
2225
}
2326

2427
# Default bar format for progress bars
@@ -182,14 +185,14 @@ def __init__(
182185
file_format: str,
183186
include_uris: bool,
184187
external_ids: bool,
185-
with_bar_flag: bool,
188+
with_bar: bool,
186189
) -> None:
187190
"""Initialize the exporter with a Spotify client."""
188191
self.spotify = spotify_client
189192
self.file_format = file_format
190193
self.include_uris = include_uris
191194
self.external_ids = external_ids
192-
self.with_bar_flag = with_bar_flag
195+
self.with_bar = with_bar
193196
self.exported_playlists = 0
194197
self.exported_tracks = 0
195198

@@ -223,7 +226,7 @@ def _fetch_all_items(
223226

224227
pbar = (
225228
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
227230
else None
228231
)
229232

@@ -268,7 +271,7 @@ def _fetch_all_items(
268271

269272
pbar = (
270273
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
272275
else None
273276
)
274277
if pbar:
@@ -429,109 +432,134 @@ def export_playlist(self, playlist: dict, output_dir: Path) -> None:
429432
)
430433

431434

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(
436463
"-c",
464+
"--config",
465+
"config",
437466
default="config.cfg",
467+
show_default=True,
438468
type=click.Path(),
439-
help="Path to configuration file",
469+
help="Path to configuration file.",
440470
)
441-
@click.option(
442-
"--output",
471+
@optgroup.option(
443472
"-o",
473+
"--output",
474+
"output_param",
444475
default="./playlists",
476+
show_default=True,
445477
type=click.Path(),
446-
help="Directory to save files",
478+
help="Directory to save exported files.",
447479
)
448-
@click.option(
449-
"--format",
480+
@optgroup.option(
450481
"-f",
451-
"file_format",
482+
"--format",
483+
"format_param",
452484
type=click.Choice(["csv", "json"]),
453485
default="csv",
454-
help="Output file format",
486+
show_default=True,
487+
help="Output file format.",
455488
)
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,
474493
is_flag=True,
475-
help="List available playlists",
494+
help="Include album and artist URIs.",
476495
)
477-
@click.option(
478-
"--uris/--no-uris",
479-
"include_uris",
496+
@optgroup.option(
497+
"--external-ids",
498+
"external_ids_flag",
480499
default=None,
481-
help="Include album and artist URIs (overrides config)",
500+
is_flag=True,
501+
help="Include track ISRC and album UPC.",
482502
)
483-
@click.option(
484-
"--external-ids/--no-external-ids",
485-
"external_ids",
503+
@optgroup.option(
504+
"--no-bar",
505+
"no_bar_flag",
486506
default=None,
487-
help="Include track ISRC and album UPC (overrides config)",
507+
is_flag=True,
508+
help="Hide progress bar.",
488509
)
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",
494517
)
495518
def main(
496-
config: str,
497-
output: str,
498-
file_format: str,
499519
export_all: bool,
500520
playlist: tuple[str, ...],
501521
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,
505528
) -> 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+
507535
cfg_path = Path(config)
508536
cfg = load_config(cfg_path)
509537

510538
# 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")
515543
)
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
519547
else cfg.getboolean("exportify-cli", "external_ids")
520548
)
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")
525553
)
526554

527555
client = init_spotify_client(cfg)
528556

529557
exporter = SpotifyExporter(
530558
spotify_client=client,
531559
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,
535563
)
536564

537565
playlist = list(playlist)
@@ -608,6 +636,7 @@ def main(
608636
f"Successfully exported {exporter.exported_tracks} tracks "
609637
f"from {exporter.exported_playlists} playlists.",
610638
)
639+
click.echo()
611640
sys.exit(0)
612641

613642

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
88
"click>=8.1.8",
9+
"click-option-group>=0.5.7",
910
"requests>=2.32.3",
1011
"spotipy>=2.25.1",
1112
"tabulate>=0.9.0",

requirements.txt

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,95 @@
11
# This file was autogenerated by uv via the following command:
2-
# uv pip compile .\pyproject.toml -o requirements.txt
3-
certifi==2024.7.4
2+
# uv export --format requirements.txt
3+
async-timeout==5.0.1 ; python_full_version < '3.11.3' \
4+
--hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
5+
--hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
6+
# via redis
7+
certifi==2025.4.26 \
8+
--hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \
9+
--hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3
410
# via requests
5-
charset-normalizer==3.3.2
11+
charset-normalizer==3.4.2 \
12+
--hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
13+
--hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \
14+
--hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \
15+
--hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \
16+
--hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \
17+
--hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \
18+
--hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \
19+
--hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \
20+
--hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \
21+
--hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \
22+
--hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \
23+
--hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \
24+
--hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \
25+
--hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \
26+
--hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \
27+
--hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \
28+
--hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \
29+
--hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \
30+
--hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \
31+
--hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \
32+
--hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \
33+
--hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \
34+
--hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \
35+
--hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \
36+
--hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \
37+
--hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \
38+
--hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \
39+
--hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \
40+
--hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \
41+
--hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \
42+
--hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \
43+
--hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \
44+
--hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \
45+
--hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \
46+
--hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \
47+
--hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \
48+
--hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \
49+
--hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \
50+
--hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \
51+
--hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \
52+
--hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f
653
# via requests
7-
colorama==0.4.6
8-
# via tqdm
9-
idna==3.8
54+
click==8.1.8 \
55+
--hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
56+
--hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
57+
# via exportify-cli
58+
colorama==0.4.6 ; sys_platform == 'win32' \
59+
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
60+
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
61+
# via
62+
# click
63+
# tqdm
64+
idna==3.10 \
65+
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
66+
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
1067
# via requests
11-
redis==5.0.8
68+
redis==6.0.0 \
69+
--hash=sha256:5446780d2425b787ed89c91ddbfa1be6d32370a636c8fdb687f11b1c26c1fa88 \
70+
--hash=sha256:a2e040aee2cdd947be1fa3a32e35a956cd839cc4c1dbbe4b2cdee5b9623fd27c
1271
# via spotipy
13-
requests==2.32.3
72+
requests==2.32.3 \
73+
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
74+
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
1475
# via
15-
# exportify-cli (./pyproject.toml)
76+
# exportify-cli
1677
# spotipy
17-
spotipy==2.25.1
18-
# via exportify-cli (./pyproject.toml)
19-
tabulate==0.9.0
20-
# via exportify-cli (./pyproject.toml)
21-
tqdm==4.67.1
22-
# via exportify-cli (./pyproject.toml)
23-
urllib3==2.2.2
78+
spotipy==2.25.1 \
79+
--hash=sha256:18f47d9a0594ced8001b3baa5a4160ea6dcf570abbdad371971f475a81892844 \
80+
--hash=sha256:607d3c43722b7e217a7e6c82f17258b798b92a512017d60c06323d62ae814cd7
81+
# via exportify-cli
82+
tabulate==0.9.0 \
83+
--hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \
84+
--hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f
85+
# via exportify-cli
86+
tqdm==4.67.1 \
87+
--hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \
88+
--hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2
89+
# via exportify-cli
90+
urllib3==2.4.0 \
91+
--hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \
92+
--hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813
2493
# via
2594
# requests
2695
# spotipy

0 commit comments

Comments
 (0)