Skip to content

Persian-Caesar/discord-player

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@persian-caesar/discord-player

A lightweight, type-safe music player for Discord self-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 features like queue management, looping, shuffling, and lyrics retrieval.

Table of Contents

Introduction

@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 using libraries like ytdl-core, play-dl, and soundcloud-downloader. 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 queue management, history tracking, and event-driven notifications.

Developed by Sobhan-SRZA for Persian Caesar, this package is licensed under MIT and actively maintained.

Features

  • Multi-Platform Streaming: Supports YouTube, SoundCloud, Spotify, and Deezer via ytdl-core, play-dl, and soundcloud-downloader.
  • Queue Management: Add tracks to a queue, 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.

Installation

Install the package:

npm install @persian-caesar/discord-player

Ensure you have Node.js version 16 or higher, as specified in package.json.

Dependencies

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.
ytdl-core ^4.11.5 Streams audio from YouTube videos, with fallback support for reliability.
ytdl-core-discord ^1.3.1 Alternative YouTube streaming library for compatibility.
@distube/ytdl-core ^4.16.10 Enhanced YouTube streaming with additional features and reliability.
play-dl ^1.9.7 Streams audio from Spotify, YouTube, and Deezer with search capabilities.
soundcloud-downloader ^1.0.0 Downloads and streams audio from SoundCloud URLs.
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/voice and @discordjs/opus are core to Discord voice functionality, enabling the bot to join channels and stream audio.
  • Multiple YouTube streaming libraries (ytdl-core, ytdl-core-discord, @distube/ytdl-core) ensure robust streaming with fallbacks for reliability.
  • play-dl adds support for Spotify and Deezer, broadening the range of supported platforms.
  • soundcloud-downloader enables streaming from SoundCloud, a popular music platform.
  • html-to-text is used for scraping and cleaning lyrics from Google search results.
  • libsodium-wrappers and ffmpeg-static are required for secure and efficient audio processing.

Usage Examples

Below are two 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.

TypeScript Example

This example shows how to create a Discord bot that uses MusicPlayer to play music in a voice channel and handle commands.

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, 50, {
    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, queue }) => {
    message.channel.send(`➕ Added to queue: ${metadata.title || metadata.url} (${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 === 'pause') {
    player.pause();
    message.channel.send('⏸️ Paused playback.');
  } else if (command === 'resume') {
    player.resume();
    message.channel.send('▶️ Resumed playback.');
  } else if (command === 'skip') {
    player.skip();
    message.channel.send('⏭️ Skipped to next track.');
  } else if (command === 'stop') {
    player.stop();
    message.channel.send('⏹️ Stopped playback.');
  } else if (command === 'lyrics') {
    const title = args.join(' ');
    if (!title) {
      message.channel.send('Please provide a song title.');
      return;
    }
    const lyrics = await player.searchLyrics(title);
    message.channel.send(lyrics ? `🎵 Lyrics:\n${lyrics}` : '❌ No lyrics found.');
  }
});

client.login(TOKEN);

Steps to run:

  1. Save the above code as bot.ts.
  2. Replace YOUR_BOT_TOKEN with your Discord bot token.
  3. Ensure all dependencies are installed.
  4. Compile with tsc bot.ts and run with node bot.js.

JavaScript Example

This example is similar but uses plain JavaScript with JSDoc annotations for type hints.

const { Client, GatewayIntentBits } = require('discord.js');
const { MusicPlayer, MusicPlayerEvent } = require('@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('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, 50, {
    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, queue }) => {
    message.channel.send(`➕ Added to queue: ${metadata.title || metadata.url} (${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 === 'pause') {
    player.pause();
    message.channel.send('⏸️ Paused playback.');
  } else if (command === 'resume') {
    player.resume();
    message.channel.send('▶️ Resumed playback.');
  } else if (command === 'skip') {
    player.skip();
    message.channel.send('⏭️ Skipped to next track.');
  } else if (command === 'stop') {
    player.stop();
    message.channel.send('⏹️ Stopped playback.');
  } else if (command === 'lyrics') {
    const title = args.join(' ');
    if (!title) {
      message.channel.send('Please provide a song title.');
      return;
    }
    const lyrics = await player.searchLyrics(title);
    message.channel.send(lyrics ? `🎵 Lyrics:\n${lyrics}` : '❌ No lyrics found.');
  }
});

client.login(TOKEN);

Steps to run:

  1. Save the above code as bot.js.
  2. Replace YOUR_BOT_TOKEN with your Discord bot token.
  3. Ensure all dependencies are installed.
  4. Run with node bot.js.

API Reference

MusicPlayer Class

Constructor:

new MusicPlayer(
  channel: VoiceChannel,
  initialVolume?: number, // Default: 100
  options?: MusicPlayerOptions // { autoLeaveOnEmptyQueue?: boolean, autoLeaveOnIdleMs?: number }
)

Methods:

Method Description
play(input: string) Plays a track by URL or search query, enqueues if playing.
pause() Pauses the current track.
resume() Resumes playback.
setVolume(percent: number) Sets volume (0–200%).
skip() Skips to the next track in the queue.
previous() Plays the previous track from history.
shuffle() Shuffles the queue, saving the original order.
undoShuffle() Restores the queue to its pre-shuffle order.
toggleLoopQueue() Toggles queue looping.
toggleLoopTrack() Toggles single-track looping.
startRadio(urls: string[]) Starts radio mode with shuffled URLs.
stop(noLeave?: boolean) Stops playback, optionally disconnects.
disconnect() Disconnects from the voice channel.
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.
searchLyrics(title: string, artist?: string) Fetches song lyrics from Google.

MusicPlayerEvent Enum

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, queue: TrackMetadata[] }
  • VolumeChange: { volume: number }
  • Skip: { queue: TrackMetadata[], history: string[] }
  • Previous: { metadata: TrackMetadata, queue: TrackMetadata[], history: string[] }
  • Shuffle: { queue: TrackMetadata[] }
  • Finish: { queue: TrackMetadata[], history: string[] }
  • Error: Error
  • Others: No payload

See types.ts for full type definitions.

Method Usage and Examples

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.

play(input: string): Promise<void>

Plays a track by URL or search query. If a track is already playing, it adds the new track to the queue.

Example:

// Command: !play <query>
if (command === 'play') {
  const query = args.join(' ');
  if (!query) {
    message.channel.send('Please provide a URL or search query.');
    return;
  }
  await player.play(query);
  // The Start or QueueAdd event will handle the response
}
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
  message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, queue }) => {
  message.channel.send(`➕ Added to queue: ${metadata.title || metadata.url} (${queue.length} in queue)`);
});

pause(): void

Pauses the currently playing track.

Example:

if (command === 'pause') {
  player.pause();
  message.channel.send('⏸️ Playback paused.');
}
player.on(MusicPlayerEvent.Pause, () => {
  message.channel.send('⏸️ Playback paused.');
});

resume(): void

Resumes playback of a paused track.

Example:

if (command === 'resume') {
  player.resume();
  message.channel.send('▶️ Playback resumed.');
}
player.on(MusicPlayerEvent.Resume, () => {
  message.channel.send('▶️ Playback resumed.');
});

setVolume(percent: number): void

Sets the playback volume (0–200%). Values outside this range are capped at 200%.

Example:

if (command === 'volume') {
  const volume = parseInt(args[0]);
  if (isNaN(volume)) {
    message.channel.send('Please provide a valid volume (0–200).');
    return;
  }
  player.setVolume(volume);
  // The VolumeChange event will handle the response
}
player.on(MusicPlayerEvent.VolumeChange, ({ volume }) => {
  message.channel.send(`🔊 Volume set to ${volume}%`);
});

skip(): void

Skips the current track and plays the next track in the queue.

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 in queue.`);
});

previous(): Promise<void>

Plays the previous track from the history, if available.

Example:

if (command === 'previous') {
  await player.previous();
  // The Previous event will handle the response
}
player.on(MusicPlayerEvent.Previous, ({ metadata }) => {
  message.channel.send(`⏮️ Playing previous track: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.Error, (error) => {
  if (error.message.includes('No track to previous')) {
    message.channel.send('❌ No previous track available.');
  }
});

shuffle(): void

Shuffles the queue and saves the original order for potential restoration.

Example:

if (command === 'shuffle') {
  player.shuffle();
  message.channel.send('🔀 Queue shuffled.');
}
player.on(MusicPlayerEvent.Shuffle, ({ queue }) => {
  message.channel.send(`🔀 Queue shuffled. ${queue.length} tracks in new order.`);
});

undoShuffle(): void

Restores the queue to its pre-shuffle order, excluding played tracks.

Example:

if (command === 'unshuffle') {
  player.undoShuffle();
  message.channel.send('🔄 Queue restored to original order.');
}
player.on(MusicPlayerEvent.Shuffle, ({ queue }) => {
  message.channel.send(`🔄 Queue restored. ${queue.length} tracks in queue.`);
});

toggleLoopQueue(): void

Toggles queue looping on or off.

Example:

if (command === 'loopqueue') {
  player.toggleLoopQueue();
  message.channel.send(`🔁 Queue looping ${player.isLoopQueue() ? 'enabled' : 'disabled'}.`);
}

toggleLoopTrack(): void

Toggles single-track looping on or off.

Example:

if (command === 'looptrack') {
  player.toggleLoopTrack();
  message.channel.send(`🔂 Track looping ${player.isLoopTrack() ? 'enabled' : 'disabled'}.`);
}

startRadio(urls: string[]): Promise<void>

Starts radio mode by shuffling a list of URLs and playing them continuously.

Example:

if (command === 'radio') {
  const urls = args; // Array of URLs
  if (!urls.length) {
    message.channel.send('Please provide at least one URL.');
    return;
  }
  await player.startRadio(urls);
  // The Start event will handle the response
}
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
  message.channel.send(`📻 Radio mode started: ${metadata.title || metadata.url}`);
});

stop(noLeave?: boolean): void

Stops playback, clears the queue and history, and optionally disconnects from the voice channel.

Example:

if (command === 'stop') {
  player.stop(true); // Stay in voice channel
  message.channel.send('⏹️ Playback stopped.');
}
player.on(MusicPlayerEvent.Stop, () => {
  message.channel.send('⏹️ Playback stopped.');
});

disconnect(): void

Disconnects the bot from the voice channel and clears all resources.

Example:

if (command === 'disconnect') {
  player.disconnect();
  message.channel.send('🔌 Disconnected from voice channel.');
}
player.on(MusicPlayerEvent.Disconnect, () => {
  message.channel.send('🔌 Disconnected from voice channel.');
});

getQueue(): TrackMetadata[]

Returns a copy of the current queue as an array of TrackMetadata.

Example:

if (command === 'queue') {
  const queue = player.getQueue();
  if (!queue.length) {
    message.channel.send('📃 Queue is empty.');
    return;
  }
  const queueList = queue.map((track, index) => `${index + 1}. ${track.title || track.url}`).join('\n');
  message.channel.send(`📃 Queue:\n${queueList}`);
}

getVolume(): number

Returns the current volume as a percentage (0–200%).

Example:

if (command === 'volume') {
  const currentVolume = player.getVolume();
  message.channel.send(`🔊 Current volume: ${currentVolume}%`);
}

isPlaying(): boolean

Checks if a track is currently playing.

Example:

if (command === 'status') {
  const status = player.isPlaying() ? 'playing' : 'not playing';
  message.channel.send(`🎵 Player is ${status}.`);
}

isPaused(): boolean

Checks if the player is currently paused.

Example:

if (command === 'status') {
  const paused = player.isPaused() ? 'paused' : 'not paused';
  message.channel.send(`⏯️ Playback is ${paused}.`);
}

isShuffiled(): boolean

Checks if the queue is shuffled.

Example:

if (command === 'status') {
  const shuffled = player.isShuffiled() ? 'shuffled' : 'not shuffled';
  message.channel.send(`🔀 Queue is ${shuffled}.`);
}

searchLyrics(title: string, artist?: string): Promise<string | null>

Fetches song lyrics from Google based on the provided title and optional artist.

Example:

if (command === 'lyrics') {
  const title = args.join(' ');
  if (!title) {
    message.channel.send('Please provide a song title.');
    return;
  }
  const lyrics = await player.searchLyrics(title);
  message.channel.send(lyrics ? `🎵 Lyrics:\n${lyrics}` : '❌ No lyrics found.');
}

Support and Contributions

License

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!

Contact

Packages