Skip to content

Repository files navigation

Scrobbletail

A typed, runtime-validating TypeScript client for the Last.fm API. Scrobbletail covers all 57 currently documented, non-deprecated methods, groups them into nine namespaces, and validates every successful response with TypeBox.

  • Complete Last.fm API coverage
  • Inferred TypeScript response types
  • TypeBox schemas exported for runtime validation
  • Stable Last.fm request signing
  • Web, desktop, and mobile-session authentication helpers
  • ESM-only package for Bun, Node.js 18.17+, and Web API-compatible runtimes
  • Fetch-compatible transport powered by ofetch

Scrobbletail is an independent project. It is not affiliated with, endorsed by, or sponsored by Last.fm or Paramount Global.

Contents

Install

npm install scrobbletail
bun add scrobbletail

Scrobbletail is ESM-only and includes TypeScript declarations.

Get API credentials

Create a Last.fm API application from the Last.fm API account page. Public read methods need an API key. Signed application and user-session methods also need the API secret.

Keep API secrets, session keys, and user passwords out of browser bundles, source control, and logs.

Quick start

Create a client with an API key and call a namespace method:

import { createScrobbletailClient } from "scrobbletail";

const lastfm = createScrobbletailClient({
  apiKey: process.env.LASTFM_API_KEY!,
});

const chart = await lastfm.chart.getTopArtists({ limit: 10 });

for (const artist of chart.artist) {
  console.log(artist.name, artist.url);
}

Response values use Last.fm's JSON representation. Counts, ranks, booleans, and timestamps returned by Last.fm are generally strings rather than numbers.

Usage examples

Search the catalog

const results = await lastfm.track.search("Teardrop", {
  artist: "Massive Attack",
  limit: 5,
});

for (const track of results.trackmatches.track) {
  console.log(`${track.artist}${track.name}`);
}

Search methods are available for albums, artists, and tracks:

const albums = await lastfm.album.search("Mezzanine", { limit: 5 });
const artists = await lastfm.artist.search("Portishead", { limit: 5 });
const tracks = await lastfm.track.search("Jóga", { artist: "Björk" });

Look up music by name or MBID

Album, artist, and track methods accept an exclusive identifier object. Supply names or an MBID, not both:

const album = await lastfm.album.getInfo({
  artist: "Massive Attack",
  album: "Mezzanine",
});

const artist = await lastfm.artist.getInfo({
  mbid: "10adbe2f-6c87-4cc8-a679-39c13f7b3d63",
});

const track = await lastfm.track.getInfo(
  { artist: "Björk", track: "Jóga" },
  { autocorrect: true, username: "lastfm-user" },
);

console.log(album.name, artist.name, track.listeners);

TypeScript rejects identifier objects that mix an MBID with artist, album, or track names.

Browse related music

const [similar, topAlbums, taggedTracks] = await Promise.all([
  lastfm.artist.getSimilar({ artist: "Portishead" }, { limit: 10 }),
  lastfm.artist.getTopAlbums(
    { artist: "Portishead" },
    { limit: 10, autocorrect: true },
  ),
  lastfm.tag.getTopTracks("trip-hop", { limit: 10 }),
]);

console.log(similar.artist.map((item) => item.name));
console.log(topAlbums.album.map((item) => item.name));
console.log(taggedTracks.track.map((item) => item.name));

Read user listening history

const recent = await lastfm.user.getRecentTracks("lastfm-user", {
  from: new Date("2026-01-01T00:00:00Z"),
  to: new Date("2026-02-01T00:00:00Z"),
  extended: true,
  limit: 50,
});

for (const track of recent.track) {
  const playedAt = track.date?.uts ?? "now playing";
  console.log(`${track.artist}${track.name} (${playedAt})`);
}

from and to accept a Date or a non-negative integer Unix timestamp in seconds.

Top-item methods accept Last.fm reporting periods:

const topTracks = await lastfm.user.getTopTracks("lastfm-user", {
  period: "1month",
  limit: 25,
});

console.log(topTracks.track.map((item) => item.name));

Abort a request

Every endpoint accepts signal and custom headers in its final options object:

const result = await lastfm.chart.getTopTracks({
  limit: 20,
  signal: AbortSignal.timeout(5_000),
  headers: {
    "x-request-id": crypto.randomUUID(),
  },
});

Scrobbletail overwrites protected transport headers such as Accept, Content-Type, and User-Agent where required.

Authentication

Last.fm authorization creates a long-lived session key. Scrobbletail returns the key but never stores it; persistence belongs to your application.

Web flow

Create an authorization URL, redirect the user to it, then exchange the callback token:

import {
  createAuthorizationUrl,
  createScrobbletailClient,
} from "scrobbletail";

const apiKey = process.env.LASTFM_API_KEY!;
const apiSecret = process.env.LASTFM_API_SECRET!;

const authorizationUrl = createAuthorizationUrl({
  apiKey,
  callbackUrl: "https://music.example.com/auth/lastfm/callback",
});

// Redirect the user to authorizationUrl.

In the callback handler:

const authClient = createScrobbletailClient({ apiKey, apiSecret });

async function handleLastFmCallback(request: Request) {
  const token = new URL(request.url).searchParams.get("token");
  if (!token) throw new Error("Last.fm callback did not include a token");

  const session = await authClient.auth.getSession(token);
  await secureSessionStore.write(session.key);

  return session.name;
}

The authorization endpoint must use HTTPS. Callback URLs may use HTTP only for local development.

Desktop flow

Desktop applications can request a token before opening the authorization page:

const authClient = createScrobbletailClient({ apiKey, apiSecret });
const token = await authClient.auth.getToken();
const authorizationUrl = createAuthorizationUrl({ apiKey, token });

await openInBrowser(authorizationUrl);
await waitForUserApproval();

const session = await authClient.auth.getSession(token);
await secureSessionStore.write(session.key);

Mobile session

auth.getMobileSession sends a plaintext password in a signed HTTPS form body. Use it only from a trusted server or native application—never from browser code.

const session = await authClient.auth.getMobileSession(
  usernameFromSecureInput,
  passwordFromSecureInput,
);

Scrobbletail intentionally excludes the deprecated authToken parameter.

Authenticated actions

Create a signed-session client with an API secret and either a fixed session key or a lazy provider. The provider takes precedence and runs only for methods that require a session.

const lastfm = createScrobbletailClient({
  apiKey: process.env.LASTFM_API_KEY!,
  apiSecret: process.env.LASTFM_API_SECRET!,
  getSessionKey: async () => secureSessionStore.read(),
});

await lastfm.track.love("Massive Attack", "Teardrop");
await lastfm.track.addTags("Massive Attack", "Teardrop", [
  "trip-hop",
  "favorite",
]);
await lastfm.album.removeTag("Massive Attack", "Mezzanine", "favorite");

Tag mutations accept 1–10 tags. Empty lists and larger batches fail locally before a network request.

Four read methods use the active session when their optional user is omitted:

const profile = await lastfm.user.getInfo();
const tags = await lastfm.track.getTags({
  artist: "Massive Attack",
  track: "Teardrop",
});

Pass a user explicitly to use ordinary API-key mode:

const profile = await lastfm.user.getInfo("lastfm-user");
const tags = await lastfm.track.getTags(
  { artist: "Massive Attack", track: "Teardrop" },
  { user: "lastfm-user" },
);

The other two methods with this behavior are album.getTags and artist.getTags.

Scrobbling

Update the active session user's now-playing track:

const nowPlaying = await lastfm.track.updateNowPlaying({
  artist: "Massive Attack",
  track: "Teardrop",
  album: "Mezzanine",
  albumArtist: "Massive Attack",
  duration: 330,
});

if (nowPlaying.ignoredMessage.code !== "0") {
  console.error(nowPlaying.ignoredMessage["#text"]);
}

Submit one scrobble or a batch of up to 50:

const result = await lastfm.track.scrobble([
  {
    artist: "Portishead",
    track: "Roads",
    album: "Dummy",
    timestamp: new Date("2026-01-10T20:30:00Z"),
  },
  {
    artist: "Björk",
    track: "Jóga",
    album: "Homogenic",
    timestamp: 1_768_077_240,
    chosenByUser: true,
  },
]);

console.log(
  `Accepted ${result["@attr"].accepted}; ignored ${result["@attr"].ignored}`,
);

Scrobble timestamps must be valid Date objects or non-negative integer Unix seconds. Every request uses Last.fm's indexed form fields, including single-item submissions. Retry policy and offline persistence are intentionally left to the consuming application.

Configuration

const lastfm = createScrobbletailClient({
  apiKey: process.env.LASTFM_API_KEY!,
  apiSecret: process.env.LASTFM_API_SECRET,
  sessionKey: sessionKeyFromSecureStorage,
  getSessionKey: async () => secureSessionStore.read(),
  baseUrl: "https://lastfm-proxy.example/2.0/",
  userAgent: "my-music-app/1.0",
  timeout: 10_000,
  fetch: customFetch,
});
Option Required Purpose
apiKey Yes Last.fm application API key sent with every request.
apiSecret For signed methods Signs application and session requests. Never sent as a request parameter.
sessionKey For session methods Fixed Last.fm user-session key.
getSessionKey No Lazy session-key provider; takes precedence over sessionKey.
baseUrl No HTTPS Last.fm-compatible API root. Defaults to https://ws.audioscrobbler.com/2.0/.
userAgent No Requested User-Agent value. Defaults to scrobbletail.
timeout No Request timeout in milliseconds. Omit to leave timeout handling unset.
fetch No Fetch-compatible transport for proxies, custom networking, or tests.

Browsers may strip the forbidden User-Agent header. Browser applications that require guaranteed identification should use a custom fetch or an HTTPS proxy.

Undefined request parameters are omitted. 0, false, and empty strings are preserved. Automatic retries are not performed.

Errors

import {
  LastFmApiError,
  ScrobbletailResponseValidationError,
  ScrobbletailTransportError,
} from "scrobbletail";

try {
  await lastfm.track.love("Massive Attack", "Teardrop");
} catch (error) {
  if (error instanceof LastFmApiError) {
    console.error(error.code, error.method, error.status, error.message);
  } else if (error instanceof ScrobbletailResponseValidationError) {
    console.error(error.method, error.issues);
  } else if (error instanceof ScrobbletailTransportError) {
    console.error(error.method, error.status);
  } else {
    throw error;
  }
}

LastFmApiError covers provider errors, including errors returned in an HTTP 200 response. ScrobbletailResponseValidationError means a successful response did not match its endpoint schema. ScrobbletailTransportError reports redacted network and non-provider HTTP failures without retaining signed URLs, request parameters, credentials, or underlying causes.

Schemas and types

Every response schema and its inferred type are exported from the package root:

import { Value } from "@sinclair/typebox/value";
import {
  TrackInfoSchema,
  type Scrobble,
  type TrackInfo,
} from "scrobbletail";

const cached: unknown = await loadCachedValue();

if (Value.Check(TrackInfoSchema, cached)) {
  const track: TrackInfo = cached;
  console.log(track.name);
}

const queuedScrobble = {
  artist: "Massive Attack",
  track: "Teardrop",
  timestamp: new Date(),
} satisfies Scrobble;

The same schemas validate live API responses before Scrobbletail returns them.

API coverage

The client exposes exactly nine namespaces and 57 methods:

Namespace Methods Purpose
album 6 Album metadata, tags, and search.
artist 10 Artist metadata, corrections, similarity, charts, tags, and search.
auth 3 Tokens and Last.fm sessions.
chart 3 Global top artists, tags, and tracks.
geo 2 Country and location charts.
library 1 User library artists.
tag 7 Tag metadata, similarity, charts, and weekly ranges.
track 12 Track metadata, tags, love state, search, now playing, and scrobbling.
user 13 Profiles, friends, listening history, personal tags, and charts.

See API_COVERAGE.md for the exact method inventory and authentication mode of every endpoint.

Development

bun install
bun run typecheck
bun run test
bun run coverage
bun run build
bun run pack:dry

Publishing is triggered by a v* Git tag through the npm workflow in .github/workflows/npm-publish.yml.

About

A typed, runtime-validating TypeScript client for the Last.fm API.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages