Skip to content

Live Radio stations and Youtube url to mp3 #352

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
38 changes: 38 additions & 0 deletions buttons/radio_stop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { EmbedBuilder } = require('discord.js');
const { Translate } = require('../process_tools');
const { stopRadio } = require('../utils/radioPlayer');

module.exports = async ({ client, inter }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused client parameter

try {
// Only allow the requester or admins to stop the radio
if (inter.member.permissions.has('ADMINISTRATOR') || inter.message.interaction.user.id === inter.user.id) {
const result = stopRadio(inter.guild.id);

const embed = new EmbedBuilder()
.setColor('#2f3136')
.setAuthor({ name: await Translate(result ?
`Radio playback has been stopped. <✅>` :
`No active radio to stop. <❌>`)
});

return inter.update({ embeds: [embed], components: [] });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This causes an "InteractionAlreadyReplied" error

} else {
return inter.reply({
content: await Translate(`Only the person who started the radio or an administrator can stop it.`),
ephemeral: true
});
}
} catch (error) {
console.error('Error in radio_stop button handler:', error);

// Try to provide some feedback even if there's an error
try {
return inter.reply({
content: await Translate(`There was an error stopping the radio. Please try again.`),
ephemeral: true
}).catch(console.error);
} catch (replyError) {
console.error('Failed to send error message:', replyError);
}
}
};
67 changes: 67 additions & 0 deletions buttons/youtube_stop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const { EmbedBuilder } = require('discord.js');
const { Translate } = require('../process_tools');

module.exports = async ({ client, inter }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused client parameter

try {
// Get the active connections from the YouTube command
const activeConnections = require('../commands/music/youtube').activeConnections;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connections should be fetched from discord-player's hooks (useQueue)


// Only allow the requester or admins to stop the YouTube player
if (inter.member.permissions.has('ADMINISTRATOR') || inter.message.interaction.user.id === inter.user.id) {
let result = false;

// Check if there's an active connection for this guild
if (activeConnections && activeConnections.has(inter.guild.id)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use optional chaining (activeConnections?.has(inter.guild.id)) instead (lint)

const connection = activeConnections.get(inter.guild.id);
connection.destroy();
activeConnections.delete(inter.guild.id);
result = true;
}

const embed = new EmbedBuilder()
.setColor('#FF0000')
.setAuthor({ name: await Translate(result ?
`YouTube playback has been stopped. <✅>` :
`No active YouTube player to stop. <❌>`)
});

// Use try-catch to handle potential "already replied" errors
try {
// Try to update the message first
await inter.update({ embeds: [embed], components: [] });
} catch (updateError) {
console.log('Could not update interaction, trying to reply instead:', updateError.message);

// If update fails, try to reply
try {
await inter.reply({ embeds: [embed], ephemeral: true });
} catch (replyError) {
console.error('Failed to reply to interaction:', replyError.message);
}
}
} else {
// For unauthorized users, use deferReply + editReply pattern which is more reliable
try {
await inter.deferReply({ ephemeral: true });
await inter.editReply({
content: await Translate(`Only the person who started the YouTube player or an administrator can stop it.`)
});
} catch (error) {
console.error('Failed to respond to unauthorized user:', error.message);
}
}
} catch (error) {
console.error('Error in youtube_stop button handler:', error);

// Try to provide some feedback even if there's an error
try {
// Use deferReply + editReply pattern which is more reliable
await inter.deferReply({ ephemeral: true }).catch(console.error);
await inter.editReply({
content: await Translate(`There was an error stopping the YouTube player. Please try again.`)
}).catch(console.error);
} catch (replyError) {
console.error('Failed to send error message:', replyError);
}
}
};
19 changes: 18 additions & 1 deletion commands/music/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ const { ApplicationCommandOptionType, EmbedBuilder } = require('discord.js');
const { AudioFilters, useQueue } = require('discord-player');
const { Translate } = require('../../process_tools');

// Adding custom anti-static filters
AudioFilters.define('antistatic', 'highpass=f=200,lowpass=f=15000,silenceremove=start_periods=1:detection=peak');
AudioFilters.define('clearvoice', 'pan=stereo|c0=c0|c1=c1,highpass=f=75,lowpass=f=12000,dynaudnorm=f=150:g=15:p=0.7');
AudioFilters.define('crystalclear', 'volume=1.5,highpass=f=60,lowpass=f=17000,afftdn=nr=10:nf=-25:tn=1,loudnorm=I=-16:TP=-1.5:LRA=11');
AudioFilters.define('crisp', 'treble=g=5,bass=g=2:f=110:w=0.6,volume=1.25,loudnorm');

module.exports = {
name: 'filter',
description:('Add a filter to your track'),
Expand All @@ -12,7 +18,18 @@ module.exports = {
description:('The filter you want to add'),
type: ApplicationCommandOptionType.String,
required: true,
choices: [...Object.keys(AudioFilters.filters).map(m => Object({ name: m, value: m })).splice(0, 25)],
choices: [
// Add custom filters at the top for better visibility
{ name: 'antistatic', value: 'antistatic' },
{ name: 'clearvoice', value: 'clearvoice' },
{ name: 'crystalclear', value: 'crystalclear' },
{ name: 'crisp', value: 'crisp' },
// Include all standard filters
...Object.keys(AudioFilters.filters)
.filter(f => !['antistatic', 'clearvoice', 'crystalclear', 'crisp'].includes(f))
.map(m => ({ name: m, value: m }))
.splice(0, 21) // Limit to 21 to stay under 25 choices with our 4 custom ones
],
}
],

Expand Down
80 changes: 61 additions & 19 deletions commands/music/play.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,95 @@ const { Translate } = require('../../process_tools');

module.exports = {
name: 'play',
description:("Play a song!"),
description:("Play a song from YouTube, Spotify, or other sources"),
voiceChannel: true,
options: [
{
name: 'song',
description:('The song you want to play'),
description:('The song you want to play (title, artist, or partial URL)'),
type: ApplicationCommandOptionType.String,
required: true,
},
{
name: 'quality',
description:('Audio quality (higher uses more bandwidth)'),
type: ApplicationCommandOptionType.String,
required: false,
choices: [
{ name: 'Low', value: 'low' },
{ name: 'Medium', value: 'medium' },
{ name: 'High', value: 'high' }
]
}
],

async execute({ inter, client }) {
const player = useMainPlayer();

const song = inter.options.getString('song');
const res = await player.search(song, {
requestedBy: inter.member,
searchEngine: QueryType.AUTO
});

let defaultEmbed = new EmbedBuilder().setColor('#2f3136');

if (!res?.tracks.length) {
defaultEmbed.setAuthor({ name: await Translate(`No results found... try again ? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed] });
const qualityOption = inter.options.getString('quality') || 'high';

const defaultEmbed = new EmbedBuilder().setColor('#2f3136');

// Set quality based on user selection
let volumeLevel = client.config.opt.volume;

switch(qualityOption) {
case 'low':
volumeLevel = Math.min(volumeLevel, 70);
break;
case 'medium':
volumeLevel = Math.min(volumeLevel, 80);
break;
case 'high':
volumeLevel = client.config.opt.volume;
break;
}

try {
// Tell user we're searching
defaultEmbed.setAuthor({ name: await Translate(`Searching for "${song}"... <🔍>`) });
await inter.editReply({ embeds: [defaultEmbed], ephemeral: false });

// Handle normal song playback (YouTube, Spotify, etc.)
const res = await player.search(song, {
requestedBy: inter.member,
searchEngine: QueryType.AUTO
});

if (!res?.tracks.length) {
defaultEmbed.setAuthor({ name: await Translate(`No results found... try again? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed], ephemeral: false });
}

const { track } = await player.play(inter.member.voice.channel, song, {
nodeOptions: {
metadata: {
channel: inter.channel
},
volume: client.config.opt.volume,
volume: volumeLevel,
leaveOnEmpty: client.config.opt.leaveOnEmpty,
leaveOnEmptyCooldown: client.config.opt.leaveOnEmptyCooldown,
leaveOnEnd: client.config.opt.leaveOnEnd,
leaveOnEndCooldown: client.config.opt.leaveOnEndCooldown,
connectionOptions: {
enableLiveBuffer: true
},
// Don't pre-download the track
fetchBeforeQueued: false,
// Stream directly
streamOptions: {
seek: 0,
opusEncoding: true
}
}
});

defaultEmbed.setAuthor({ name: await Translate(`Loading <${track.title}> to the queue... <✅>`) });
await inter.editReply({ embeds: [defaultEmbed] });
defaultEmbed.setAuthor({ name: await Translate(`Now playing: <${track.title}> <✅>`) });
await inter.editReply({ embeds: [defaultEmbed], ephemeral: false });
} catch (error) {
console.log(`Play error: ${error}`);
defaultEmbed.setAuthor({ name: await Translate(`I can't join the voice channel... try again ? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed] });
defaultEmbed.setAuthor({ name: await Translate(`I can't join the voice channel... try again? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed], ephemeral: false });
}
}
}
111 changes: 111 additions & 0 deletions commands/music/radio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const { ApplicationCommandOptionType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { Translate } = require('../../process_tools');
const radioStations = require('../../radioStations');
const { playRadioStation, stopRadio } = require('../../utils/radioPlayer');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused stopRadio import


module.exports = {
name: 'radio',
description: ('Play a live radio station'),
voiceChannel: true,
options: [
{
name: 'station',
description: ('The radio station you want to listen to'),
type: ApplicationCommandOptionType.String,
required: true,
choices: radioStations.map(station => ({
name: station.name,
value: station.name
}))
},
{
name: 'quality',
description: ('Audio quality (higher uses more bandwidth)'),
type: ApplicationCommandOptionType.String,
required: false,
choices: [
{ name: 'Low', value: 'low' },
{ name: 'Medium', value: 'medium' },
{ name: 'High', value: 'high' }
]
}
],

async execute({ inter, client }) {
const stationName = inter.options.getString('station');
const qualityOption = inter.options.getString('quality') || 'high';

const defaultEmbed = new EmbedBuilder().setColor('#2f3136');

// Set quality based on user selection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why base quality off of volume ? Aren't these 2 different things ?

let volumeLevel = client.config.opt.volume;

switch(qualityOption) {
case 'low':
volumeLevel = Math.min(client.config.opt.volume, 70);
break;
case 'medium':
volumeLevel = Math.min(client.config.opt.volume, 80);
break;
case 'high':
volumeLevel = client.config.opt.volume;
break;
}

try {
// Find the selected radio station
const station = radioStations.find(s => s.name === stationName);

if (!station) {
defaultEmbed.setAuthor({ name: await Translate(`Radio station not found. Try again? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed], ephemeral: false });
}

// Tell user we're connecting to the radio station
defaultEmbed.setAuthor({ name: await Translate(`Connecting to ${station.name} radio... <📻>`) });
await inter.editReply({ embeds: [defaultEmbed], ephemeral: false });

// Use our custom radio player instead of discord-player
try {
const result = await playRadioStation({
voiceChannel: inter.member.voice.channel,
interaction: inter,
station: station,
volume: volumeLevel,
client: client
});

if (result.success) {
// Create a stop button that uses the button handler system
const stopButton = new ButtonBuilder()
.setCustomId('radio_stop')
.setLabel('Stop Radio')
.setStyle(ButtonStyle.Danger)
.setEmoji('⏹️');

const row = new ActionRowBuilder().addComponents(stopButton);

// Send a non-ephemeral message with the button
await inter.editReply({
embeds: [result.embed],
components: [row],
ephemeral: false
});

return;
} else {
defaultEmbed.setAuthor({ name: await Translate(`Error playing radio station. Try another station? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed], ephemeral: false });
}
} catch (error) {
console.log(`Radio play error: ${error}`);
defaultEmbed.setAuthor({ name: await Translate(`Error playing radio station. Try another station? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed], ephemeral: false });
}
} catch (error) {
console.log(`Radio command error: ${error}`);
defaultEmbed.setAuthor({ name: await Translate(`I can't join the voice channel... try again? <❌>`) });
return inter.editReply({ embeds: [defaultEmbed], ephemeral: false });
}
}
};
26 changes: 26 additions & 0 deletions commands/music/radiostop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { EmbedBuilder } = require('discord.js');
const { Translate } = require('../../process_tools');
const { stopRadio, activeRadioConnections } = require('../../utils/radioPlayer');

module.exports = {
name: 'radiostop',
description:("Stop the radio playback"),
voiceChannel: true,

async execute({ inter }) {
const defaultEmbed = new EmbedBuilder().setColor('#2f3136');

// Check if there's a radio playing in this guild
if (activeRadioConnections.has(inter.guild.id)) {
const stopped = stopRadio(inter.guild.id);

if (stopped) {
defaultEmbed.setAuthor({ name: await Translate(`Radio playback has been stopped. <✅>`) });
return inter.editReply({ embeds: [defaultEmbed] });
}
}

defaultEmbed.setAuthor({ name: await Translate(`No radio is currently playing. <❌>`) });
return inter.editReply({ embeds: [defaultEmbed] });
}
};
Loading