Skip to content

🎧 Lightweight integration with the Spotify Web API for modern Javascript runtimes

License

Notifications You must be signed in to change notification settings

MellKam/soundify

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Soundify is a lightweight and flexible library for interacting with the Spotify API, designed to work seamlessly with TypeScript and support all available runtimes.

Getting Started | Error handling | Token refreshing | Pagination

Installation

The package doesn't depend on runtime specific apis, so you should be able to use it without any problems everywhere.

npm install @soundify/web-api
// deno.json
{
	"imports": {
		"@soundify/web-api": "https://deno.land/x/soundify/mod.ts"
	}
}

Install from JSR registry

deno add @soundify/web-api

Getting Started

Soundify has a very simple structure. It consists of a SpotifyClient capable of making requests to the Spotify API, along with a set of functions (like getCurrentUser) that utilize the client to make requests to specific endpoints.

import { getCurrentUser, search, SpotifyClient } from "@soundify/web-api";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN");

const me = await getCurrentUser(client);
console.log(me);

const result = await search(client, "track", "Never Gonna Give You Up");
console.log(result.tracks.items.at(0));

Compared to the usual OOP way of creating API clients, this approach has several advantages. The main one is that it is tree-shakable. You only ship code you use. This may be not that important for server-side apps, but I'm sure frontend users will thank you for not including an extra 10kb of crappy js into your bundle.

import {
	getAlbumTracks,
	getArtist,
	getArtistAlbums,
	getRecommendations,
	SpotifyClient,
} from "@soundify/web-api";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN");

const radiohead = await getArtist(client, "4Z8W4fKeB5YxbusRsdQVPb");
console.log(`Radiohead popularity - ${radiohead.popularity}`);

const pagingResult = await getArtistAlbums(client, radiohead.id, { limit: 1 });
const album = pagingResult.items.at(0)!;
console.log(`Album - ${album.name}`);

const tracks = await getAlbumTracks(client, album.id, { limit: 5 });
console.table(
	tracks.items.map((track) => ({
		name: track.name,
		duration: track.duration_ms,
	})),
);

const recomendations = await getRecommendations(client, {
	seed_artists: [radiohead.id],
	seed_tracks: tracks.items.map((track) => track.id).slice(0, 4),
	market: "US",
	limit: 5,
});
console.table(
	recomendations.tracks.map((track) => ({
		artist: track.artists.at(0)!.name,
		name: track.name,
	})),
);

Error handling πŸ“›

import { getCurrentUser, SpotifyClient, SpotifyError } from "@soundify/web-api";

const client = new SpotifyClient("INVALID_ACCESS_TOKEN");

try {
	const me = await getCurrentUser(client);
	console.log(me);
} catch (error) {
	if (error instanceof SpotifyError) {
		error.status; // 401

		const message = typeof error.body === "string"
			? error.body
			: error.body?.error.message;
		console.error(message); // "Invalid access token"

		error.response.headers.get("Date"); // You can access the response here

		console.error(error);
		// SpotifyError: 401 Unauthorized (https://api.spotify.com/v1/me) : Invalid access token
		return;
	}

	// If it's not a SpotifyError, then it's some type of network error that fetch throws
	// Or can be DOMException if you abort the request
	console.error("We're totally f#%ked!");
}

Rate Limiting πŸ•’

If you're really annoying customer, Spotify may block you for some time. To know what time you need to wait, you can use Retry-After header, which will tell you time in seconds. More about rate limiting↗

To handle this automatically, you can use waitForRateLimit option in SpotifyClient. (it's disabled by default, because it may block your code for unknown time)

const client = new SpotifyClient("YOUR_ACCESS_TOKEN", {
	waitForRateLimit: true,
	// wait only if it's less than a minute
	waitForRateLimit: (retryAfter) => retryAfter < 60,
});

Authorization

Soundify doesn't provide any tools for authorization, because that would require to write whole oauth library in here. We have many other battle-tested oauth solutions, like oauth4webapi or oidc-client-ts. I just don't see a point in reinventing the wheel 🫀.

Despite this, we have a huge directory of examples, including those for authorization. OAuth2 Examples↗

Token Refreshing

import { getCurrentUser, SpotifyClient } from "@soundify/web-api";

// if you don't have access token yet, you can pass null to first argument
const client = new SpotifyClient(null, {
	// but you have to provide a function that will return a new access token
	refresher: () => {
		return Promise.resolve("YOUR_NEW_ACCESS_TOKEN");
	},
});

const me = await getCurrentUser(client);
// client will call your refresher to get the token
// and only then make the request
console.log(me);

// let's wait some time to expire the token ...

const me = await getCurrentUser(client);
// client will receive 401 and call your refresher to get new token
// you don't have to worry about it as long as your refresher is working
console.log(me);

Pagination

To simplify the process of paginating through the results, we provide a PageIterator and CursorPageIterator classes.

import { getPlaylistTracks, SpotifyClient } from "@soundify/web-api";
import { PageIterator } from "@soundify/web-api/pagination";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN");

const playlistIter = new PageIterator(
	(offset) =>
		getPlaylistTracks(client, "37i9dQZEVXbMDoHDwVN2tF", {
			// you can find the max limit for specific endpoint
			// in spotify docs or in the jsdoc comments of this property
			limit: 50,
			offset,
		}),
);

// iterate over all tracks in the playlist
for await (const track of playlistIter) {
	console.log(track);
}

// or collect all tracks into an array
const allTracks = await playlistIter.collect();
console.log(allTracks.length);

// Want to get the last 100 items? No problem
const lastHundredTracks = new PageIterator(
	(offset) =>
		getPlaylistTracks(
			client,
			"37i9dQZEVXbMDoHDwVN2tF",
			{ limit: 50, offset },
		),
	{ initialOffset: -100 }, // this will work just as `Array.slice(-100)`
).collect();
import { getFollowedArtists, SpotifyClient } from "@soundify/web-api";
import { CursorPageIterator } from "@soundify/web-api/pagination";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN");

// loop over all followed artists
for await (
	const artist of new CursorPageIterator(
		(opts) => getFollowedArtists(client, { limit: 50, after: opts.after }),
	)
) {
	console.log(artist.name);
}

// or collect all followed artists into an array
const artists = await new CursorPageIterator(
	(opts) => getFollowedArtists(client, { limit: 50, after: opts.after }),
).collect();

// get all followed artists starting from Radiohead
const artists = await new CursorPageIterator(
	(opts) => getFollowedArtists(client, { limit: 50, after: opts.after }),
	{ initialAfter: "4Z8W4fKeB5YxbusRsdQVPb" }, // let's start from Radiohead
).collect();

Other customizations

import { SpotifyClient } from "@soundify/web-api";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN", {
	// You can use any fetch implementation you want
	// For example, you can use `node-fetch` in node.js
	fetch: (input, init) => {
		return fetch(input, init);
	},
	// You can change the base url of the client
	// by default it's "https://api.spotify.com/"
	beseUrl: "https://example.com/",
	middlewares: [(next) => (url, opts) => {
		// You can add your own middleware
		// For example, you can add some headers to every request
		return next(url, opts);
	}],
});

Contributors ✨

All contributions are very welcome ❀️ (emoji key)

Artem Melnyk
Artem Melnyk

🚧
danluki
danluki

πŸ’»
Andrii Zontov
Andrii Zontov

πŸ›
Brayden Babbitt
Brayden Babbitt

πŸ›