@hoobyt/grooveid-sdk
v1.0.0
Published
TypeScript/JavaScript client for the GrooveID audio catalog matching API
Maintainers
Readme
songmatch-sdk
TypeScript client library for the SongMatch audio recognition API.
Table of Contents
- Installation
- Quick Start
- Configuration
- API Reference
- File Input
- Error Handling
- Auto-Retry
- Types Reference
- Requirements
Installation
npm install songmatch-sdkQuick 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); // 0client.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-Afterheader from the response (falls back to 60 seconds if absent). - It waits for the specified duration, then retries.
- Retries happen up to
maxRetriestimes (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
fetchandFormData) - No runtime dependencies -- the SDK uses only Node.js built-in modules
