soundcord
v2.0.1
Published
Simple YouTube audio streaming for Discord bots
Downloads
465
Maintainers
Readme
soundcord
All-in-one audio streaming library for Discord bots. YouTube, Spotify, SoundCloud, Deezer, Radio, Lyrics, and more.
Join our Discord Server for support, updates, and discussions!
Features
- Multi-source: YouTube, Spotify, SoundCloud, Deezer, Radio, Twitch
- Lyrics: Synced lyrics with timestamps (karaoke mode)
- YouTube Chapters: Navigate long videos
- Audio Filters: Bass boost, nightcore, vaporwave, 8D, and more
- Quality Selection: best, medium, low
- Live Streams: YouTube Live & Twitch
- Queue Persistence: Save/restore queues
- Zero ytdl-core: Uses yt-dlp (more reliable)
- TypeScript: Full type support
Installation
npm install soundcordSetup Dependencies
Run the setup helper to install yt-dlp and ffmpeg:
npx soundcord-setupThis will:
- Detect your OS and package manager
- Install yt-dlp and ffmpeg automatically
- Add to PATH if needed
Quick Start
import { YouTube } from 'soundcord';
const yt = new YouTube();
const results = await yt.search('never gonna give you up');
const stream = await yt.getStream(results[0].id);
console.log(stream.url);YouTube
Search
const results = await yt.search('lofi hip hop', 5);
for (const video of results) {
console.log(`${video.title} - ${video.author} (${video.duration}s)`);
}Get Video Info
const info = await yt.getInfo('dQw4w9WgXcQ');
console.log(info.title, info.author, info.duration);Get Stream URL
const stream = await yt.getStream('dQw4w9WgXcQ');
console.log(stream.url, stream.bitrate);Quality Selection
const stream = await yt.getStreamWithQuality('dQw4w9WgXcQ', 'best');
const streamLow = await yt.getStreamWithQuality('dQw4w9WgXcQ', 'low');
const qualities = await yt.getAvailableQualities('dQw4w9WgXcQ');
console.log(qualities);Playlists
if (yt.isPlaylistUrl(url)) {
const playlist = await yt.getPlaylist(url, 50);
console.log(`${playlist.title} - ${playlist.videoCount} videos`);
for (const video of playlist.videos) {
console.log(video.title);
}
}Chapters
const chapters = await yt.getChapters('dQw4w9WgXcQ');
for (const chapter of chapters) {
console.log(`${chapter.title}: ${chapter.start}s - ${chapter.end}s`);
}Related Videos (Autoplay)
const related = await yt.getRelated('dQw4w9WgXcQ', 5);
const nextVideo = related[0];Live Streams
if (await yt.isLive('VIDEO_ID')) {
const stream = await yt.getLiveStream('VIDEO_ID');
console.log(stream.url);
}Spotify
Play Spotify tracks, playlists, and albums by searching YouTube.
if (yt.isSpotifyUrl(url)) {
const result = await yt.playSpotify(url);
if (Array.isArray(result)) {
console.log(`Playlist with ${result.length} tracks`);
} else {
console.log(`Track: ${result.title}`);
}
}Track
const track = await yt.getSpotifyTrack('https://open.spotify.com/track/...');
console.log(`${track.artist} - ${track.name}`);Playlist
const tracks = await yt.getSpotifyPlaylist('https://open.spotify.com/playlist/...', 50);Album
const tracks = await yt.getSpotifyAlbum('https://open.spotify.com/album/...');SoundCloud
import { SoundCloud } from 'soundcord';
const sc = new SoundCloud();
if (sc.isSoundCloudUrl(url)) {
const track = await sc.getTrack(url);
console.log(track.title, track.author);
const streamUrl = await sc.getStream(track.id);
}Playlists
if (sc.isSoundCloudPlaylistUrl(url)) {
const playlist = await sc.getPlaylist(url);
for (const track of playlist.tracks) {
console.log(track.title);
}
}Search
const results = await sc.search('chill beats', 10);Deezer
import { Deezer } from 'soundcord';
const dz = new Deezer();
if (dz.isDeezerUrl(url)) {
const info = dz.extractDeezerInfo(url);
if (info.type === 'track') {
const track = await dz.getTrack(url);
console.log(`${track.artist} - ${track.title}`);
}
if (info.type === 'album') {
const album = await dz.getAlbum(url);
console.log(`${album.title} - ${album.tracks.length} tracks`);
}
if (info.type === 'playlist') {
const playlist = await dz.getPlaylist(url);
console.log(`${playlist.title} - ${playlist.tracks.length} tracks`);
}
}Radio
15+ preset stations + worldwide search via radio-browser API.
import { Radio, RadioPresets } from 'soundcord';
const radio = new Radio();Presets
const presets = radio.getPresets();
for (const station of presets) {
console.log(`${station.name} - ${station.genre} (${station.country})`);
}
const fip = radio.getPreset('fip');
console.log(fip.url);Available Presets
| Name | Genre | Country |
|------|-------|---------|
| franceinter | Généraliste | France |
| franceinfo | News | France |
| fip | Eclectic | France |
| mouv | Urban/Hip-Hop | France |
| francemusique | Classical | France |
| franceculture | Culture | France |
| skyrock | Hip-Hop/Rap | France |
| funradio | Dance/Electronic | France |
| rtl | Généraliste | France |
| rtl2 | Pop/Rock | France |
| bbcradio1 | Pop/Rock | UK |
| bbcradio2 | Adult Contemporary | UK |
| lofi | Lofi/Chill | International |
| jazz | Jazz | France |
| classique | Classical | France |
Search Stations
const stations = await radio.search('jazz', 10);
for (const station of stations) {
console.log(`${station.name} - ${station.url}`);
}Search by Genre
const stations = await radio.searchByGenre('rock', 10);Search by Country
const stations = await radio.searchByCountry('France', 10);Popular Stations
const popular = await radio.getPopular(20);Lyrics
Get lyrics with optional synchronized timestamps.
const lyrics = await yt.getLyrics('Artist - Song Title');
if (lyrics) {
console.log(`${lyrics.artist} - ${lyrics.title}`);
console.log(lyrics.lyrics);
if (lyrics.synced) {
console.log('Synced lyrics available');
}
}Synced Lyrics (Karaoke)
import { Lyrics } from 'soundcord';
const lyricsEngine = new Lyrics(15000);
const synced = await lyricsEngine.getSynced('Artist - Song');
if (synced) {
for (const line of synced) {
console.log(`[${line.time}s] ${line.text}`);
}
}Twitch
if (yt.isTwitchUrl(url)) {
const channel = yt.extractTwitchChannel(url);
const stream = await yt.getTwitchStream(url);
console.log(stream.url);
}Audio Filters
15+ built-in audio filters for ffmpeg.
import { getFilter, getFilterNames, combineFilters } from 'soundcord';
const filters = getFilterNames();
console.log(filters);
const bass = getFilter('bass');
console.log(bass.ffmpegArgs);Available Filters
| Filter | Description |
|--------|-------------|
| bass | Bass boost |
| nightcore | Nightcore effect |
| vaporwave | Vaporwave/slowed |
| 8d | 8D audio |
| karaoke | Remove vocals |
| treble | Treble boost |
| echo | Echo effect |
| flanger | Flanger effect |
| phaser | Phaser effect |
| tremolo | Tremolo effect |
| vibrato | Vibrato effect |
| reverse | Reverse audio |
| pitch_up | Higher pitch |
| pitch_down | Lower pitch |
| speed | 1.25x speed |
Using with FFmpeg
const filter = getFilter('bass');
const ffmpegArgs = ['-i', streamUrl, ...filter.ffmpegArgs, '-f', 's16le', '-'];Combine Filters
const combined = combineFilters(['bass', 'nightcore']);
console.log(combined);Queue Persistence
Save and restore queues across bot restarts.
import { QueuePersistence, BookmarkManager } from 'soundcord';
const persistence = new QueuePersistence('./data/queues');
const bookmarks = new BookmarkManager('./data/bookmarks');Save Queue
await persistence.save({
guildId: '123456789',
songs: [
{ id: 'dQw4w9WgXcQ', title: 'Never Gonna Give You Up', author: 'Rick Astley', duration: 213 }
],
currentIndex: 0,
position: 45,
volume: 100,
loop: 'off'
});Load Queue
const data = await persistence.load('123456789');
if (data) {
console.log(`Restored ${data.songs.length} songs`);
}Bookmarks
await bookmarks.add('123456789', {
name: 'My Favorite',
videoId: 'dQw4w9WgXcQ',
title: 'Never Gonna Give You Up',
timestamp: 45
});
const userBookmarks = await bookmarks.list('123456789');Discord.js Integration
import { YouTube } from 'soundcord';
import {
createAudioPlayer,
createAudioResource,
joinVoiceChannel,
StreamType
} from '@discordjs/voice';
import { spawn } from 'child_process';
const yt = new YouTube();
async function play(voiceChannel, query) {
const connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
selfDeaf: true
});
const results = await yt.search(query, 1);
const stream = await yt.getStream(results[0].id);
const ffmpeg = spawn('ffmpeg', [
'-reconnect', '1',
'-reconnect_streamed', '1',
'-reconnect_delay_max', '5',
'-i', stream.url,
'-vn',
'-f', 's16le',
'-ar', '48000',
'-ac', '2',
'-'
]);
const resource = createAudioResource(ffmpeg.stdout, {
inputType: StreamType.Raw
});
const player = createAudioPlayer();
player.play(resource);
connection.subscribe(player);
return results[0];
}Configuration
const yt = new YouTube({
timeout: 15000,
minDuration: 0,
cacheTTL: 30000,
ytdlpPath: '/custom/path/to/yt-dlp'
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| timeout | number | 15000 | Request timeout (ms) |
| minDuration | number | 0 | Min video duration for search |
| cacheTTL | number | 30000 | Stream URL cache (ms) |
| ytdlpPath | string | auto | Custom yt-dlp path |
Error Handling
import { YouTube, YouTubeError } from 'soundcord';
try {
await yt.getStream('invalid');
} catch (err) {
if (err instanceof YouTubeError) {
switch (err.code) {
case 'NOT_FOUND':
console.log('Video not found');
break;
case 'NETWORK_ERROR':
console.log('Network error');
break;
case 'YTDLP_MISSING':
console.log('yt-dlp not installed');
break;
case 'RATE_LIMITED':
console.log('Rate limited by YouTube');
break;
case 'PARSE_ERROR':
console.log('Failed to parse response');
break;
}
}
}Types
interface SearchResult {
id: string;
title: string;
author: string;
duration: number;
thumbnail: string;
}
interface VideoInfo {
id: string;
title: string;
author: string;
duration: number;
thumbnail: string;
url: string;
}
interface StreamInfo {
url: string;
mimeType: string;
bitrate: number;
}
interface PlaylistInfo {
id: string;
title: string;
author: string;
videoCount: number;
videos: SearchResult[];
}
interface SpotifyTrack {
name: string;
artist: string;
album: string;
duration: number;
url: string;
}
interface RadioStation {
name: string;
url: string;
genre?: string;
country?: string;
bitrate?: number;
}
interface LyricsResult {
title: string;
artist: string;
lyrics: string;
synced?: string;
}
interface Chapter {
title: string;
start: number;
end: number;
}Comparison
| Feature | soundcord | discord-player | distube | |---------|-----------|----------------|---------| | No ytdl-core | ✅ | ❌ | ❌ | | Radio streams | ✅ | ❌ | ❌ | | Synced lyrics | ✅ | ❌ | Plugin | | YouTube chapters | ✅ | ❌ | ❌ | | Audio normalization | ✅ | ❌ | ❌ | | Quality selection | ✅ | ❌ | ❌ | | Queue persistence | ✅ | ❌ | ❌ | | Bundle size | ~50KB | ~500KB+ | ~300KB+ |
Disclaimer
This library is for educational and personal use only.
By using this software, you agree to:
- Comply with YouTube's Terms of Service
- Comply with Spotify's Terms of Service
- Use the library responsibly and ethically
- Respect copyright and intellectual property rights
The authors are not responsible for any misuse of this library.
License
MIT
