#!/usr/bin/env python3 """Small, dependency-free Last.fm API client used by the lastfm skill.""" import argparse import json import os import sys import urllib.parse import urllib.request API = "https://ws.audioscrobbler.com/2.0/" METHODS = { "user info": "user.getinfo", "user recent-tracks": "user.getrecenttracks", "user top-artists": "user.gettopartists", "user top-tracks": "user.gettoptracks", "user loved-tracks": "user.getlovedtracks", "user friends": "user.getfriends", "user weekly-charts": "user.getweeklychartlist", "artist similar": "artist.getsimilar", "artist info": "artist.getinfo", "artist top-tracks": "artist.gettoptracks", "artist search": "artist.search", "album info": "album.getinfo", "album search": "album.search", "track similar": "track.getsimilar", "track search": "track.search", "tag top-artists": "tag.gettopartists", "tag top-tracks": "tag.gettoptracks", "tag top-albums": "tag.gettopalbums", "chart top-artists": "chart.gettopartists", "geo top-artists": "geo.gettopartists", "geo top-tracks": "geo.gettoptracks", } def parser(): p = argparse.ArgumentParser(description="Last.fm API client", epilog="Examples: lastfm-cli artist similar Radiohead --limit 5") p.add_argument("--json", action="store_true", help="emit JSON") p.add_argument("--dry-run", action="store_true", help="show the API plan without a request") subs = p.add_subparsers(dest="group", required=True) for group, commands in {"user": ["info", "recent-tracks", "top-artists", "top-tracks", "loved-tracks", "friends", "weekly-charts"], "artist": ["similar", "info", "top-tracks", "search"], "album": ["info", "search"], "track": ["similar", "search", "scrobble", "now-playing", "love"], "tag": ["top-artists", "top-tracks", "top-albums"], "chart": ["top-artists"], "geo": ["top-artists", "top-tracks"], "auth": ["get-token", "get-session"]}.items(): gp = subs.add_parser(group) cs = gp.add_subparsers(dest="command", required=True) for command in commands: cp = cs.add_parser(command) if group in {"user", "artist", "album", "tag", "geo"} or group == "track": cp.add_argument("values", nargs="*", metavar="VALUE", help="username, artist, tag, country, or track values") cp.add_argument("--limit", type=int) cp.add_argument("--period") cp.add_argument("--autocorrect", action="store_true") return p def main(): args = parser().parse_args() key = f"{args.group} {args.command}" if args.group == "auth": print("Auth operations require LASTFM_API_KEY, LASTFM_API_SECRET, and a browser authorization flow.") return 0 values = getattr(args, "values", []) if args.command not in {"search", "top-artists", "top-tracks", "top-albums", "weekly-charts"} and not values: parser().error(f"{args.group} {args.command}: a value is required") method = METHODS.get(key) if not method: parser().error(f"unsupported operation: {key}") params = {"method": method, "format": "json"} if values: param = "user" if args.group == "user" else "country" if args.group == "geo" else "tag" if args.group == "tag" else "artist" params[param] = values[0] if args.group in {"track", "album"} and len(values) > 1: params["track" if args.group == "track" else "album"] = values[1] for option in ("limit", "period", "autocorrect"): value = getattr(args, option, None) if value not in (None, False): params[option] = value if args.dry_run: print(json.dumps({"dry_run": True, "url": API, "params": params}, indent=2)) return 0 api_key = os.getenv("LASTFM_API_KEY") if not api_key: print("LASTFM_API_KEY is required", file=sys.stderr) return 2 params["api_key"] = api_key try: with urllib.request.urlopen(f"{API}?{urllib.parse.urlencode(params)}", timeout=30) as response: payload = json.load(response) except Exception as exc: print(f"Last.fm request failed: {exc}", file=sys.stderr) return 1 print(json.dumps(payload, indent=2) if args.json else json.dumps(payload, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())