A Node.js client for the Odesli API (formerly song.link/album.link) that helps you find links to music across multiple streaming platforms.
- odesli.js
- 🔗 Cross-platform links: Get links for music across Spotify, Apple Music, YouTube Music, and more
- 🎵 Song & Album support: Works with both individual tracks and full albums
- 🌍 Multi-country support: Specify country codes for region-specific results
- 🔑 API Key support: Optional API key for higher rate limits
- 📦 TypeScript support: Full TypeScript definitions included
- ⚡ Lightweight: Minimal dependencies, fast performance
- 🛡️ Robust error handling: Comprehensive error messages and validation
- 🧪 Fully tested: 100% test coverage with comprehensive test suite
npm install odesli.js
CommonJS (require):
const Odesli = require('odesli.js');
// Initialize without API key (10 requests/minute limit)
const odesli = new Odesli();
// Or with API key for higher limits
// const odesli = new Odesli({
// apiKey: 'your-api-key-here',
// version: 'v1-alpha.1', // optional, defaults to v1-alpha.1
// });
// You can also disable the metrics collector if you don't need it
// const odesliLight = new Odesli({ metrics: false });
// Fetch a song by URL (this is the actual working example)
(async () => {
try {
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
console.log(`🎵 ${song.title} by ${song.artist.join(', ')}`);
console.log(`🔗 Song.link: ${song.pageUrl}`);
} catch (error) {
console.error('❌ Error:', error.message);
}
})();
ESM (import):
import Odesli from 'odesli.js';
// Initialize without API key (10 requests/minute limit)
const odesli = new Odesli();
// Or with API key for higher limits
// const odesli = new Odesli({
// apiKey: 'your-api-key-here',
// version: 'v1-alpha.1', // optional, defaults to v1-alpha.1
// });
// You can also disable the metrics collector if you don't need it
// const odesliLight = new Odesli({ metrics: false });
// Fetch a song by URL (this is the actual working example)
try {
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
console.log(`🎵 ${song.title} by ${song.artist.join(', ')}`);
console.log(`🔗 Song.link: ${song.pageUrl}`);
} catch (error) {
console.error('❌ Error:', error.message);
}
An API key is optional but recommended for production use. Without an API key, you're limited to 10 requests per minute.
To get an API key, email developers@song.link
.
CommonJS (require):
const Odesli = require('odesli.js');
// Initialize without API key (10 requests/minute limit)
const odesli = new Odesli();
// Or with API key for higher limits
// const odesli = new Odesli({
// apiKey: 'your-api-key-here',
// });
(async () => {
// Fetch a song by URL
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
console.log(`${song.title} by ${song.artist.join(', ')}`);
// Fetch multiple songs at once
const urls = [
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
'https://open.spotify.com/track/0V3wPSX9ygBnCm8psDIegu',
];
const songs = await odesli.fetch(urls);
songs.forEach(song => console.log(song.title));
})();
ESM (import):
import Odesli from 'odesli.js';
// Initialize without API key (10 requests/minute limit)
const odesli = new Odesli();
// Or with API key for higher limits
// const odesli = new Odesli({
// apiKey: 'your-api-key-here',
// });
// Fetch a song by URL
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
console.log(`${song.title} by ${song.artist.join(', ')}`);
// Fetch multiple songs at once
const urls = [
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
'https://open.spotify.com/track/0V3wPSX9ygBnCm8psDIegu',
];
const songs = await odesli.fetch(urls);
songs.forEach(song => console.log(song.title));
CommonJS (require):
const Odesli = require('odesli.js');
// Initialize with custom options
const odesli = new Odesli({
apiKey: 'your-api-key-here',
version: 'v1-alpha.1',
cache: true,
timeout: 10000,
maxRetries: 3,
retryDelay: 1000,
headers: { 'User-Agent': 'MyApp/1.0' },
validateParams: true,
logger: (message, level) => console.log(`[${level}] ${message}`),
});
(async () => {
// Fetch with options
const song = await odesli.fetch('https://open.spotify.com/track/123', {
country: 'GB',
skipCache: false,
timeout: 5000,
});
// Batch fetch with concurrency control
const urls = [
'https://open.spotify.com/track/123',
'https://music.apple.com/us/album/test/456?i=789',
'https://www.youtube.com/watch?v=abc123',
];
const songs = await odesli.fetch(urls, {
country: 'US',
concurrency: 3,
skipCache: true,
});
// Handle errors in batch results
songs.forEach((song, index) => {
if (song.error) {
console.log(`Song ${index + 1}: Error - ${song.error}`);
} else {
console.log(`Song ${index + 1}: ${song.title}`);
}
});
})();
ESM (import):
import Odesli from 'odesli.js';
// Initialize with custom options
const odesli = new Odesli({
apiKey: 'your-api-key-here',
version: 'v1-alpha.1',
cache: true,
timeout: 10000,
maxRetries: 3,
retryDelay: 1000,
headers: { 'User-Agent': 'MyApp/1.0' },
validateParams: true,
logger: (message, level) => console.log(`[${level}] ${message}`),
});
// Fetch with options
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
{
country: 'GB',
skipCache: false,
timeout: 5000,
}
);
// Batch fetch with concurrency control
const urls = [
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
'https://open.spotify.com/track/0V3wPSX9ygBnCm8psDIegu',
'https://open.spotify.com/track/3n3Ppam7vgaVa1iaRUc9Lp',
];
const songs = await odesli.fetch(urls, {
country: 'US',
concurrency: 3,
skipCache: true,
});
// Handle errors in batch results
songs.forEach((song, index) => {
if (song.error) {
console.log(`Song ${index + 1}: Error - ${song.error}`);
} else {
console.log(`Song ${index + 1}: ${song.title}`);
}
});
All methods return a response object with the following structure:
{
entityUniqueId: "SPOTIFY_SONG::4Km5HrUvYTaSUfiSGPJeQR",
title: "Bad and Boujee",
artist: ["Migos"],
type: "song",
thumbnail: "https://i.scdn.co/image/...",
userCountry: "US",
pageUrl: "https://song.link/i/4Km5HrUvYTaSUfiSGPJeQR",
linksByPlatform: {
spotify: {
entityUniqueId: "SPOTIFY_SONG::4Km5HrUvYTaSUfiSGPJeQR",
url: "https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR",
nativeAppUriMobile: "spotify://track/4Km5HrUvYTaSUfiSGPJeQR",
nativeAppUriDesktop: "spotify://track/4Km5HrUvYTaSUfiSGPJeQR"
},
appleMusic: {
// ... similar structure
}
// ... other platforms
},
entitiesByUniqueId: {
"SPOTIFY_SONG::4Km5HrUvYTaSUfiSGPJeQR": {
id: "4Km5HrUvYTaSUfiSGPJeQR",
type: "song",
title: "Bad and Boujee",
artistName: ["Migos"],
thumbnailUrl: "https://i.scdn.co/image/...",
apiProvider: "spotify",
platforms: ["spotify"]
}
// ... other entities
}
}
- Spotify
- Apple Music
- iTunes
- YouTube Music
- YouTube
- Google Play Music
- Pandora
- Deezer
- Tidal
- Amazon Music
- SoundCloud
- Napster
- Yandex Music
- Spinrilla
This package includes full TypeScript definitions with strict country code validation:
CommonJS (require):
const Odesli = require('odesli.js');
const odesli = new Odesli({ apiKey: 'your-key' });
(async () => {
// Get all valid country codes and names for UI dropdowns
const countryOptions = Odesli.getCountryOptions();
console.log('Top 3 countries:', countryOptions.slice(0, 3));
// Output: [
// { code: 'US', name: 'United States' },
// { code: 'GB', name: 'United Kingdom' },
// { code: 'CA', name: 'Canada' }
// ]
// TypeScript enforces valid country codes
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
{
country: 'US', // ✅ Valid - TypeScript autocomplete shows all valid codes
// country: 'INVALID' // ❌ TypeScript error - not a valid CountryCode
}
);
console.log(`Fetched: ${song.title}`);
})();
ESM (import):
import Odesli, { CountryCode } from 'odesli.js';
const odesli = new Odesli({ apiKey: 'your-key' });
// Get all valid country codes and names for UI dropdowns
const countryOptions = Odesli.getCountryOptions();
console.log('Top 3 countries:', countryOptions.slice(0, 3));
// Output: [
// { code: 'US', name: 'United States' },
// { code: 'GB', name: 'United Kingdom' },
// { code: 'CA', name: 'Canada' }
// ]
// TypeScript enforces valid country codes
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
{
country: 'US', // ✅ Valid - TypeScript autocomplete shows all valid codes
// country: 'INVALID' // ❌ TypeScript error - not a valid CountryCode
}
);
console.log(`Fetched: ${song.title}`);
Check out the examples directory for comprehensive usage examples:
- Basic Usage - Simple song fetching with country options
- Advanced Features - Rate limiting, metrics, and plugin system
- User Agent Example - Custom headers and User-Agent usage
Country-specific fetching:
CommonJS (require):
const Odesli = require('odesli.js');
const odesli = new Odesli();
// Get country options for UI dropdowns
const countries = Odesli.getCountryOptions();
console.log(`Available countries: ${countries.length}`);
(async () => {
// Fetch with specific country
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
{
country: 'GB', // United Kingdom
}
);
console.log(`Fetched: ${song.title}`);
})();
ESM (import):
import Odesli from 'odesli.js';
const odesli = new Odesli();
// Get country options for UI dropdowns
const countries = Odesli.getCountryOptions();
console.log(`Available countries: ${countries.length}`);
// Fetch with specific country
const song = await odesli.fetch(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
{
country: 'GB', // United Kingdom
}
);
console.log(`Fetched: ${song.title}`);
Batch fetching with error handling:
CommonJS (require):
const Odesli = require('odesli.js');
const odesli = new Odesli();
const urls = [
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
'https://open.spotify.com/track/0V3wPSX9ygBnCm8psDIegu',
];
(async () => {
const results = await odesli.fetch(urls, { country: 'US' });
results.forEach((result, index) => {
if (result.error) {
console.log(`Song ${index + 1}: Error - ${result.error}`);
} else {
console.log(`Song ${index + 1}: ${result.title}`);
}
});
})();
ESM (import):
import Odesli from 'odesli.js';
const odesli = new Odesli();
const urls = [
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
'https://open.spotify.com/track/0V3wPSX9ygBnCm8psDIegu',
];
const results = await odesli.fetch(urls, { country: 'US' });
results.forEach((result, index) => {
if (result.error) {
console.log(`Song ${index + 1}: Error - ${result.error}`);
} else {
console.log(`Song ${index + 1}: ${result.title}`);
}
});
Platform detection and ID extraction:
CommonJS (require):
const Odesli = require('odesli.js');
const odesli = new Odesli();
// These methods are synchronous, so no async handling needed
const platform = odesli.detectPlatform(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
const id = odesli.extractId(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
console.log(`Platform: ${platform}, ID: ${id}`);
ESM (import):
import Odesli from 'odesli.js';
const odesli = new Odesli();
// These methods are synchronous, so no async handling needed
const platform = odesli.detectPlatform(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
const id = odesli.extractId(
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR'
);
console.log(`Platform: ${platform}, ID: ${id}`);
For more detailed information about the Odesli API, check the official documentation.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
This project is licensed under the ISC License - see the LICENSE file for details.
- 📧 Email:
developers@song.link
(for API key requests) - 🐛 Issues: GitHub Issues
- 📖 Documentation: Odesli API Docs
Odesli.js provides several advanced extensions to help you build robust, scalable, and customizable integrations:
What: Controls the number of API requests per time window using strategies like token bucket, leaky bucket, or sliding window.
Default Rate Limiting Behavior:
The main Odesli client does not include built-in rate limiting. Rate limits are enforced by the Odesli API server:
- Without API key: 10 requests per minute (enforced by API)
- With API key: Higher limits (enforced by API)
When you exceed the API rate limit, you'll receive a 429 error: "You are being rate limited, No API Key is 10 Requests / Minute"
Why use the RateLimiter extension?
- Prevent API errors: Proactively limit requests before hitting API rate limits
- Smooth traffic: Spread requests evenly across time windows
- Handle bursts: Manage sudden spikes in request volume
- Better UX: Avoid 429 errors by staying under limits
- Predictable behavior: Ensure consistent request patterns
Available Strategies:
- 🎯 Token Bucket: Handles bursts well, efficient memory usage
- 🔄 Sliding Window: Most accurate, precise timing (recommended)
- 💧 Leaky Bucket: Smooths traffic, predictable output rate
Strategy Options:
// Token Bucket Strategy
const tokenBucketLimiter = new RateLimiter({
maxRequests: 10,
windowMs: 60000, // 1 minute
strategy: 'token-bucket',
// Optional: burst capacity (defaults to maxRequests)
burstCapacity: 15,
});
// Sliding Window Strategy (recommended)
const slidingWindowLimiter = new RateLimiter({
maxRequests: 5,
windowMs: 10000, // 10 seconds
strategy: 'sliding-window',
// No additional options needed
});
// Leaky Bucket Strategy
const leakyBucketLimiter = new RateLimiter({
maxRequests: 3,
windowMs: 5000, // 5 seconds
strategy: 'leaky-bucket',
// Optional: queue size limit (defaults to maxRequests * 2)
queueSize: 10,
});
Strategy Comparison:
Strategy | Pros | Cons | Best For | Configuration |
---|---|---|---|---|
Token Bucket | Handles bursts, efficient memory | Less precise timing | Allowing some burst traffic | burstCapacity option |
Sliding Window | Most accurate, no overages | More memory usage | Exact rate limiting (recommended) | No additional options |
Leaky Bucket | Smooths traffic, predictable | Can delay requests | Smoothing traffic spikes | queueSize option |
When to Use Each Strategy:
-
🎯 Token Bucket: Use when you want to allow some burst traffic while maintaining overall limits. Good for user-facing applications where occasional bursts are acceptable.
-
🔄 Sliding Window: Use when you need precise rate limiting with no overages. Best for API integrations where you must stay strictly within limits.
-
💧 Leaky Bucket: Use when you want to smooth out traffic spikes and maintain a steady, predictable output rate. Good for background processing or batch operations.
const Odesli = require('odesli.js');
// Without RateLimiter - relies on API rate limiting
const odesli = new Odesli();
// This works fine for < 10 requests/minute
// But will throw 429 errors if you exceed the limit
const urls = [
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
'https://open.spotify.com/track/0V3wPSX9ygBnCm8psDIegu',
];
const songs = await odesli.fetch(urls); // May hit API rate limit
// With RateLimiter - proactive client-side limiting
const { RateLimiter } = require('odesli.js/rate-limiter');
const limiter = new RateLimiter({
maxRequests: 8, // Stay safely under the 10/minute limit
windowMs: 60000,
strategy: 'sliding-window',
});
// This prevents hitting API rate limits
async function safeFetch(url) {
await limiter.waitForSlot();
return await odesli.fetch(url);
}
CommonJS (require):
const Odesli = require('odesli.js');
const { RateLimiter } = require('odesli.js/rate-limiter');
const odesli = new Odesli();
const limiter = new RateLimiter({
maxRequests: 2,
windowMs: 3000, // 2 requests per 3 seconds
strategy: 'sliding-window', // More reliable strategy
});
const urls = [
'https://open.spotify.com/track/4Km5HrUvYTaSUfiSGPJeQR',
'https://open.spotify.com/track/0V3wPSX9ygBnCm8psDIegu',
'https://open.spotify.com/track/3n3Ppam7vgaVa1iaRUc9Lp',
];
// Function to fetch with rate limiting
async function fetchWithRateLimit(url, index) {
console.log(`⏳ Request ${index + 1}: Waiting for slot...`);
await limiter.waitForSlot();
console.log(`✅ Request ${index + 1}: Got slot, fetching...`);
const song = await odesli.fetch(url);
console.log(`🎵 Request ${index + 1}: ${song.title}`);
return song;
}
// Submit all requests at once - they'll be processed automatically
(async () => {
console.log('🚀 Auto-Queue Rate Limiter Demo');
console.log(
`📊 Limit: ${limiter.maxRequests} requests per ${limiter.windowMs / 1000}s`
);
console.log(`🔗 Submitting ${urls.length} requests...\n`);
// Submit all requests - they'll be processed as slots become available
const promises = urls.map((url, index) => fetchWithRateLimit(url, index));
// Wait for all to complete
const results = await Promise.all(promises);
console.log('\n🎉 All requests completed!');
console.log(`📊 Total songs fetched: ${results.length}`);
})();
What: Tracks requests, errors, cache hits, response times, and rate limit events.
Why use it?
- Monitor API usage and performance
- Debug slowdowns or error spikes
- Gather analytics for reporting or dashboards
CommonJS (require):
const { MetricsCollector } = require('odesli.js/metrics');
ESM (import):
import { MetricsCollector } from 'odesli.js/metrics';
What: Extensible system for adding hooks, middleware, and data transformers to Odesli.js.
Why use it?
- Add custom logging, analytics, or caching
- Transform responses or inject custom logic
- Cleanly separate concerns and enable community plugins
CommonJS (require):
const { PluginSystem } = require('odesli.js/plugin-system');
ESM (import):
import { PluginSystem } from 'odesli.js/plugin-system';
See the examples/advanced-features-example.js
for a full demonstration of these extensions in action.