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

soundcord

v2.0.1

Published

Simple YouTube audio streaming for Discord bots

Downloads

465

Readme

soundcord

npm version npm downloads GitHub Discord

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 soundcord

Setup Dependencies

Run the setup helper to install yt-dlp and ffmpeg:

npx soundcord-setup

This 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