Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as loop from "./loop";
import * as list from "./list";
import * as playlist from "./playlist";
import * as mc_stats from "./mc_stats";
import * as radio from "./radio";

type CommnadConfig = {
name: string;
Expand Down Expand Up @@ -35,6 +36,7 @@ const commands: CommandsCollection = {
list,
playlist,
mc_stats,
radio,
};

const slashCommands: CommandsCollection = {};
Expand Down
120 changes: 120 additions & 0 deletions commands/radio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import { request } from "undici";

import {
AudioPlayerStatus,
createAudioPlayer,
createAudioResource,
entersState,
getVoiceConnection,
joinVoiceChannel,
StreamType,
VoiceConnectionStatus,
} from "@discordjs/voice";
import { songQueue } from "../constants";
import { shouldDisconnect } from "../utils/voice";

export const config = {
name: "radio",
description: "Play radio stream",
usage: "/radio",
slashCommand: true,
};

export const data = new SlashCommandBuilder()
.setName(config.name)
.setDescription(config.description);

export async function execute(interaction: CommandInteraction) {
// @ts-ignore
const voiceChannel = interaction.member?.voice?.channel;

if (!voiceChannel) {
return interaction.reply("You must be in a voice channel!");
}

await interaction.reply("📻 Connecting...");

const guildId = interaction.guildId!;
let connection = getVoiceConnection(guildId!);

if (!connection) {
connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
});
} else if (
connection &&
connection.joinConfig.channelId !== voiceChannel.id
) {
return interaction.editReply(
"Song already playing on another voice channel",
);
}

try {
await entersState(connection, VoiceConnectionStatus.Ready, 20_000);
} catch (error) {
connection.destroy();
return interaction.editReply("❌ Failed to join voice channel.");
}

if (!songQueue.has(guildId)) {
const player = createAudioPlayer();
connection.subscribe(player);
songQueue.set(guildId, {
tracks: [],
index: 0,
disconnectTimeout: null,
disconnectInterval: null,
connection,
player,
});
}

const serverQueue = songQueue.get(guildId);

if (serverQueue.disconnectTimeout) {
clearTimeout(serverQueue.disconnectTimeout);
}

if (serverQueue.disconnectInterval) {
clearInterval(serverQueue.disconnectInterval);
}

const response = await request("http://asculta.radioromanian.net:8100/");

const resource = createAudioResource(response.body, {
inputType: StreamType.Arbitrary,
});

connection.subscribe(serverQueue.player);

serverQueue.player.stop();
serverQueue.isRadio = true;
serverQueue.player.play(resource);

serverQueue.player.on(AudioPlayerStatus.Playing, () => {
console.log("✅ Playing radio stream");
});

serverQueue.player.on(AudioPlayerStatus.Idle, () => {
console.log("⚠️ Stream ended or idle");
});

serverQueue.player.on("error", (error) => {
console.error("❌ Player error:", error);
});

serverQueue.disconnectInterval = setInterval(() => {
console.log(serverQueue.player.state.status);
if (shouldDisconnect(serverQueue)) {
serverQueue.connection.destroy();
songQueue.delete(serverQueue.connection.guildId!);
clearInterval(serverQueue.disconnectTimeout);
}
}, Number(process.env.DC_IDLE));

await interaction.editReply("📻 Now playing radio!");
}
5 changes: 2 additions & 3 deletions commands/skip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ export async function execute(interaction: CommandInteraction) {

const index = serverQueue.index + skip_no;

console.log(serverQueue.tracks.length, index);
if (index > serverQueue.tracks.length) {
if (index > serverQueue.tracks.length - 1) {
return interaction.reply({
content: `Skip number ${index} is greater then the length of the playlist: ${serverQueue.tracks.length}`,
content: `No next song to play`,
flags: MessageFlags.Ephemeral,
});
}
Expand Down
147 changes: 78 additions & 69 deletions commands/stop.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,78 @@
import {
CommandInteraction,
SlashCommandBuilder,
MessageFlags,
} from "discord.js";

import { songQueue } from "../constants";
import { getVoiceConnection } from "@discordjs/voice";

import Sentry from "@sentry/node";

export const config = {
name: "stop",
description: "Stop song from playing and clears queue",
usage: "/stop",
slashCommand: true,
};

export const data = new SlashCommandBuilder()
.setName(config.name)
.setDescription(config.description);

export async function execute(interaction: CommandInteraction) {
// @ts-ignore
const voiceChannel = interaction.member?.voice?.channel;
if (!voiceChannel) {
return interaction.reply({
content: "Not connected to voice channel",
flags: MessageFlags.Ephemeral,
});
}

const guildId = interaction.guildId;
const connection = getVoiceConnection(guildId!);

if (!connection) {
return interaction.reply({
content: "Bot not connected to the voice channel",
flags: MessageFlags.Ephemeral,
});
}

if (connection && connection.joinConfig.channelId !== voiceChannel.id) {
return interaction.reply({
content: "Not connected to the correct voice channel",
flags: MessageFlags.Ephemeral,
});
}

if (!songQueue.has(guildId)) {
return interaction.reply({
content: "❌ No active song queue.",
flags: MessageFlags.Ephemeral,
});
}

const serverQueue = songQueue.get(guildId);

try {
serverQueue.player.stop();
songQueue.delete(guildId);

return interaction.reply("⏹️ Stopped playback and cleared queue.");
} catch (error) {
Sentry.captureException(error);

return interaction.reply("Failed to stop playback");
}
}
import {
CommandInteraction,
SlashCommandBuilder,
MessageFlags,
} from "discord.js";

import { songQueue } from "../constants";
import { getVoiceConnection } from "@discordjs/voice";

import Sentry from "@sentry/node";

export const config = {
name: "stop",
description: "Stop song from playing and clears queue",
usage: "/stop",
slashCommand: true,
};

export const data = new SlashCommandBuilder()
.setName(config.name)
.setDescription(config.description);

export async function execute(interaction: CommandInteraction) {
// @ts-ignore
const voiceChannel = interaction.member?.voice?.channel;
if (!voiceChannel) {
return interaction.reply({
content: "Not connected to voice channel",
flags: MessageFlags.Ephemeral,
});
}

const guildId = interaction.guildId;
const connection = getVoiceConnection(guildId!);

if (!connection) {
return interaction.reply({
content: "Bot not connected to the voice channel",
flags: MessageFlags.Ephemeral,
});
}

if (connection && connection.joinConfig.channelId !== voiceChannel.id) {
return interaction.reply({
content: "Not connected to the correct voice channel",
flags: MessageFlags.Ephemeral,
});
}

if (!songQueue.has(guildId)) {
return interaction.reply({
content: "❌ No active song queue.",
flags: MessageFlags.Ephemeral,
});
}

const serverQueue = songQueue.get(guildId);

try {
serverQueue.player.stop();

if (serverQueue.disconnectInterval) {
clearInterval(serverQueue.disconnectInterval);
}

if (serverQueue.disconnectTimeout) {
clearTimeout(serverQueue.disconnectTimeout);
}

songQueue.delete(guildId);

return interaction.reply("⏹️ Stopped playback and cleared queue.");
} catch (error) {
Sentry.captureException(error);

return interaction.reply("Failed to stop playback");
}
}
Loading