A lightweight, type-safe music player for Discord bots, built with TypeScript and integrated with discord.js. This package provides a robust MusicPlayer class for streaming audio from platforms like YouTube, SoundCloud, Spotify, and Deezer, with optional Lavalink support via Erela.js for enhanced performance and playlist handling. Without Lavalink, it falls back to direct streaming using libraries like play-dl, soundcloud-downloader, and yt-dlp-exec.
- @persian-caesar/discord-player
- Table of Contents
- Introduction
- Features
- Installation
- Dependencies
- Usage Examples
- API Reference
- MusicPlayer Class
- MusicPlayerEvent Enum
- Method Usage and Examples
isPlaylist(url: string): Promise<boolean>searchPlaylists(query: string, platform?: SearchPlatform, limit?: number): Promise<PlaylistMetadata[]>search(query: string, platform?: SearchPlatform, limit?: number): Promise<TrackMetadata[]>play(input: string | TrackMetadata | TrackMetadata[], radio?: boolean): Promise<void>pause(): voidresume(): voidsetVolume(percent: number): numberskip(): voidprevious(): Promise<void>shuffle(): voidundoShuffle(): voidtoggleLoopQueue(): booleantoggleLoopTrack(): booleanstartRadio(urls: string[]): Promise<void>stop(noLeave?: boolean): voiddisconnect(): voidjoin(): VoiceConnectiongetQueue(): TrackMetadata[]getVolume(): numberisPlaying(): booleanisPaused(): booleanisShuffiled(): booleanisConnected(guildId?: string): booleansearchLyrics(title: string, artist?: string): Promise<string | null>
- Support and Contributions
- License
- Contact
@persian-caesar/discord-player is designed to simplify audio playback in Discord bots. It leverages the @discordjs/voice library for voice channel interactions and supports streaming from multiple platforms. Lavalink integration (via Erela.js) is optional for better scalability, playlist support, and performance. Without Lavalink, the player uses direct streaming for flexibility in smaller setups. The package is fully typed, making it ideal for TypeScript projects, and includes JSDoc annotations for JavaScript users. The MusicPlayer class handles all aspects of music playback, including multi-platform search, queue management, history tracking, and event-driven notifications.
Developed by Sobhan-SRZA for Persian Caesar, this package is licensed under MIT and actively maintained.
- Optional Lavalink Support: Use Erela.js for advanced features like playlist loading and better audio handling, or fallback to direct streaming without it.
- Multi-Platform Search and Streaming: Supports YouTube, SoundCloud, Spotify, and Deezer. Search prioritizes platforms in order (configurable), returns a list of results.
- Direct Stream Handling: Streams non-platform URLs (e.g., radio stations) directly without searching.
- Playlist Support: Detect, search, and load playlists from all supported platforms, automatically enqueues tracks.
- Queue Management: Add tracks (single or multiple), shuffle, or revert to the original order.
- Looping Options: Toggle looping for a single track or the entire queue.
- Volume Control: Adjust playback volume (0–200%).
- Lyrics Retrieval: Fetch song lyrics from Google search results using
html-to-text. - Radio Mode: Play a shuffled list of URLs in a continuous loop.
- Event System: Strongly-typed events for playback status, queue changes, errors, and more.
- Auto-Disconnect: Configurable options to leave voice channels when the queue is empty or after idle time.
- Type Safety: Full TypeScript support with defined interfaces and enums in
types.ts. - Lightweight: Minimal dependencies with no external framework requirements beyond
discord.js.
Install the package:
npm install @persian-caesar/discord-playerEnsure you have Node.js version 16 or higher, as specified in package.json.
The following dependencies are required for the package to function correctly:
| Package | Version | Purpose |
|---|---|---|
@discordjs/voice |
^0.18.0 | Handles voice channel connections and audio playback in Discord. |
@discordjs/opus |
^0.10.0 | Provides Opus audio encoding/decoding for high-quality audio streaming. |
erela.js |
^2.4.0 | Optional: Access to Lavalink for enhanced audio and playlist support. |
play-dl |
^1.9.7 | Streams audio from Spotify, YouTube, and Deezer with search capabilities (fallback mode). |
soundcloud-downloader |
^1.0.0 | Downloads and streams audio from SoundCloud URLs (fallback mode). |
html-to-text |
^9.0.5 | Converts HTML (from Google lyrics searches) to plain text. |
libsodium-wrappers |
^0.7.15 | Required for secure audio encryption in @discordjs/voice. |
ffmpeg-static |
(peer) | Provides FFmpeg for audio processing and stream conversion. |
Why these dependencies?
@discordjs/voiceand@discordjs/opusare core to Discord voice functionality, enabling the bot to join channels and stream audio.erela.jsenables optional Lavalink integration for better scalability.play-dlandsoundcloud-downloaderprovide fallback streaming without Lavalink.html-to-textis used for scraping and cleaning lyrics from Google search results.libsodium-wrappersandffmpeg-staticare required for secure and efficient audio processing.
Below are examples demonstrating how to integrate @persian-caesar/discord-player with discord.js in both TypeScript and JavaScript. These examples assume you have a Discord bot set up with discord.js.
This example uses Lavalink for playlist support and better performance.
import { Client, GatewayIntentBits, TextChannel, VoiceChannel } from 'discord.js';
import { MusicPlayer, MusicPlayerEvent, LavalinkManager } from '@persian-caesar/discord-player';
// Initialize Discord client with necessary intents
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
// Initialize Erela.js Manager for Lavalink
const manager = new LavalinkManager(client, {
nodes: [
{
host: "lava-v4.ajieblogs.eu.org",
port: 443,
password: "https://dsc.gg/ajidevserver",
secure: true
}
]
});
// Bot configuration
const PREFIX = '!';
const TOKEN = 'YOUR_BOT_TOKEN'; // Replace with your bot token
client.on('ready', () => {
console.log(`Logged in as ${client.user?.tag}`);
});
client.on('messageCreate', async (message) => {
if (!message.content.startsWith(PREFIX) || message.author.bot) return;
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
const command = args.shift()?.toLowerCase();
if (!message.guild || !message.member?.voice.channel) return;
const voiceChannel = message.member.voice.channel as VoiceChannel;
const player = new MusicPlayer(voiceChannel, message.channel as TextChannel, manager, {
autoLeaveOnEmptyQueue: true,
autoLeaveOnIdleMs: 300_000 // 5 minutes
});
// Event listeners for music player
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, metadatas, queue }) => {
const added = metadatas ? metadatas.length + ' tracks' : metadata?.title || metadata?.url;
message.channel.send(`➕ Added: ${added} (${queue.length} in queue)`);
});
player.on(MusicPlayerEvent.Error, (error) => {
message.channel.send(`❌ Error: ${error.message}`);
});
player.on(MusicPlayerEvent.Finish, () => {
message.channel.send('⏹️ Playback finished.');
});
// Command handling
if (command === 'play') {
const query = args.join(' ');
if (!query) {
message.channel.send('Please provide a URL or search query.');
return;
}
await player.play(query);
}
else if (command === 'search') {
const query = args.join(' ');
const results = await player.search(query, 'youtube');
const resultList = results.map((r, i) => `${i + 1}. ${r.title || r.url}`).join('\n');
message.channel.send(resultList || 'No results.');
}
else if (command === 'playlists') {
const query = args.join(' ');
const playlists = await player.searchPlaylists(query, 'spotify');
const list = playlists.map((p, i) => `${i + 1}. ${p.title} (${p.trackCount} tracks): ${p.url}`).join('\n');
message.channel.send(list || 'No playlists found.');
}
else if (command === 'isplaylist') {
const url = args[0];
const isPlaylist = await player.isPlaylist(url);
message.channel.send(isPlaylist ? 'Yes, this is a playlist.' : 'No, this is not a playlist.');
} // Add other commands as needed
});
client.login(TOKEN);This example uses fallback streaming without Lavalink.
import { Client, GatewayIntentBits, TextChannel, VoiceChannel } from 'discord.js';
import { MusicPlayer, MusicPlayerEvent } from '@persian-caesar/discord-player';
// Initialize Discord client with necessary intents
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
// Bot configuration
const PREFIX = '!';
const TOKEN = 'YOUR_BOT_TOKEN'; // Replace with your bot token
client.on('ready', () => {
console.log(`Logged in as ${client.user?.tag}`);
});
client.on('messageCreate', async (message) => {
if (!message.content.startsWith(PREFIX) || message.author.bot) return;
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
const command = args.shift()?.toLowerCase();
if (!message.guild || !message.member?.voice.channel) return;
const voiceChannel = message.member.voice.channel as VoiceChannel;
const player = new MusicPlayer(voiceChannel, message.channel as TextChannel, undefined, {
autoLeaveOnEmptyQueue: true,
autoLeaveOnIdleMs: 300_000, // 5 minutes
youtubeCookie: 'YOUR_YOUTUBE_COOKIE', // Optional for age-restricted content
});
// Event listeners for music player
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, metadatas, queue }) => {
const added = metadatas ? metadatas.length + ' tracks' : metadata?.title || metadata?.url;
message.channel.send(`➕ Added: ${added} (${queue.length} in queue)`);
});
player.on(MusicPlayerEvent.Error, (error) => {
message.channel.send(`❌ Error: ${error.message}`);
});
player.on(MusicPlayerEvent.Finish, () => {
message.channel.send('⏹️ Playback finished.');
});
// Command handling
if (command === 'play') {
const query = args.join(' ');
if (!query) {
message.channel.send('Please provide a URL or search query.');
return;
}
await player.play(query);
}
else if (command === 'search') {
const query = args.join(' ');
const results = await player.search(query, 'spotify'); // Optional platform
const resultList = results.map((r, i) => `${i + 1}. ${r.title || r.url}`).join('\n');
message.channel.send(resultList || 'No results.');
}
else if (command === 'playlists') {
const query = args.join(' ');
const playlists = await player.searchPlaylists(query, 'deezer');
const list = playlists.map((p, i) => `${i + 1}. ${p.title} (${p.trackCount} tracks): ${p.url}`).join('\n');
message.channel.send(list || 'No playlists found.');
}
else if (command === 'isplaylist') {
const url = args[0];
const isPlaylist = await player.isPlaylist(url);
message.channel.send(isPlaylist ? 'Yes, this is a playlist.' : 'No, this is not a playlist.');
} // Add other commands as needed
});
client.login(TOKEN);This example uses plain JavaScript with optional Lavalink.
const { Client, GatewayIntentBits } = require('discord.js');
const { MusicPlayer, MusicPlayerEvent, LavalinkManager } = require('@persian-caesar/discord-player');
// Initialize Discord client with necessary intents
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
// Optional Lavalink Manager
const manager = new LavalinkManager(client, {
nodes: [
{
host: "lava-v4.ajieblogs.eu.org",
port: 443,
password: "https://dsc.gg/ajidevserver",
secure: true
}
]
});
// Bot configuration
const PREFIX = '!';
const TOKEN = 'YOUR_BOT_TOKEN'; // Replace with your bot token
client.on('ready', () => {
console.log(`Logged in as ${client.user?.tag}`);
});
client.on('messageCreate', async (message) => {
if (!message.content.startsWith(PREFIX) || message.author.bot) return;
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
const command = args.shift()?.toLowerCase();
if (!message.guild || !message.member?.voice.channel) return;
/** @type {import('@persian-caesar/discord-player').VoiceChannel} */
const voiceChannel = message.member.voice.channel;
const player = new MusicPlayer(voiceChannel, message.channel, manager, { // Pass manager if using Lavalink
autoLeaveOnEmptyQueue: true,
autoLeaveOnIdleMs: 300_000 // 5 minutes
});
// Event listeners for music player
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, metadatas, queue }) => {
const added = metadatas ? metadatas.length + ' tracks' : metadata?.title || metadata?.url;
message.channel.send(`➕ Added: ${added} (${queue.length} in queue)`);
});
player.on(MusicPlayerEvent.Error, (error) => {
message.channel.send(`❌ Error: ${error.message}`);
});
player.on(MusicPlayerEvent.Finish, () => {
message.channel.send('⏹️ Playback finished.');
});
// Command handling
if (command === 'play') {
const query = args.join(' ');
if (!query) {
message.channel.send('Please provide a URL or search query.');
return;
}
await player.play(query);
}
else if (command === 'search') {
const query = args.join(' ');
const results = await player.search(query);
const resultList = results.map((r, i) => `${i + 1}. ${r.title || r.url}`).join('\n');
message.channel.send(resultList || 'No results.');
}
else if (command === 'playlists') {
const query = args.join(' ');
const playlists = await player.searchPlaylists(query);
const list = playlists.map((p, i) => `${i + 1}. ${p.title} (${p.trackCount} tracks): ${p.url}`).join('\n');
message.channel.send(list || 'No playlists found.');
}
else if (command === 'isplaylist') {
const url = args[0];
const isPlaylist = await player.isPlaylist(url);
message.channel.send(isPlaylist ? 'Yes, this is a playlist.' : 'No, this is not a playlist.');
} // Add other commands as needed
});
client.login(TOKEN);Constructor:
new MusicPlayer(
channel: VoiceChannel,
textChannel: TextChannel,
lavaLinkManager?: Manager, // Optional Erela.js Manager for Lavalink
options?: MusicPlayerOptions // { initialVolume?: number, autoLeaveOnEmptyQueue?: boolean, autoLeaveOnIdleMs?: number, youtubeCookie?: string, logError?: boolean }
)Methods:
| Method | Description |
|---|---|
isPlaylist(url: string): Promise<boolean> |
Checks if the given URL is a playlist. |
searchPlaylists(query: string, platform?: SearchPlatform, limit?: number): Promise<PlaylistMetadata[]> |
Searches for playlists across platforms. |
search(query: string, platform?: SearchPlatform, limit?: number): Promise<TrackMetadata[]> |
Searches for tracks across platforms. |
| `play(input: string | TrackMetadata |
pause(): void |
Pauses the current track. |
resume(): void |
Resumes playback. |
setVolume(percent: number): number |
Sets volume (0–200%), returns new volume. |
skip(): void |
Skips to the next track in the queue. |
previous(): Promise<void> |
Plays the previous track from history. |
shuffle(): void |
Shuffles the queue, saving the original order. |
undoShuffle(): void |
Restores the queue to its pre-shuffle order. |
toggleLoopQueue(): boolean |
Toggles queue looping, returns new state. |
toggleLoopTrack(): boolean |
Toggles single-track looping, returns new state. |
startRadio(urls: string[]): Promise<void> |
Starts radio mode with shuffled URLs. |
stop(noLeave?: boolean): void |
Stops playback, optionally disconnects. |
disconnect(): void |
Disconnects from the voice channel. |
join(): VoiceConnection |
Joins the voice channel without subscribing player. |
getQueue(): TrackMetadata[] |
Returns a copy of the current queue. |
getVolume(): number |
Returns the current volume (0–200%). |
isPlaying(): boolean |
Checks if a track is playing. |
isPaused(): boolean |
Checks if playback is paused. |
isShuffiled(): boolean |
Checks if the queue is shuffled. |
isConnected(guildId?: string): boolean |
Checks if connected to a voice channel. |
| `searchLyrics(title: string, artist?: string): Promise<string | null>` |
export enum MusicPlayerEvent {
Start = "start",
QueueAdd = "queueAdd",
Pause = "pause",
Resume = "resume",
Stop = "stop",
Skip = "skip",
Previous = "previous",
Shuffle = "shuffle",
LoopQueue = "loopQueue",
LoopTrack = "loopTrack",
VolumeChange = "volumeChange",
Finish = "finish",
Disconnect = "disconnect",
Error = "error"
}Event Payloads:
Start:{ metadata: TrackMetadata, queue: TrackMetadata[] }QueueAdd:{ metadata?: TrackMetadata, metadatas?: TrackMetadata[], queue: TrackMetadata[] }VolumeChange:{ volume: number }Skip:{ queue: TrackMetadata[], history: string[] }Previous:{ metadata: TrackMetadata, queue: TrackMetadata[], history: string[] }Shuffle:{ queue: TrackMetadata[] }LoopQueue:{ enabled: boolean }LoopTrack:{ enabled: boolean }Finish:{ queue: TrackMetadata[], history: string[] }Error:Error- Others: No payload
See types.ts for full type definitions.
This section provides detailed explanations and code snippets for each MusicPlayer method, demonstrating their usage within a Discord bot context using discord.js. The examples assume a MusicPlayer instance is created as shown in the Usage Examples section.
Checks if the given URL is a playlist.
Example:
if (command === 'isplaylist') {
const url = args[0];
const isPlaylist = await player.isPlaylist(url);
message.channel.send(isPlaylist ? 'Yes, this is a playlist.' : 'No, this is not a playlist.');
}searchPlaylists(query: string, platform?: SearchPlatform, limit?: number): Promise<PlaylistMetadata[]>
Searches for playlists across platforms.
Example:
if (command === 'playlists') {
const query = args.join(' ');
const playlists = await player.searchPlaylists(query, 'spotify', 5);
if (playlists.length === 0) {
message.channel.send('No playlists found.');
return;
}
const list = playlists.map((p, i) => `${i + 1}. ${p.title} (${p.trackCount} tracks): ${p.url}`).join('\n');
message.channel.send(`Playlists:\n${list}`);
}Searches for tracks across platforms.
Example:
if (command === 'search') {
const query = args.join(' ');
const results = await player.search(query, 'youtube', 5);
if (results.length === 0) {
message.channel.send('No results found.');
return;
}
const resultList = results.map((r, i) => `${i + 1}. ${r.title || r.url} (${r.source})`).join('\n');
message.channel.send(`Results:\n${resultList}`);
}Plays input. If string, searches and plays first result or loads playlist. If TrackMetadata or array, plays directly.
Example:
if (command === 'play') {
const query = args.join(' ');
if (!query) {
message.channel.send('Please provide a URL or search query.');
return;
}
await player.play(query);
}
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, metadatas, queue }) => {
const added = metadatas ? metadatas.length + ' tracks' : metadata?.title || metadata?.url;
message.channel.send(`➕ Added: ${added} (${queue.length} in queue)`);
});Pauses the current track.
Example:
if (command === 'pause') {
player.pause();
message.channel.send('⏸️ Playback paused.');
}Resumes playback.
Example:
if (command === 'resume') {
player.resume();
message.channel.send('▶️ Playback resumed.');
}Sets volume (0–200%), returns new volume.
Example:
if (command === 'volume') {
const volume = parseInt(args[0]);
if (isNaN(volume)) {
message.channel.send('Please provide a valid volume (0–200).');
return;
}
const newVolume = player.setVolume(volume);
message.channel.send(`🔊 Volume set to ${newVolume}%`);
}
player.on(MusicPlayerEvent.VolumeChange, ({ volume }) => {
message.channel.send(`🔊 Volume changed to ${volume}%`);
});Skips to the next track.
Example:
if (command === 'skip') {
player.skip();
message.channel.send('⏭️ Skipped to next track.');
}
player.on(MusicPlayerEvent.Skip, ({ queue }) => {
message.channel.send(`⏭️ Skipped. ${queue.length} tracks remaining.`);
});Plays the previous track.
Example:
if (command === 'previous') {
await player.previous();
}
player.on(MusicPlayerEvent.Previous, ({ metadata }) => {
message.channel.send(`⏮️ Playing previous: ${metadata.title || metadata.url}`);
});Shuffles the queue.
Example:
if (command === 'shuffle') {
player.shuffle();
message.channel.send('🔀 Queue shuffled.');
}
player.on(MusicPlayerEvent.Shuffle, ({ queue }) => {
message.channel.send(`🔀 Shuffled. ${queue.length} tracks in new order.`);
});Restores pre-shuffle order.
Example:
if (command === 'unshuffle') {
player.undoShuffle();
message.channel.send('🔄 Queue restored.');
}Toggles queue loop, returns state.
Example:
if (command === 'loopqueue') {
const enabled = player.toggleLoopQueue();
message.channel.send(`🔁 Queue loop ${enabled ? 'enabled' : 'disabled'}.`);
}
player.on(MusicPlayerEvent.LoopQueue, ({ enabled }) => {
message.channel.send(`🔁 Queue loop ${enabled ? 'enabled' : 'disabled'}.`);
});Toggles track loop, returns state.
Example:
if (command === 'looptrack') {
const enabled = player.toggleLoopTrack();
message.channel.send(`🔂 Track loop ${enabled ? 'enabled' : 'disabled'}.`);
}
player.on(MusicPlayerEvent.LoopTrack, ({ enabled }) => {
message.channel.send(`🔂 Track loop ${enabled ? 'enabled' : 'disabled'}.`);
});Starts radio mode.
Example:
if (command === 'radio') {
const urls = args; // Array of URLs
await player.startRadio(urls);
message.channel.send('📻 Radio mode started.');
}Stops playback.
Example:
if (command === 'stop') {
player.stop(true); // Stay connected
message.channel.send('⏹️ Stopped.');
}
player.on(MusicPlayerEvent.Stop, () => {
message.channel.send('⏹️ Stopped.');
});Disconnects from voice.
Example:
if (command === 'leave') {
player.disconnect();
message.channel.send('🔌 Disconnected.');
}
player.on(MusicPlayerEvent.Disconnect, () => {
message.channel.send('🔌 Disconnected.');
});Joins voice channel without player subscribe.
Example:
if (command === 'join') {
const connection = player.join();
message.channel.send('🔗 Joined voice channel.');
}Gets queue copy.
Example:
if (command === 'queue') {
const queue = player.getQueue();
const list = queue.map((t, i) => `${i + 1}. ${t.title || t.url}`).join('\n');
message.channel.send(`📃 Queue:\n${list || 'Empty'}`);
}Gets volume.
Example:
if (command === 'volume') {
message.channel.send(`🔊 Volume: ${player.getVolume()}%`);
}Checks playing.
Example:
if (command === 'status') {
message.channel.send(`🎵 ${player.isPlaying() ? 'Playing' : 'Not playing'}.`);
}Checks paused.
Example:
if (command === 'status') {
message.channel.send(`⏯️ ${player.isPaused() ? 'Paused' : 'Not paused'}.`);
}Checks shuffled.
Example:
if (command === 'status') {
message.channel.send(`🔀 Queue is ${player.isShuffiled() ? 'shuffled' : 'not shuffled'}.`);
}Checks connected.
Example:
if (command === 'status') {
message.channel.send(`🔗 ${player.isConnected() ? 'Connected' : 'Not connected'}.`);
}Fetches lyrics.
Example:
if (command === 'lyrics') {
const title = args.join(' ');
const lyrics = await player.searchLyrics(title, 'artist');
message.channel.send(lyrics ? `🎵 Lyrics:\n${lyrics}` : 'No lyrics found.');
}- Repository: https://github.com/Persian-Caesar/discord-player
- Issues: https://github.com/Persian-Caesar/discord-player/issues
- Community: Join the Persian Caesar Discord for support.
- Contributions: Pull requests are welcome! Please follow the contribution guidelines in the repository.
This project is licensed under the MIT License. See the LICENSE file or the repository for details.
⌨️ Built with ❤️ by Sobhan-SRZA for Persian Caesar. Star the repo if you find it useful!