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

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.

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-normalizer

Quick 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 response

Utilities

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')    // → false

Bundle 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

  1. Graceful degradation — Missing fields return undefined, never throw
  2. No fabrication — Data that doesn't exist in the source isn't invented
  3. Platform-scoped metricsspotifyPopularitydeezerFans; don't compare across platforms
  4. Pure functions — No side effects, easy to test and cache
  5. Type safety — Full TypeScript support with exported types

Requirements

  • Node.js 18+
  • TypeScript 5.x (for type consumers)

License

MIT