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

@hoobyt/grooveid-sdk

v1.0.0

Published

TypeScript/JavaScript client for the GrooveID audio catalog matching API

Readme

songmatch-sdk

TypeScript Zero Dependencies Node 18+

TypeScript client library for the SongMatch audio recognition API.


Table of Contents


Installation

npm install songmatch-sdk

Quick Start

import { SongMatchClient } from 'songmatch-sdk';

const client = new SongMatchClient({
  apiKey: 'sm_a1b2c3d4e5f6...',
  baseUrl: 'http://localhost:4000',
});

// Create a library
const library = await client.libraries.create({ name: 'My Catalog' });

// Upload a song
const song = await client.songs.upload(library.id, '/path/to/song.mp3');

// Recognize an audio snippet
const result = await client.recognize(library.id, '/path/to/snippet.wav');

if (result.matchFound) {
  console.log(`Matched: ${result.matchedId} (${result.confidence}% confidence)`);
  console.log(result.metadata); // iTunes metadata, if available
} else {
  console.log('No match found');
}

Configuration

Pass a SongMatchClientOptions object to the constructor:

| Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | apiKey | string | Yes | -- | Your SongMatch API key (e.g. sm_a1b2c3d4...) | | baseUrl | string | No | 'http://localhost:4000' | Base URL of the SongMatch API server | | maxRetries | number | No | 3 | Maximum number of retries on 429 (rate limited) responses |

const client = new SongMatchClient({
  apiKey: process.env.SONGMATCH_API_KEY!,
  baseUrl: 'https://api.example.com',
  maxRetries: 5,
});

The constructor throws if apiKey is missing or not a non-empty string.


API Reference

Health

client.health(): Promise<HealthResponse>

Check server availability. No authentication required.

Returns: HealthResponse with status, service, and version fields.

Example:

const health = await client.health();
console.log(health.status);  // "ok"
console.log(health.version); // "1.0.0"

Libraries

A library is an isolated collection of songs. Recognition always runs against a single library.

client.libraries.create(params: { name: string }): Promise<Library>

Create a new library.

Parameters:

| Name | Type | Required | Description | |------|------|----------|-------------| | params.name | string | Yes | Display name for the library |

Returns: The created Library object.

Throws: SongMatchError with code BAD_REQUEST if name is missing.

Example:

const library = await client.libraries.create({ name: 'Radio Monitoring' });
console.log(library.id);        // "c7f3e8a1-4b2d-4f9e-8c1a-6d3b5e7f9a2c"
console.log(library.songCount); // 0

client.libraries.list(): Promise<Library[]>

List all libraries belonging to your API key.

Returns: Array of Library objects.

Example:

const libraries = await client.libraries.list();
for (const lib of libraries) {
  console.log(`${lib.name}: ${lib.songCount} songs`);
}

client.libraries.delete(id: string): Promise<void>

Delete a library and all songs within it. This action is irreversible.

Parameters:

| Name | Type | Required | Description | |------|------|----------|-------------| | id | string | Yes | Library ID |

Throws: SongMatchError with code NOT_FOUND if the library doesn't exist or belongs to a different API key.

Example:

await client.libraries.delete('c7f3e8a1-4b2d-4f9e-8c1a-6d3b5e7f9a2c');

Songs

Songs are audio files uploaded to a library. On upload, the server extracts a spectral fingerprint and stores it. The original audio file is not retained.

client.songs.upload(libraryId: string, file: FileInput, options?: UploadOptions): Promise<Song>

Upload an audio file to a library for fingerprinting.

Parameters:

| Name | Type | Required | Description | |------|------|----------|-------------| | libraryId | string | Yes | Target library ID | | file | FileInput | Yes | Audio file -- see File Input | | options.analysisDuration | number | No | Seconds of audio to fingerprint. Server default: 45 |

Returns: The created Song object.

Throws:

| Code | Cause | |------|-------| | BAD_REQUEST | No file provided, or file exceeds 10MB | | NOT_FOUND | Library doesn't exist or belongs to a different API key | | SERVER_ERROR | Audio decoding failure (corrupt or unsupported format) |

Example:

// Upload with default analysis duration (45s)
const song = await client.songs.upload(library.id, '/path/to/song.mp3');

// Upload with custom analysis duration
const song = await client.songs.upload(library.id, '/path/to/long-track.mp3', {
  analysisDuration: 120,
});

console.log(song.name);        // "song" (derived from filename)
console.log(song.durationStr); // "0:45"

client.songs.list(libraryId: string): Promise<Song[]>

List all songs in a library. Fingerprint data is excluded from the response.

Parameters:

| Name | Type | Required | Description | |------|------|----------|-------------| | libraryId | string | Yes | Library ID |

Returns: Array of Song objects.

Throws: SongMatchError with code NOT_FOUND if the library doesn't exist.

Example:

const songs = await client.songs.list(library.id);
for (const song of songs) {
  console.log(`${song.name} (${song.durationStr})`);
}

client.songs.delete(libraryId: string, songId: string): Promise<void>

Remove a song from a library. The fingerprint is permanently deleted.

Parameters:

| Name | Type | Required | Description | |------|------|----------|-------------| | libraryId | string | Yes | Library ID | | songId | string | Yes | Song ID |

Throws: SongMatchError with code NOT_FOUND if the song or library doesn't exist.

Example:

await client.songs.delete(library.id, song.id);

Recognition

client.recognize(libraryId: string, file: FileInput, options?: RecognizeOptions): Promise<RecognitionResult>

Upload an audio snippet to match against a library. Returns the best match (if any), a confidence score, and optional iTunes metadata.

Parameters:

| Name | Type | Required | Description | |------|------|----------|-------------| | libraryId | string | Yes | Library to match against | | file | FileInput | Yes | Audio snippet -- see File Input | | options.threshold | number | No | Minimum confidence (0-100) to count as a match. Server default: 30 | | options.frequencyTolerance | number | No | Bin tolerance for band matching. Server default: 1. Higher = more lenient. |

Returns: A RecognitionResult object.

Throws:

| Code | Cause | |------|-------| | BAD_REQUEST | No file provided, or file exceeds 5MB | | NOT_FOUND | Library doesn't exist or belongs to a different API key | | SERVER_ERROR | Internal error during fingerprinting or matching |

Example:

const result = await client.recognize(library.id, '/path/to/snippet.wav');

if (result.matchFound) {
  console.log(`Match: ${result.matchedId}`);
  console.log(`Confidence: ${result.confidence}%`);
  console.log(`Reason: ${result.reasoning}`);

  if (result.metadata) {
    console.log(`Track: ${result.metadata.trackName}`);
    console.log(`Artist: ${result.metadata.artistName}`);
  }
}

With custom thresholds:

const result = await client.recognize(library.id, buffer, {
  threshold: 50,          // Require higher confidence
  frequencyTolerance: 2,  // More lenient bin matching
});

File Input

All methods that accept audio files use the FileInput type:

type FileInput = string | Buffer | ReadStream | Blob;

Four input types are supported:

File path (string)

Pass an absolute or relative path. The file is read synchronously and the filename is inferred from the path.

const song = await client.songs.upload(library.id, '/path/to/song.mp3');

Buffer

Pass a Node.js Buffer. The filename defaults to the field name ("file").

import { readFileSync } from 'node:fs';

const buffer = readFileSync('/path/to/song.mp3');
const song = await client.songs.upload(library.id, buffer);

ReadStream

Pass a ReadStream created with fs.createReadStream. The stream must have a string path property (which it does by default). The file is read synchronously from that path.

import { createReadStream } from 'node:fs';

const stream = createReadStream('/path/to/song.mp3');
const song = await client.songs.upload(library.id, stream);

Blob

Pass a Blob or File instance directly. If the value has a name property (like File does), that is used as the filename.

const blob = new Blob([audioData], { type: 'audio/wav' });
const result = await client.recognize(library.id, blob);

Error Handling

All API errors are thrown as SongMatchError instances:

import { SongMatchClient, SongMatchError } from 'songmatch-sdk';

try {
  await client.libraries.create({ name: '' });
} catch (error) {
  if (error instanceof SongMatchError) {
    console.log(error.status);  // 400
    console.log(error.code);    // "BAD_REQUEST"
    console.log(error.message); // "Missing required field: name"
  }
}

Error codes

| HTTP Status | code | Meaning | |-------------|--------|---------| | 400 | BAD_REQUEST | Missing or invalid parameters | | 401 | UNAUTHORIZED | Missing X-API-Key header | | 403 | FORBIDDEN | Invalid or revoked API key | | 404 | NOT_FOUND | Resource doesn't exist or belongs to a different key | | 429 | RATE_LIMITED | Too many requests -- automatically retried (see Auto-Retry) | | 5xx | SERVER_ERROR | Server-side error |


Auto-Retry

The client automatically retries requests that receive a 429 Too Many Requests response.

  • The client reads the Retry-After header from the response (falls back to 60 seconds if absent).
  • It waits for the specified duration, then retries.
  • Retries happen up to maxRetries times (default: 3).
  • Only 429 responses trigger retries. All other errors are thrown immediately.

If all retries are exhausted, a SongMatchError with code RATE_LIMITED is thrown.

// Configure retry behavior
const client = new SongMatchClient({
  apiKey: 'sm_...',
  maxRetries: 5, // Up to 5 retries on rate limiting
});

Types Reference

All types are exported from the package and available for import:

import type {
  SongMatchClientOptions,
  Library,
  Song,
  RecognitionResult,
  iTunesMetadata,
  HealthResponse,
  UploadOptions,
  RecognizeOptions,
  FileInput,
} from 'songmatch-sdk';

SongMatchClientOptions

| Field | Type | Description | |-------|------|-------------| | apiKey | string | API key (required) | | baseUrl | string? | Server URL. Default: 'http://localhost:4000' | | maxRetries | number? | Max 429 retries. Default: 3 |

Library

| Field | Type | Description | |-------|------|-------------| | id | string | Unique library ID | | tenantId | string? | Owning tenant ID | | name | string | Display name | | createdAt | string | ISO 8601 timestamp | | songCount | number | Number of songs in the library |

Song

| Field | Type | Description | |-------|------|-------------| | id | string | Unique song ID | | name | string | Song name (derived from filename) | | durationStr | string | Formatted duration (e.g. "0:45") | | uploadedAt | string | ISO 8601 timestamp |

RecognitionResult

| Field | Type | Description | |-------|------|-------------| | matchFound | boolean | Whether a match was found above the threshold | | matchedId | string \| null | ID of the matched song, or null | | confidence | number | Match confidence (0-100) | | reasoning | string | Human-readable explanation | | metadata | iTunesMetadata \| null? | iTunes metadata, if available |

iTunesMetadata

| Field | Type | Description | |-------|------|-------------| | artworkUrl | string | Album artwork URL | | trackName | string | Track name | | artistName | string | Artist name | | albumName | string | Album name | | externalUrl | string | Apple Music URL |

HealthResponse

| Field | Type | Description | |-------|------|-------------| | status | string | Server status (e.g. "ok") | | service | string | Service name | | version | string | API version |

UploadOptions

| Field | Type | Description | |-------|------|-------------| | analysisDuration | number? | Seconds of audio to fingerprint |

RecognizeOptions

| Field | Type | Description | |-------|------|-------------| | threshold | number? | Minimum confidence (0-100) for a match | | frequencyTolerance | number? | Bin tolerance for frequency band matching |

FileInput

type FileInput = string | Buffer | ReadStream | Blob;

Requirements

  • Node.js 18+ (uses native fetch and FormData)
  • No runtime dependencies -- the SDK uses only Node.js built-in modules