I'll probably do these this summer:
I'll probably release all of these in a v1.0.0 version, making it as much backwards compatible as I can.
Fable had some interesting suggestions and bugs to fix too:
Details
Had a look through `exportify-cli.py` — a few things you might want to add:
Actual bugs I spotted:
- The
output option in config.cfg is dead code: -o has default="./playlists" in the click decorator, so output_param is always truthy and output_param if output_param else cfg.get(...) never falls back to config. Same pattern as your -l issue — config/CLI resolution deserves one unified pass.
- Sorting is lexicographic on
str(): --sort-key Popularity gives 9 > 10 > 100 order, and str(None) or "" is "None", so the or "" fallback never fires. Numeric keys need numeric comparison, and Nones should sort predictably.
os.get_terminal_size() raises OSError when stdout isn't a tty, so exportify-cli -l | grep foo or running from cron crashes. shutil.get_terminal_size() has a fallback built in.
- In
_fetch_all_items paginated mode, total = results.get("total") can be None for some endpoints, and while len(items) < total would then throw a TypeError. Worth a guard (results.get("next") is the more robust loop condition anyway).
- The
format resolution uses substring checks ("csv" in cfg.get(...)), so a typo'd or empty format silently exports nothing with exit code 0. Validate and error instead.
- If you're building with PyInstaller
--onefile, Path(__file__).parent points to the temp extraction dir (_MEIPASS), not next to the binary — so .cache and config.cfg may be written somewhere ephemeral. Check for sys.frozen and use Path(sys.executable).parent in that case (this ties into your XDG item).
Smaller stuff / polish:
- CSV output writes list fields as Python reprs (
['Artist A', 'Artist B']). Original exportify joins with commas — probably want ", ".join(...) for CSV at least.
- README says playlist names support "partial matching" but the code only does
startswith prefix matching. Align one with the other.
-c pointing at a nonexistent explicit path silently starts the config-creation wizard there; arguably that should be an error when the user passed the path themselves.
get_version() shells out to git at import time on every invocation — cheap but unnecessary; lazy-evaluate it.
- The redundant
if unfetched_ids: check inside the show-fetching loop in _fetch_all_items (it's already iterating over it).
Feature ideas:
- Skip-unchanged exports: compare the playlist
snapshot_id against the last export and skip if identical — makes -a on a cron job fast and diff-friendly (the original exportify does something similar).
- Non-zero exit code / summary when some requested playlists failed or were skipped (ambiguous prefix currently just prints "Skipping" and exits 0).
- Optional playlist metadata in output or a sidecar file: description, owner, snapshot_id, export timestamp.
- A
--quiet/--verbose flag now that you have logging set up (currently logger.info is invisible).
- Once you split into modules and add tests:
ruff + a CI lint/test workflow, since you already have .github/workflows.
The config/CLI resolution one is probably worth prioritizing since three of your existing items (-l writing config, legacy uris/external_ids, format vs fields syntax) plus the dead output fallback all stem from the same tangle — a single "config schema + precedence" refactor would knock them all out together.
I'll probably do these this summer:
mainso it's more readable._fetch_all_itemsso it doesn't take two kinds of data.usersoption toconfig.cfg.urisandexternal_idsin config by default. It shouldn't do that.-lchanges the config file unnecessarily.formatcan take json and csv separated by a space.fieldstakes multiple arguments separated by commas. Choose one or allow both.$XDG_CONFIG_HOMEand place.cachesomewhere else?fieldsoption in config should, by default, have the default fields, not be empty. Probably a comment above it with all options too.I'll probably release all of these in a v1.0.0 version, making it as much backwards compatible as I can.
Fable had some interesting suggestions and bugs to fix too:
Details
Had a look through `exportify-cli.py` — a few things you might want to add:Actual bugs I spotted:
outputoption inconfig.cfgis dead code:-ohasdefault="./playlists"in the click decorator, sooutput_paramis always truthy andoutput_param if output_param else cfg.get(...)never falls back to config. Same pattern as your-lissue — config/CLI resolution deserves one unified pass.str():--sort-key Popularitygives 9 > 10 > 100 order, andstr(None) or ""is"None", so theor ""fallback never fires. Numeric keys need numeric comparison, andNones should sort predictably.os.get_terminal_size()raisesOSErrorwhen stdout isn't a tty, soexportify-cli -l | grep fooor running from cron crashes.shutil.get_terminal_size()has a fallback built in._fetch_all_itemspaginated mode,total = results.get("total")can beNonefor some endpoints, andwhile len(items) < totalwould then throw aTypeError. Worth a guard (results.get("next")is the more robust loop condition anyway).formatresolution uses substring checks ("csv" in cfg.get(...)), so a typo'd or emptyformatsilently exports nothing with exit code 0. Validate and error instead.--onefile,Path(__file__).parentpoints to the temp extraction dir (_MEIPASS), not next to the binary — so.cacheandconfig.cfgmay be written somewhere ephemeral. Check forsys.frozenand usePath(sys.executable).parentin that case (this ties into your XDG item).Smaller stuff / polish:
['Artist A', 'Artist B']). Original exportify joins with commas — probably want", ".join(...)for CSV at least.startswithprefix matching. Align one with the other.-cpointing at a nonexistent explicit path silently starts the config-creation wizard there; arguably that should be an error when the user passed the path themselves.get_version()shells out to git at import time on every invocation — cheap but unnecessary; lazy-evaluate it.if unfetched_ids:check inside the show-fetching loop in_fetch_all_items(it's already iterating over it).Feature ideas:
snapshot_idagainst the last export and skip if identical — makes-aon a cron job fast and diff-friendly (the original exportify does something similar).--quiet/--verboseflag now that you have logging set up (currently logger.info is invisible).ruff+ a CI lint/test workflow, since you already have.github/workflows.The config/CLI resolution one is probably worth prioritizing since three of your existing items (
-lwriting config, legacyuris/external_ids,formatvsfieldssyntax) plus the deadoutputfallback all stem from the same tangle — a single "config schema + precedence" refactor would knock them all out together.