npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

scrobbletail

v0.1.2

Published

A typed TypeScript client for the Last.fm API.

Readme

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.