lastfm
Access Last.fm user profile, now playing, top tracks/artists/albums by period, loved tracks, and optionally love/unlove tracks.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-lastfm-openclaw
Repository
Skill path: skills/dennisooki/lastfm-openclaw
Access Last.fm user profile, now playing, top tracks/artists/albums by period, loved tracks, and optionally love/unlove tracks.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install lastfm into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding lastfm to shared team environments
- Use lastfm for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: lastfm
description: Access Last.fm user profile, now playing, top tracks/artists/albums by period, loved tracks, and optionally love/unlove tracks.
metadata:
{
"openclaw":
{
"requires": { "env": ["LASTFM_API_KEY", "LASTFM_USERNAME"], "bins": ["curl", "jq"] },
"primaryEnv": "LASTFM_API_KEY",
"emoji": "šµ"
}
}
---
# Last.fm Profile Skill
Retrieves Last.fm user listening data including now playing, top tracks/artists/albums by time period, and loved tracks. Optionally supports write operations (love/unlove tracks, scrobble) when `LASTFM_SESSION_KEY` is configured.
## Required Environment Variables
- `LASTFM_API_KEY`: Your Last.fm API key (get one at https://www.last.fm/api/account/create)
- `LASTFM_USERNAME`: Your Last.fm username
## Optional Environment Variables
- `LASTFM_SESSION_KEY`: Required for write operations (love/unlove, scrobble)
- `LASTFM_API_SECRET`: Required to sign write operations (love/unlove, scrobble)
## Workflow
1. Validate required environment variables are present
2. Ensure dependencies (`jq`, `curl`) are available
3. Determine which command the user is requesting
2. Determine which command the user is requesting
3. Construct API request to `ws.audioscrobbler.com/2.0/`
4. Execute HTTP GET request with appropriate method and parameters
5. Parse JSON response and format for user
## Supported Commands
### Read Operations (No Auth Required)
| Command | Description | Example |
|---------|-------------|---------|
| `now-playing`, `np` | Current or most recent track | `/lastfm np` |
| `top-tracks [period]` | Top tracks by period | `/lastfm top-tracks 7day` |
| `top-artists [period]` | Top artists by period | `/lastfm top-artists 1month` |
| `top-albums [period]` | Top albums by period | `/lastfm top-albums overall` |
| `loved` | Loved tracks | `/lastfm loved` |
| `recent [limit]` | Recent tracks (default 10) | `/lastfm recent 20` |
| `profile` | User profile info | `/lastfm profile` |
### Time Periods
- `7day` - Last 7 days
- `1month` - Last 30 days
- `3month` - Last 90 days
- `6month` - Last 180 days
- `12month` - Last year
- `overall` - All time (default if not specified)
### Write Operations (Auth Required)
| Command | Description | Example |
|---------|-------------|---------|
| `love <artist> <track>` | Love a track | `/lastfm love "Radiohead" "Creep"` |
| `unlove <artist> <track>` | Unlove a track | `/lastfm unlove "Radiohead" "Creep"` |
## API Request Construction
Base URL: `https://ws.audioscrobbler.com/2.0/`
Required parameters for all requests:
- `api_key`: Value from `LASTFM_API_KEY`
- `format`: `json`
- `method`: API method name
User-specific requests also require:
- `user`: Value from `LASTFM_USERNAME`
### Method Parameters
| Method | Additional Parameters |
|--------|----------------------|
| `user.getInfo` | `user` |
| `user.getRecentTracks` | `user`, `limit` (optional) |
| `user.getTopTracks` | `user`, `period` (optional) |
| `user.getTopArtists` | `user`, `period` (optional) |
| `user.getTopAlbums` | `user`, `period` (optional) |
| `user.getLovedTracks` | `user` |
| `track.love` | `artist`, `track`, `sk` (session key) |
| `track.unlove` | `artist`, `track`, `sk` (session key) |
## Response Parsing
### Now Playing Response
Extract from `recenttracks.track[0]`:
- If `@attr.nowplaying === "true"`: currently playing
- `artist.#text` - Artist name
- `name` - Track name
- `album.#text` - Album name
### Top Items Response
Extract array from:
- `toptracks.track[]` for top tracks
- `topartists.artist[]` for top artists
- `topalbums.album[]` for top albums
Each item includes:
- `name` - Item name
- `playcount` - Play count
- `artist.name` - Artist (for tracks/albums)
- `@attr.rank` - Position in chart
### Profile Response
Extract from `user`:
- `name` - Username
- `realname` - Real name (if set)
- `playcount` - Total scrobbles
- `country` - Country
- `registered` - Account creation date
- `url` - Profile URL
## Guardrails
- Never log or expose API keys or session keys in output
- Rate limit: respect Last.fm's 5 requests/second limit
- Write operations must fail gracefully if `LASTFM_SESSION_KEY` not set
- All user inputs must be URL-encoded before API calls
- Only connect to `ws.audioscrobbler.com` - no external endpoints
- Handle missing data gracefully (e.g., no now playing, empty loved tracks)
- Validate period parameter is one of: 7day, 1month, 3month, 6month, 12month, overall
- Validate `recent` limit is numeric and within 1ā200
## Error Handling
| Error Code | Meaning | Action |
|------------|---------|--------|
| 10 | Invalid API key | Tell user to check `LASTFM_API_KEY` |
| 6 | Invalid parameters | Check required params are present |
| 29 | Rate limit exceeded | Wait and retry, inform user |
| 26 | Suspended API key | Direct user to Last.fm support |
| 4 | Authentication failed | Check session key for write ops |
## Example Output Formats
### Now Playing
```
šµ Now Playing:
"Track Name" by Artist Name
from Album Name
```
Or if not currently playing:
```
šµ Last Played:
"Track Name" by Artist Name
Listened: [timestamp]
```
### Top Tracks
```
šµ Top Tracks (7 days):
1. "Track One" by Artist One (42 plays)
2. "Track Two" by Artist Two (38 plays)
3. "Track Three" by Artist Three (31 plays)
...
```
### Profile
```
šµ Last.fm Profile: username
š 15,432 total scrobbles
š United Kingdom
š
Member since: Nov 2002
š last.fm/user/username
```
## Setup Instructions
1. Get a Last.fm API key at https://www.last.fm/api/account/create
2. Add to `~/.openclaw/openclaw.json`:
```json5
{
skills: {
entries: {
lastfm: {
enabled: true,
env: {
LASTFM_API_KEY: "your_api_key_here",
LASTFM_USERNAME: "your_username"
}
}
}
}
}
```
3. For write operations, see `{baseDir}/references/auth-guide.md`
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### README.md
```markdown
# Last.fm OpenClaw Skill
A secure, configurable OpenClaw skill for accessing Last.fm user profile data.
## Features
- **Now Playing**: Get current or most recently played track
- **Top Tracks**: View top tracks by time period (7day, 1month, 3month, 6month, 12month, overall)
- **Top Artists**: View top artists by time period
- **Top Albums**: View top albums by time period
- **Loved Tracks**: See your loved tracks collection
- **Recent Tracks**: View recent listening history
- **Profile Info**: Get user profile statistics
- **Love/Unlove**: Mark tracks as loved (requires auth)
## Installation
### Prerequisites
- `jq` (for JSON parsing)
- `curl`
Install on Ubuntu/Debian:
```
sudo apt-get update && sudo apt-get install -y jq curl
```
### 1. Get a Last.fm API Key
1. Visit https://www.last.fm/api/account/create
2. Fill in the application details:
- **Name**: Your preferred name (e.g., "OpenClaw Integration")
- **Description**: "Personal OpenClaw skill for Last.fm"
- **Application URL**: Leave blank or use your site
3. Save the **API Key** shown after creation
### 2. Configure OpenClaw
Add to `~/.openclaw/openclaw.json`:
```json5
{
skills: {
entries: {
lastfm: {
enabled: true,
apiKey: "YOUR_API_KEY_HERE", // convenience for LASTFM_API_KEY
env: {
LASTFM_API_KEY: "YOUR_API_KEY_HERE",
LASTFM_USERNAME: "YOUR_USERNAME"
}
}
}
}
}
```
**Note:** OpenClaw injects these env vars at runtime (not globally). Never commit secrets to git. If you run the agent in a sandboxed session, set the env vars under `agents.defaults.sandbox.docker.env` (or per-agent) because sandboxed skills do not inherit host env.
### 3. Install the Skill
**From ClawHub:**
```bash
clawhub install lastfm
```
**From local directory:**
```bash
# Copy skill to your workspace
cp -r lastfm ~/.openclaw/skills/
# Or place in workspace skills folder
cp -r lastfm ./skills/
```
### 4. Verify Installation
```
/lastfm profile
```
## Usage
### Read Commands (No Auth Required)
**Read-only endpoints require only `LASTFM_API_KEY` + `LASTFM_USERNAME`.**
| Command | Description | Example |
|---------|-------------|---------|
| `now-playing`, `np` | Current/last track | `/lastfm np` |
| `top-tracks [period]` | Top tracks by period | `/lastfm top-tracks 7day` |
| `top-artists [period]` | Top artists by period | `/lastfm top-artists 1month` |
| `top-albums [period]` | Top albums by period | `/lastfm top-albums overall` |
| `loved` | Loved tracks | `/lastfm loved` |
| `recent [limit]` | Recent tracks | `/lastfm recent 20` |
| `profile` | User profile | `/lastfm profile` |
### Time Periods
- `7day` - Last 7 days
- `1month` - Last 30 days
- `3month` - Last 90 days
- `6month` - Last 180 days
- `12month` - Last year
- `overall` - All time
### Write Commands (Auth Required)
**Write endpoints require `LASTFM_SESSION_KEY` + `LASTFM_API_SECRET`.**
See [Authentication Guide](references/auth-guide.md) for setup.
| Command | Description | Example |
|---------|-------------|---------|
| `love` | Love a track | `/lastfm love "Artist" "Track"` |
| `unlove` | Unlove a track | `/lastfm unlove "Artist" "Track"` |
## Examples
### Check What's Playing
```
User: What am I listening to on Last.fm?
šµ Now Playing:
"The Less I Know The Better" by Tame Impala
from Currents
```
### Weekly Top Tracks
```
User: Show me my top tracks this week
šµ Top Tracks (7 days):
1. "Blinding Lights" by The Weeknd (23 plays)
2. "Levitating" by Dua Lipa (18 plays)
3. "Save Your Tears" by The Weeknd (15 plays)
...
```
### Profile Stats
```
User: Show my Last.fm profile
šµ Last.fm Profile: musiclover
š 42,156 total scrobbles
š United States
š
Member since: Mar 2015
š last.fm/user/musiclover
```
## Security
This skill follows OpenClaw security best practices:
- **No hardcoded credentials** - All secrets via environment variables
- **No external endpoints** - Only connects to `ws.audioscrobbler.com`
- **No install hooks** - No lifecycle scripts or setup commands
- **No obfuscated code** - All files are human-readable
- **Minimal permissions** - Only requests what's needed
## Configuration Options
```json5
{
skills: {
entries: {
lastfm: {
enabled: true,
env: {
LASTFM_API_KEY: "required",
LASTFM_USERNAME: "required",
LASTFM_SESSION_KEY: "optional, for write ops",
LASTFM_API_SECRET: "optional, for write ops"
}
}
}
}
}
```
## Troubleshooting
### "Missing LASTFM_API_KEY"
Ensure your API key is set in `~/.openclaw/openclaw.json` under `skills.entries.lastfm.env.LASTFM_API_KEY`.
### "Invalid API key"
1. Verify your API key at https://www.last.fm/api/accounts
2. Check for typos in your configuration
3. Ensure the API key wasn't revoked
### "Rate limit exceeded"
Last.fm allows 5 requests/second. Wait a moment and try again.
### "Authentication failed for write operations"
Write operations require a session key. See [Authentication Guide](references/auth-guide.md).
## API Reference
See [API Endpoints](references/api-endpoints.md) for detailed API documentation.
## License
MIT
## Contributing
Issues and pull requests welcome at the repository.
```
### _meta.json
```json
{
"owner": "dennisooki",
"slug": "lastfm-openclaw",
"displayName": "Last.fm (OpenClaw)",
"latest": {
"version": "1.0.0",
"publishedAt": 1772088157435,
"commit": "https://github.com/openclaw/skills/commit/f85dd84126a05e4c0110a35c1ed013aa04955c17"
},
"history": []
}
```
### references/api-endpoints.md
```markdown
# Last.fm API Endpoints Reference
This document describes the Last.fm API endpoints used by this skill.
## Base URL
```
https://ws.audioscrobbler.com/2.0/
```
## Authentication
All read operations require only an API key. Write operations require a session key obtained through the authentication flow.
See [auth-guide.md](auth-guide.md) for authentication setup.
## Required Parameters
All requests must include:
| Parameter | Description |
|-----------|-------------|
| `api_key` | Your Last.fm API key |
| `method` | API method name |
| `format` | Response format (`json` recommended) |
---
## User Methods
### user.getInfo
Get information about a user profile.
**URL:** `?method=user.getInfo&user=USERNAME&api_key=KEY&format=json`
**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `user` | Yes | Last.fm username |
| `api_key` | Yes | API key |
**Response Fields:**
```json
{
"user": {
"name": "username",
"realname": "Real Name",
"url": "https://www.last.fm/user/username",
"image": [...],
"country": "Country",
"age": "25",
"gender": "m",
"subscriber": "0",
"playcount": "54189",
"playlists": "4",
"registered": {
"unixtime": "1037793040",
"#text": "2002-11-20 11:50"
}
}
}
```
---
### user.getRecentTracks
Get recent tracks, including now playing.
**URL:** `?method=user.getRecentTracks&user=USERNAME&api_key=KEY&format=json&limit=10`
**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `user` | Yes | Last.fm username |
| `api_key` | Yes | API key |
| `limit` | No | Number of results (default: 50, max: 200) |
| `page` | No | Page number |
| `from` | No | Unix timestamp start |
| `to` | No | Unix timestamp end |
| `extended` | No | Include extended data (0 or 1) |
**Now Playing Detection:**
The first track in `recenttracks.track[]` has `@attr.nowplaying="true"` if currently playing.
**Response Fields:**
```json
{
"recenttracks": {
"user": "username",
"track": [
{
"@attr": { "nowplaying": "true" },
"artist": { "mbid": "...", "#text": "Artist Name" },
"name": "Track Name",
"album": { "mbid": "", "#text": "Album Name" },
"url": "https://www.last.fm/music/...",
"date": { "uts": "1213031819", "#text": "9 Jun 2008, 17:16" }
}
]
}
}
```
---
### user.getTopTracks
Get top tracks for a user.
**URL:** `?method=user.getTopTracks&user=USERNAME&api_key=KEY&format=json&period=7day`
**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `user` | Yes | Last.fm username |
| `api_key` | Yes | API key |
| `period` | No | Time period (see below) |
| `limit` | No | Number of results (default: 50) |
| `page` | No | Page number |
**Period Values:**
| Value | Description |
|-------|-------------|
| `overall` | All time (default) |
| `7day` | Last 7 days |
| `1month` | Last 30 days |
| `3month` | Last 90 days |
| `6month` | Last 180 days |
| `12month` | Last year |
**Response Fields:**
```json
{
"toptracks": {
"user": "username",
"track": [
{
"@attr": { "rank": "1" },
"name": "Track Name",
"playcount": "42",
"artist": { "name": "Artist Name", "mbid": "..." },
"album": { "mbid": "..." },
"url": "https://www.last.fm/music/..."
}
]
}
}
```
---
### user.getTopArtists
Get top artists for a user.
**URL:** `?method=user.getTopArtists&user=USERNAME&api_key=KEY&format=json&period=7day`
**Parameters:**
Same as `user.getTopTracks` (minus `period` variation).
**Response Fields:**
```json
{
"topartists": {
"user": "username",
"artist": [
{
"@attr": { "rank": "1" },
"name": "Artist Name",
"playcount": "156",
"mbid": "...",
"url": "https://www.last.fm/music/..."
}
]
}
}
```
---
### user.getTopAlbums
Get top albums for a user.
**URL:** `?method=user.getTopAlbums&user=USERNAME&api_key=KEY&format=json&period=7day`
**Parameters:**
Same as `user.getTopTracks`.
**Response Fields:**
```json
{
"topalbums": {
"user": "username",
"album": [
{
"@attr": { "rank": "1" },
"name": "Album Name",
"playcount": "38",
"artist": { "name": "Artist Name", "mbid": "..." },
"mbid": "...",
"url": "https://www.last.fm/music/..."
}
]
}
}
```
---
### user.getLovedTracks
Get loved tracks for a user.
**URL:** `?method=user.getLovedTracks&user=USERNAME&api_key=KEY&format=json`
**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `user` | Yes | Last.fm username |
| `api_key` | Yes | API key |
| `limit` | No | Number of results (default: 50) |
| `page` | No | Page number |
**Response Fields:**
```json
{
"lovedtracks": {
"user": "username",
"track": [
{
"artist": { "name": "Artist Name", "mbid": "..." },
"name": "Track Name",
"mbid": "...",
"url": "https://www.last.fm/music/...",
"date": { "uts": "1213031819", "#text": "9 Jun 2008, 17:16" }
}
]
}
}
```
---
## Track Methods
### track.love
Love a track for a user session. **Requires authentication.**
**URL:** POST to `https://ws.audioscrobbler.com/2.0/`
**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `method` | Yes | `track.love` |
| `api_key` | Yes | API key |
| `sk` | Yes | Session key |
| `artist` | Yes | Artist name |
| `track` | Yes | Track name |
| `api_sig` | Yes | Method signature (see auth guide) |
**Response:**
```json
{
"lfm": {
"status": "ok"
}
}
```
---
### track.unlove
Remove a loved track. **Requires authentication.**
Same parameters and response as `track.love`.
---
## Error Codes
| Code | Description |
|------|-------------|
| 2 | Invalid service |
| 3 | Invalid method |
| 4 | Authentication failed |
| 5 | Invalid format |
| 6 | Invalid parameters |
| 7 | Invalid resource |
| 8 | Operation failed |
| 9 | Invalid session key |
| 10 | Invalid API key |
| 11 | Service offline |
| 13 | Invalid method signature |
| 16 | Temporary error |
| 26 | Suspended API key |
| 29 | Rate limit exceeded |
---
## Rate Limits
- **5 requests per second** per IP address
- Implement delays between requests if making multiple calls
- Error 29 indicates rate limit exceeded
---
## References
- Official API docs: https://www.last.fm/api
- Authentication: https://www.last.fm/api/webauth
```
### references/auth-guide.md
```markdown
# Last.fm Authentication Guide
This guide explains how to set up authentication for Last.fm write operations (love/unlove tracks, scrobble).
## Overview
Read operations only require an API key. Write operations require a session key obtained through Last.fm's authentication flow.
## Authentication Types
| Type | Use Case | Requires |
|------|----------|----------|
| API Key Only | Read operations | API key |
| Desktop Auth | Write operations (local app) | API key + secret |
| Web Auth | Write operations (web app) | API key + secret + callback URL |
This skill uses **Desktop Auth** for write operations.
---
## Step 1: Get API Key and Secret
1. Visit https://www.last.fm/api/account/create
2. Fill in application details:
- **Name**: Your preferred name
- **Description**: Personal OpenClaw skill
- **Application URL**: Leave blank or use your site
3. After creation, note both:
- **API Key** - Used for all requests
- **Shared Secret** - Used for signing write requests
## Step 2: Store Credentials
Add to `~/.openclaw/openclaw.json`:
```json5
{
skills: {
entries: {
lastfm: {
enabled: true,
env: {
LASTFM_API_KEY: "your_api_key",
LASTFM_API_SECRET: "your_shared_secret",
LASTFM_USERNAME: "your_username",
LASTFM_SESSION_KEY: "" // Will be filled after auth
}
}
}
}
}
```
**Security Note:** Never commit your API secret to a public repository.
---
## Step 3: Get Authentication Token
Request a token from Last.fm:
```bash
curl "https://ws.audioscrobbler.com/2.0/?method=auth.gettoken&api_key=YOUR_API_KEY&format=json"
```
Response:
```json
{
"token": "your_auth_token"
}
```
**Note:** Tokens expire after 60 minutes if not used.
---
## Step 4: Authorize the Token
1. Construct authorization URL:
```
https://www.last.fm/api/auth/?api_key=YOUR_API_KEY&token=YOUR_TOKEN
```
2. Open this URL in your browser
3. Log in to Last.fm and click "Yes, allow access"
4. You'll be redirected to a confirmation page
---
## Step 5: Get Session Key
After authorizing, exchange the token for a session key:
```bash
# First, create the method signature
# Signature = md5(api_key + method + token + secret)
# Example using common tools:
API_KEY="your_api_key"
API_SECRET="your_secret"
TOKEN="your_token"
# Create signature string (params in alphabetical order, without format)
SIG_STRING="api_key${API_KEY}methodauth.getSessiontoken${TOKEN}${API_SECRET}"
SIGNATURE=$(echo -n "$SIG_STRING" | md5sum | cut -d' ' -f1)
# Request session key
curl "https://ws.audioscrobbler.com/2.0/?method=auth.getSession&api_key=${API_KEY}&token=${TOKEN}&api_sig=${SIGNATURE}&format=json"
```
Response:
```json
{
"session": {
"name": "your_username",
"key": "your_session_key",
"subscriber": "0"
}
}
```
---
## Step 6: Store Session Key
Add the session key to your configuration:
```json5
{
skills: {
entries: {
lastfm: {
enabled: true,
env: {
LASTFM_API_KEY: "your_api_key",
LASTFM_API_SECRET: "your_secret",
LASTFM_USERNAME: "your_username",
LASTFM_SESSION_KEY: "your_session_key"
}
}
}
}
}
```
**Important:** Session keys do not expire unless the user revokes access.
---
## Creating Method Signatures
For write operations, you must create an API signature:
### Rules
1. Sort all parameters alphabetically (excluding `format`)
2. Concatenate: `param1value1param2value2...`
3. Append your API secret
4. Calculate MD5 hash of the string
### Example
For `track.love`:
```
Parameters: api_key=XXX, artist=Radiohead, method=track.love, sk=YYY, track=Creep
Sorted: api_keyXXXartistRadioheadmethodtrack.loveskYYYtrackCreep
With secret: api_keyXXXartistRadioheadmethodtrack.loveskYYYtrackCreepSECRET
Signature: md5("api_keyXXXartistRadioheadmethodtrack.loveskYYYtrackCreepSECRET")
```
### Code Example (Bash)
```bash
generate_signature() {
local params=("$@")
local secret="${LASTFM_API_SECRET}"
# Sort params alphabetically
IFS=$'\n' sorted=($(sort <<<"${params[*]}"))
unset IFS
# Concatenate
local sig_string=""
for param in "${sorted[@]}"; do
sig_string+="${param}"
done
sig_string+="${secret}"
# MD5 hash
echo -n "$sig_string" | md5sum | cut -d' ' -f1
}
# Usage
signature=$(generate_signature "api_key${LASTFM_API_KEY}" "artistRadiohead" "methodtrack.love" "sk${LASTFM_SESSION_KEY}" "trackCreep")
```
---
## Revoking Access
To revoke session key access:
1. Visit https://www.last.fm/settings/applications
2. Find your application
3. Click "Revoke access"
After revoking, you'll need to repeat the authentication flow.
---
## Troubleshooting
### "Invalid session key"
- Token expired before authorization (60 min limit)
- User revoked access
- Session key is incorrect in config
### "Invalid method signature"
- Parameters not sorted alphabetically
- Secret incorrect
- Parameter names/values concatenated incorrectly
### "Token has expired"
Tokens are single-use and expire after 60 minutes. Request a new token and start over.
---
## Security Notes
1. **Never share** your API secret or session key
2. **Never commit** secrets to version control
3. **Use environment variables** for all credentials
4. **Revoke access** if you suspect compromise
5. **Session keys are long-lived** - store them securely
```
### scripts/lastfm-api.sh
```bash
#!/bin/bash
set -euo pipefail
LASTFM_API_ROOT="https://ws.audioscrobbler.com/2.0/"
print_usage() {
cat <<EOF
Usage: lastfm-api.sh <command> [options]
Commands:
now-playing, np Get currently playing track
top-tracks [period] Get top tracks (period: 7day, 1month, 3month, 6month, 12month, overall)
top-artists [period] Get top artists
top-albums [period] Get top albums
loved Get loved tracks
recent [limit] Get recent tracks (default: 10)
profile Get user profile info
love <artist> <track> Love a track (requires session key)
unlove <artist> <track> Unlove a track (requires session key)
Environment Variables Required:
LASTFM_API_KEY Your Last.fm API key
LASTFM_USERNAME Your Last.fm username
Optional for Write Operations:
LASTFM_SESSION_KEY Session key for authenticated requests
LASTFM_API_SECRET API secret for signing requests
Examples:
LASTFM_API_KEY=xxx LASTFM_USERNAME=user ./lastfm-api.sh now-playing
LASTFM_API_KEY=xxx LASTFM_USERNAME=user ./lastfm-api.sh top-tracks 7day
LASTFM_API_KEY=xxx LASTFM_USERNAME=user LASTFM_SESSION_KEY=yyy LASTFM_API_SECRET=zzz ./lastfm-api.sh love "Radiohead" "Creep"
EOF
}
check_required_vars() {
if [[ -z "${LASTFM_API_KEY:-}" ]]; then
echo "Error: LASTFM_API_KEY not set" >&2
exit 1
fi
if [[ -z "${LASTFM_USERNAME:-}" ]]; then
echo "Error: LASTFM_USERNAME not set" >&2
exit 1
fi
}
check_bins() {
local bins=("curl" "jq")
for b in "${bins[@]}"; do
if ! command -v "$b" >/dev/null 2>&1; then
echo "Error: missing dependency '$b'" >&2
exit 1
fi
done
}
check_write_vars() {
if [[ -z "${LASTFM_SESSION_KEY:-}" ]]; then
echo "Error: LASTFM_SESSION_KEY not set (required for write operations)" >&2
echo "See references/auth-guide.md for authentication instructions" >&2
exit 1
fi
if [[ -z "${LASTFM_API_SECRET:-}" ]]; then
echo "Error: LASTFM_API_SECRET not set (required for signing write requests)" >&2
exit 1
fi
}
url_encode() {
local string="$1"
jq -nr --arg str "$string" '$str | @uri'
}
generate_signature() {
local params=("$@")
local secret="${LASTFM_API_SECRET}"
local sorted_params
sorted_params=$(printf '%s\n' "${params[@]}" | sort)
local sig_string=""
while IFS= read -r param; do
sig_string+="${param}"
done <<< "$sorted_params"
sig_string+="${secret}"
echo -n "$sig_string" | md5sum | cut -d' ' -f1
}
make_request() {
local method="$1"
shift
local extra_params=("$@")
local url="${LASTFM_API_ROOT}?method=${method}&user=$(url_encode "$LASTFM_USERNAME")&api_key=${LASTFM_API_KEY}&format=json"
for param in "${extra_params[@]}"; do
url+="&${param}"
done
curl -s "$url"
}
make_write_request() {
local method="$1"
local artist="$2"
local track="$3"
check_write_vars
local artist_enc
artist_enc=$(url_encode "$artist")
local track_enc
track_enc=$(url_encode "$track")
local api_sig
api_sig=$(generate_signature \
"api_key${LASTFM_API_KEY}" \
"artist${artist}" \
"method${method}" \
"sk${LASTFM_SESSION_KEY}" \
"track${track}"
)
local url="${LASTFM_API_ROOT}"
local data="method=${method}&api_key=${LASTFM_API_KEY}&artist=${artist_enc}&track=${track_enc}&sk=${LASTFM_SESSION_KEY}&api_sig=${api_sig}&format=json"
curl -s -X POST -d "$data" "$url"
}
format_now_playing() {
local json="$1"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
local track
track=$(echo "$json" | jq -r '.recenttracks.track[0]')
if [[ -z "$track" || "$track" == "null" ]]; then
echo "No recent tracks found"
exit 0
fi
local is_now_playing
is_now_playing=$(echo "$json" | jq -r '.recenttracks.track[0]."@attr".nowplaying // "false"')
local artist
artist=$(echo "$json" | jq -r '.recenttracks.track[0].artist."#text" // .recenttracks.track[0].artist')
local name
name=$(echo "$json" | jq -r '.recenttracks.track[0].name')
local album
album=$(echo "$json" | jq -r '.recenttracks.track[0].album."#text" // "Unknown Album"')
if [[ "$is_now_playing" == "true" ]]; then
echo "šµ Now Playing:"
else
local date
date=$(echo "$json" | jq -r '.recenttracks.track[0].date."#text" // "Unknown time"')
echo "šµ Last Played:"
fi
echo "\"${name}\" by ${artist}"
echo "from ${album}"
if [[ "$is_now_playing" != "true" ]]; then
echo "Listened: ${date}"
fi
}
format_top_tracks() {
local json="$1"
local period="${2:-overall}"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
echo "šµ Top Tracks (${period}):"
echo ""
echo "$json" | jq -r '.toptracks.track[:10][] | "\(.["@attr"].rank). \"\(.name)\" by \(.artist.name) (\(.playcount) plays)"'
}
format_top_artists() {
local json="$1"
local period="${2:-overall}"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
echo "šµ Top Artists (${period}):"
echo ""
echo "$json" | jq -r '.topartists.artist[:10][] | "\(.["@attr"].rank). \(.name) (\(.playcount) plays)"'
}
format_top_albums() {
local json="$1"
local period="${2:-overall}"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
echo "šµ Top Albums (${period}):"
echo ""
echo "$json" | jq -r '.topalbums.album[:10][] | "\(.["@attr"].rank). \"\(.name)\" by \(.artist.name) (\(.playcount) plays)"'
}
format_loved() {
local json="$1"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
local count
count=$(echo "$json" | jq -r '.lovedtracks."@attr".total // 0')
echo "šµ Loved Tracks (${count} total):"
echo ""
if [[ "$count" == "0" ]]; then
echo "No loved tracks found"
return
fi
echo "$json" | jq -r '.lovedtracks.track[:10][] | "- \"\(.name)\" by \(.artist.name)"'
}
format_recent() {
local json="$1"
local limit="${2:-10}"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
echo "šµ Recent Tracks:"
echo ""
echo "$json" | jq -r --argjson limit "$limit" '.recenttracks.track[:$limit][] | "- \"\(.name)\" by \(.artist."#text" // .artist) [\(.date."#text" // "now playing")]"'
}
format_profile() {
local json="$1"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
local name
name=$(echo "$json" | jq -r '.user.name')
local realname
realname=$(echo "$json" | jq -r '.user.realname // ""')
local playcount
playcount=$(echo "$json" | jq -r '.user.playcount')
local country
country=$(echo "$json" | jq -r '.user.country // "Unknown"')
local registered
registered=$(echo "$json" | jq -r '.user.registered."#text" // "Unknown"')
local url
url=$(echo "$json" | jq -r '.user.url')
echo "šµ Last.fm Profile: ${name}"
[[ -n "$realname" && "$realname" != "null" ]] && echo " ($realname)"
echo ""
echo "š ${playcount} total scrobbles"
echo "š ${country}"
echo "š
Member since: ${registered}"
echo "š ${url}"
}
format_write_response() {
local json="$1"
local action="$2"
if echo "$json" | jq -e '.error' > /dev/null 2>&1; then
echo "Error: $(echo "$json" | jq -r '.message // .error')"
exit 1
fi
echo "ā Track ${action}!"
}
validate_period() {
local period="$1"
local valid="7day 1month 3month 6month 12month overall"
if [[ ! " $valid " =~ " $period " ]]; then
echo "Error: Invalid period '$period'" >&2
echo "Valid periods: 7day, 1month, 3month, 6month, 12month, overall" >&2
exit 1
fi
}
validate_limit() {
local limit="$1"
if [[ ! "$limit" =~ ^[0-9]+$ ]]; then
echo "Error: limit must be a number" >&2
exit 1
fi
if (( limit < 1 || limit > 200 )); then
echo "Error: limit must be between 1 and 200" >&2
exit 1
fi
}
main() {
if [[ $# -lt 1 ]]; then
print_usage
exit 1
fi
local command="$1"
shift
case "$command" in
now-playing|np)
check_required_vars
check_bins
format_now_playing "$(make_request "user.getRecentTracks" "limit=1")"
;;
top-tracks)
check_required_vars
check_bins
local period="${1:-overall}"
validate_period "$period"
format_top_tracks "$(make_request "user.getTopTracks" "period=${period}")" "$period"
;;
top-artists)
check_required_vars
check_bins
local period="${1:-overall}"
validate_period "$period"
format_top_artists "$(make_request "user.getTopArtists" "period=${period}")" "$period"
;;
top-albums)
check_required_vars
check_bins
local period="${1:-overall}"
validate_period "$period"
format_top_albums "$(make_request "user.getTopAlbums" "period=${period}")" "$period"
;;
loved)
check_required_vars
check_bins
format_loved "$(make_request "user.getLovedTracks")"
;;
recent)
check_required_vars
check_bins
local limit="${1:-10}"
validate_limit "$limit"
format_recent "$(make_request "user.getRecentTracks" "limit=${limit}")" "$limit"
;;
profile)
check_required_vars
check_bins
format_profile "$(make_request "user.getInfo")"
;;
love)
if [[ $# -lt 2 ]]; then
echo "Usage: lastfm-api.sh love <artist> <track>" >&2
exit 1
fi
check_required_vars
check_bins
format_write_response "$(make_write_request "track.love" "$1" "$2")" "loved"
;;
unlove)
if [[ $# -lt 2 ]]; then
echo "Usage: lastfm-api.sh unlove <artist> <track>" >&2
exit 1
fi
check_required_vars
check_bins
format_write_response "$(make_write_request "track.unlove" "$1" "$2")" "unloved"
;;
help|--help|-h)
print_usage
;;
*)
echo "Error: Unknown command '$command'" >&2
echo "Run 'lastfm-api.sh help' for usage" >&2
exit 1
;;
esac
}
main "$@"
```