Discord Bot Setup Guide
Follow these steps to get your Nyuki-powered 24/7 live radio bot running. This guide assumes you have a basic understanding of Discord and the Nyuki API.
Step 1: Project Setup & Dependencies
Create a folder for your bot and install all necessary packages.
npm install discord.js @discordjs/voice axios dotenv ffmpeg-static libsodium-wrappers
Step 2: Credentials
Create a file named .env
in your project folder and add your credentials:
DISCORD_BOT_TOKEN=your_bot_token_here
NYUKI_API_KEY=your_nyuki_api_key_here
BOT_PREFIX=!
Step 3: Example Bot Code
Create a file named bot.js
and paste the complete code below. This version correctly handles the live `seek` time from the API to ensure all listeners are synchronized.
require('dotenv').config();
const { Client, GatewayIntentBits, Partials } = require('discord.js');
const {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
VoiceConnectionStatus,
AudioPlayerStatus,
StreamType,
} = require('@discordjs/voice');
const axios = require('axios');
// --- Configuration ---
const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
const API_KEY = process.env.NYUKI_API_KEY;
const PREFIX = process.env.BOT_PREFIX || '!';
const NYUKI_API_URL = 'https://nyukimusic.top/api';
// --- Discord Client Setup ---
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates,
],
partials: [Partials.Channel],
});
// --- State Management ---
const guildPlayers = new Map();
// --- Axios Instance for API calls ---
const api = axios.create({
baseURL: NYUKI_API_URL,
headers: {
'Authorization': API_KEY,
'User-Agent': 'Nyuki-Discord-Bot/4.0 (Node.js; 24/7 Live)',
},
});
// --- Bot Ready Event ---
client.on('ready', () => {
console.log(`[READY] Logged in as ${client.user.tag}`);
console.log(`[INFO] Ready to stream 24/7 radio.`);
});
// --- Message Handler ---
client.on('messageCreate', async (message) => {
if (message.author.bot || !message.content.startsWith(PREFIX)) return;
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
const command = args.shift().toLowerCase();
// --- Command: !play ---
if (command === 'play') {
const voiceChannel = message.member.voice.channel;
if (!voiceChannel) return message.channel.send('You need to be in a voice channel to play music!');
const station = args[0];
if (!station) return message.channel.send(`Please provide a station. Usage: \`${PREFIX}play lofi\``);
if (guildPlayers.has(message.guild.id)) {
await stopPlayback(message.guild.id);
}
const loadingMessage = await message.channel.send(`đĩ Tuning into the \`${station}\` station...`);
try {
const connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
});
const player = createAudioPlayer();
connection.subscribe(player);
guildPlayers.set(message.guild.id, { connection, player, station });
connection.once(VoiceConnectionStatus.Ready, async () => {
console.log(`[VC-CONNECT] Successfully connected to voice channel: ${voiceChannel.name}`);
await api.post('/session/start', { station });
loadingMessage.edit(`â
Now live with the \`${station}\` station in **${voiceChannel.name}**!`);
playLive(message.guild.id, station); // Start the 24/7 loop
});
// This is the core of the 24/7 feature.
player.on(AudioPlayerStatus.Idle, () => {
console.log(`[IDLE] Track finished for guild ${message.guild.id}. Fetching next live track automatically...`);
// When one song ends, immediately get the next one.
playLive(message.guild.id, station);
});
player.on('error', error => {
console.error(`[PLAYER-ERROR] Audio player error for guild ${message.guild.id}:`, error.message);
// Attempt to recover by fetching the next track after a short delay
setTimeout(() => playLive(message.guild.id, station), 5000);
});
} catch (error) {
console.error('[ERROR] An error occurred in the !play command:', error);
const errorMsg = error.response?.data?.error || 'Could not connect to the Nyuki API.';
loadingMessage.edit(`â **Error:** ${errorMsg}`);
await stopPlayback(message.guild.id); // Clean up on failure
}
}
// --- Command: !stop ---
if (command === 'stop') {
if (guildPlayers.has(message.guild.id)) {
await stopPlayback(message.guild.id);
message.channel.send('âšī¸ Radio stopped. Thanks for listening!');
}
}
});
/**
* The core function to fetch and play a live track.
* @param {string} guildId The ID of the guild.
* @param {string} station The station name.
*/
async function playLive(guildId, station) {
const guildData = guildPlayers.get(guildId);
if (!guildData) {
console.log(`[WARN] playLive called for guild ${guildId}, but no player was found. Aborting loop.`);
return;
}
try {
console.log(`[API-CALL] Fetching live track for station '${station}'...`);
const { data: streamInfo } = await api.get(`/stream/${station}`);
const seekSeconds = parseFloat(streamInfo.seek);
console.log(`[API-RESPONSE] Received stream URL: ${streamInfo.streamUrl}`);
console.log(`[FFMPEG] Will start playback at ${seekSeconds.toFixed(2)} seconds.`);
// **THE CRITICAL FIX IS HERE**
// This ensures FFmpeg seeks to the live time provided by the API.
// The bot has no way to override this.
const resource = createAudioResource(streamInfo.streamUrl, {
ffmpegArgs: ['-ss', `${seekSeconds}s`],
inputType: StreamType.FFmpeg,
});
guildData.player.play(resource);
console.log(`[PLAYING] Streaming live from station '${station}'.`);
} catch (error) {
const errorMsg = error.response?.data?.error || "Connection to API failed.";
console.error(`[ERROR] Failed to fetch next track for station '${station}':`, errorMsg);
if (guildData.player.state.status !== AudioPlayerStatus.Playing) {
console.warn(`[RETRY] Player is not running. Retrying fetch in 5 seconds...`);
setTimeout(() => playLive(guildId, station), 5000);
}
}
}
/**
* Stops playback and cleans up resources for a guild.
* @param {string} guildId The ID of the guild.
*/
async function stopPlayback(guildId) {
const guildData = guildPlayers.get(guildId);
if (guildData) {
console.log(`[STOP] Stopping playback and destroying connection for guild: ${guildId}`);
guildData.player.stop(true);
guildData.connection.destroy();
guildPlayers.delete(guildId);
try {
await api.post('/session/stop');
console.log('[API-CALL] Successfully notified API of session stop.');
} catch (error) {
console.error('[ERROR] Failed to notify API about session stop:', error.response?.data?.error);
}
}
}
client.login(BOT_TOKEN);