Back to skills
SkillHub ClubShip Full StackFull Stack

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.

Stars
3,084
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C65.6

Install command

npx @skill-hub/cli install openclaw-skills-lastfm-openclaw

Repository

openclaw/skills

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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 "$@"

```

lastfm | SkillHub