Skip to content

User123331/dsp-normalizer

Repository files navigation

dsp-normalizer

Universal music platform data normalizer. Transforms raw API responses from 8 DSPs into a unified, typed schema.

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
  • Zero dependencies
// Tree-shake for smaller bundles
import { normalizeSpotifyArtist } from 'dsp-normalizer'

Principles

  1. Missing fields return undefined, never throw
  2. 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

About

Universal music platform data normalizer. Transform raw API responses from 8 DSPs into a unified, typed schema.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors