dsp-normalizer
v0.1.0
Published
Universal music platform data normalizer — normalize raw API responses from Spotify, Apple Music, YouTube, Deezer, MusicBrainz, ListenBrainz, Last.fm, and SoundCloud into a unified typed schema.
Maintainers
Readme
dsp-normalizer
Universal music platform data normalizer. Transforms raw API responses from 8 DSPs into a unified, typed schema.
Zero runtime dependencies. Pure functions. Node 18+, browsers, edge runtimes.
Supported Platforms
| Platform | Artist | Track | Album | |----------|--------|-------|-------| | Spotify | ✅ | ✅ | ✅ | | Apple Music | ✅ | ✅ | ✅ | | YouTube | ✅ | ✅ | ✅ | | Deezer | ✅ | ✅ | ✅ | | MusicBrainz | ✅ | ✅ | ✅ | | Last.fm | ✅ | ✅ | ✅ | | ListenBrainz | ✅ | — | — | | SoundCloud | ✅ | ✅ | — |
Installation
npm install dsp-normalizer
# or
pnpm add dsp-normalizer
# or
yarn add dsp-normalizerQuick Start
import { normalizeArtist, normalizeTrack, normalizeAlbum } from 'dsp-normalizer'
// Use the dispatcher function with any platform
const artist = normalizeArtist('spotify', spotifyApiResponse)
const track = normalizeTrack('deezer', deezerApiResponse)
const album = normalizeAlbum('apple-music', appleMusicApiResponse)Platform Examples
Spotify
import { normalizeArtist, normalizeTrack, normalizeAlbum } from 'dsp-normalizer'
// Artist
const spotifyArtist = {
id: '4Z8W4fKeB5YxbusRsdQVPb',
name: 'Radiohead',
genres: ['alternative rock', 'art rock'],
images: [
{ url: 'https://i.scdn.co/image/large.jpg', width: 640, height: 640 },
{ url: 'https://i.scdn.co/image/small.jpg', width: 160, height: 160 }
],
external_urls: { spotify: 'https://open.spotify.com/artist/4Z8W4fKeB5YxbusRsdQVPb' }
}
const artist = normalizeArtist('spotify', spotifyArtist)
// → { id: '4Z8W4fKeB5YxbusRsdQVPb', name: 'Radiohead', platformIds: { spotify: '...' }, ... }Apple Music
import { normalizeArtist } from 'dsp-normalizer'
const appleArtist = {
id: '153945',
type": 'artists',
attributes: {
name: 'Radiohead',
genreNames: ['Alternative', 'Rock'],
artwork: { url: 'https://is1-ssl.mzstatic.com/image/thumb/{w}x{h}bb.jpg' }
}
}
const artist = normalizeArtist('apple-music', appleArtist)
// → { id: '153945', name: 'Radiohead', platformIds: { appleMusic: '153945' }, ... }YouTube
import { normalizeArtist, normalizeTrack } from 'dsp-normalizer'
// YouTube uses "channels" for artists
const ytChannel = {
id: 'UCqJncRxHBvInVvjZ4RyKvrg',
snippet: {
title: 'Bonobo',
description: 'Official YouTube channel',
thumbnails: {
high: { url: 'https://yt3.ggpht.com/high.jpg', width: 800, height: 800 },
default: { url: 'https://yt3.ggpht.com/default.jpg', width: 88, height: 88 }
}
},
statistics: {
subscriberCount: '892453',
viewCount: '456789012'
}
}
const artist = normalizeArtist('youtube', ytChannel)
// → { id: 'UCqJncRxHBvInVvjZ4RyKvrg', name: 'Bonobo',
// metrics: { youtubeSubscribers: 892453, youtubeTotalViews: 456789012 }, ... }Deezer
import { normalizeArtist } from 'dsp-normalizer'
const deezerArtist = {
id: 1,
name: 'Daft Punk',
link: 'https://www.deezer.com/artist/1',
picture_xl: 'https://e-cdns-images.dzcdn.net/images/artist/xl.jpg',
nb_fan: 4500000,
genres: { data: [{ name: 'Electronic' }, { name: 'Dance' }] }
}
const artist = normalizeArtist('deezer', deezerArtist)
// → { id: '1', name: 'Daft Punk', platformIds: { deezer: '1' },
// metrics: { deezerFans: 4500000 }, genres: ['Electronic', 'Dance'], ... }MusicBrainz
import { normalizeArtist } from 'dsp-normalizer'
const mbArtist = {
id: '056e4f3e-d505-4dad-8ec1-d04f521cbb56',
name: 'Daft Punk',
'type-id': 'b6e035f4-3ce9-331c-97df-8339eeaacadd',
type: 'Group',
country: 'FR',
'life-span': { begin: '1992', end: '2021-02-22', ended: true },
tags: [{ name: 'electronic', count: 50 }, { name: 'house', count: 30 }]
}
const artist = normalizeArtist('musicbrainz', mbArtist)
// → { id: '056e4f3e-d505-4dad-8ec1-d04f521cbb56', name: 'Daft Punk',
// platformIds: { musicbrainz: '056e4f3e-d505-4dad-8ec1-d04f521cbb56' },
// country: 'FR', activeFrom: '1992', activeTo: '2021-02-22', ... }Last.fm
import { normalizeArtist } from 'dsp-normalizer'
const lastfmArtist = {
name: 'Daft Punk',
mbid: '056e4f3e-d505-4dad-8ec1-d04f521cbb56',
url: 'https://www.last.fm/music/Daft+Punk',
listeners: '2847293',
playcount: '127894562',
image: [
{ '#text': 'https://lastfm.freetls.fastly.net/i/u/small.jpg', size: 'small' },
{ '#text': 'https://lastfm.freetls.fastly.net/i/u/large.jpg', size: 'large' }
],
tags: { tag: [{ name: 'electronic' }, { name: 'dance' }] }
}
const artist = normalizeArtist('lastfm', lastfmArtist)
// → { name: 'Daft Punk', platformIds: { musicbrainz: '...', lastfm: 'Daft+Punk' },
// metrics: { lastfmListeners: 2847293, lastfmPlayCount: 127894562 }, ... }ListenBrainz
import { normalizeArtist } from 'dsp-normalizer'
const listenbrainzArtist = {
id: 'd2d6e6ad-6b3e-4f5a-bc72-c8e6f8a9b0c1',
name: 'Bonobo',
listen_count: 847293,
mbid: '38c890f5-4d28-41b4-93d7-2c4e6d8d1a9e',
images: [
{ url: 'https://listenbrainz.org/artist/.../500.jpg', size: 'large' },
{ url: 'https://listenbrainz.org/artist/.../250.jpg', size: 'medium' }
],
external_urls: {
spotify: 'https://open.spotify.com/artist/0cmWgDluPb1DKYsWuO4fzd',
youtube: 'https://music.youtube.com/channel/UCrJHFwqJ3rF9FP5vY6s-cOg'
},
tags: ['electronic', 'downtempo', 'trip-hop']
}
const artist = normalizeArtist('listenbrainz', listenbrainzArtist)
// → { id: 'd2d6e6ad-...', name: 'Bonobo',
// platformIds: { listenbrainz: 'd2d6e6ad-...', musicbrainz: '38c890f5-...' },
// metrics: { listenbrainzListenCount: 847293 }, tags: ['electronic', ...], ... }SoundCloud
import { normalizeArtist, normalizeTrack } from 'dsp-normalizer'
// SoundCloud uses "users" for artists
const scUser = {
id: 3207,
username: 'Bonobo',
full_name: 'Simon Green',
followers_count: 892453,
permalink: 'bonobo',
permalink_url: 'https://soundcloud.com/bonobo',
avatar_url: 'https://i1.sndcdn.com/avatars-xxx-large.jpg',
country: 'United Kingdom'
}
const artist = normalizeArtist('soundcloud', scUser)
// → { id: '3207', name: 'Bonobo',
// platformIds: { soundcloud: 'bonobo' },
// metrics: { soundcloudFollowers: 892453 }, country: 'United Kingdom', ... }
// SoundCloud track
const scTrack = {
id: 123456789,
title: 'Kong',
user: { id: 3207, username: 'Bonobo', permalink: 'bonobo' },
duration: 245000,
playback_count: 1234567,
likes_count: 45678,
permalink_url: 'https://soundcloud.com/bonobo/kong'
}
const track = normalizeTrack('soundcloud', scTrack)
// → { id: '123456789', title: 'Kong', durationMs: 245000,
// metrics: { soundcloudPlaybackCount: 1234567, soundcloudLikeCount: 45678 }, ... }Platform Comparison
| Field | Spotify | Apple Music | YouTube | Deezer | MusicBrainz | Last.fm | ListenBrainz | SoundCloud |
|-------|:-------:|:-----------:|:-------:|:------:|:-----------:|:-------:|:------------:|:----------:|
| Artist Fields |
| id | ✅ | ✅ | ✅ | ✅ | ✅ (MBID) | ✅ | ✅ (MSID) | ✅ |
| name | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (username) |
| genres | ✅ | ✅ | — | ✅ | — | — | — | — |
| tags | — | — | — | — | ✅ | ✅ | ✅ | — |
| images | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| country | — | — | — | — | ✅ | — | — | ✅ |
| activeFrom/To | — | — | — | — | ✅ | — | — | — |
| Artist Metrics |
| spotifyPopularity | ✅ | — | — | — | — | — | — | — |
| deezerFans | — | — | — | ✅ | — | — | — | — |
| youtubeSubscribers | — | — | ✅ | — | — | — | — | — |
| lastfmListeners | — | — | — | — | — | ✅ | — | — |
| lastfmPlayCount | — | — | — | — | — | ✅ | — | — |
| listenbrainzListenCount | — | — | — | — | — | — | ✅ | — |
| soundcloudFollowers | — | — | — | — | — | — | — | ✅ |
| Track Fields |
| id | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ |
| title | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ |
| durationMs | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ |
| isrc | ✅ | ✅ | — | ✅ | — | — | — | — |
| explicit | ✅ | ✅ | — | ✅ | — | — | — | — |
TypeScript Types
import type {
NormalizedArtist,
NormalizedTrack,
NormalizedAlbum,
NormalizedImage,
PlatformName,
AlbumType
} from 'dsp-normalizer'
// All fields except id, name, source, and normalizedAt are optional
const artist: NormalizedArtist = {
id: 'string',
name: 'string',
nameNormalized: 'string (lowercased, diacritics stripped)',
platformIds: {
spotify?: string,
appleMusic?: string,
youtube?: string,
deezer?: string,
musicbrainz?: string,
listenbrainz?: string,
lastfm?: string,
soundcloud?: string
},
genres: string[],
tags: string[],
images: NormalizedImage[],
metrics: {
spotifyPopularity?: number, // 0-100 algorithmic score
deezerFans?: number, // literal follower count
youtubeSubscribers?: number,
lastfmListeners?: number,
lastfmPlayCount?: number,
listenbrainzListenCount?: number,
soundcloudFollowers?: number
},
externalUrls: Record<string, string>,
country?: string,
area?: string,
activeFrom?: string,
activeTo?: string | null,
source: PlatformName,
normalizedAt: string // ISO 8601
}Options
All normalize functions accept an optional second parameter:
import { normalizeArtist } from 'dsp-normalizer'
// Preserve the original raw response
const artist = normalizeArtist('spotify', raw, { includeRaw: true })
console.log(artist._raw) // Original API responseUtilities
import {
normalizeName, // Lowercase, strip diacritics, collapse whitespace
stripDiacritics, // Remove accents: "Beyoncé" → "Beyonce"
parseArtistCredits, // Parse "Artist feat. Other" into credits
selectBestImage, // Pick optimal image for target size
sortImagesByWidth, // Sort images largest-first
safeGet // Safe nested property access
} from 'dsp-normalizer'
// Name normalization for matching
normalizeName('Beyoncé Knowles') // → 'beyonce knowles'
// Image selection
selectBestImage(artist.images, 300) // → { url: '...', width: 320, height: 320 }Platform Registry
import {
getSupportedPlatforms,
isPlatformSupported,
SUPPORTED_PLATFORMS
} from 'dsp-normalizer'
getSupportedPlatforms() // → ['spotify', 'apple-music', 'youtube', ...]
isPlatformSupported('spotify') // → true
isPlatformSupported('tidal') // → falseBundle Size
This library is designed to be tiny:
- Full library: < 30KB gzipped
- Tree-shakable: Import only what you need
- Zero dependencies: No supply chain risk
// Tree-shake for smaller bundles
import { normalizeSpotifyArtist } from 'dsp-normalizer'Design Principles
- Graceful degradation — Missing fields return
undefined, never throw - No fabrication — Data that doesn't exist in the source isn't invented
- Platform-scoped metrics —
spotifyPopularity≠deezerFans; don't compare across platforms - Pure functions — No side effects, easy to test and cache
- Type safety — Full TypeScript support with exported types
Requirements
- Node.js 18+
- TypeScript 5.x (for type consumers)
License
MIT
