Compare commits

..

No commits in common. "9e02e50693fac71ad6af47c93fcef23b7acbb146" and "253f369a890d20c115e4ec62de4c8e2ef44607b9" have entirely different histories.

5 changed files with 37 additions and 71 deletions

View File

@ -1,4 +1,4 @@
FROM node:23-slim FROM node:18-alpine
WORKDIR /app WORKDIR /app

View File

@ -30,12 +30,12 @@ lavalink:
sources: sources:
# The default Youtube source is now deprecated and won't receive further updates. Please use https://github.com/lavalink-devs/youtube-source#plugin instead. # The default Youtube source is now deprecated and won't receive further updates. Please use https://github.com/lavalink-devs/youtube-source#plugin instead.
youtube: false youtube: false
bandcamp: false bandcamp: true
soundcloud: false soundcloud: true
twitch: false twitch: true
vimeo: false vimeo: true
nico: false nico: true
http: false # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address. http: true # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address.
local: false local: false
filters: # All filters are enabled by default filters: # All filters are enabled by default
volume: true volume: true

View File

@ -12,8 +12,8 @@ services:
# Removed LAVALINK_PLUGIN_URLS environment variable # Removed LAVALINK_PLUGIN_URLS environment variable
volumes: volumes:
- ./application.yml:/opt/Lavalink/application.yml:ro,Z - ./application.yml:/opt/Lavalink/application.yml:ro,Z
# Mount local plugins directory into the container with SELinux label # Mount local plugins directory into the container
- ./plugins:/plugins:ro,Z - ./plugins:/plugins:ro
# Add healthcheck to verify Lavalink is ready # Add healthcheck to verify Lavalink is ready
healthcheck: healthcheck:
# Use CMD-SHELL to allow environment variable expansion for the password # Use CMD-SHELL to allow environment variable expansion for the password

View File

@ -9,23 +9,13 @@ module.exports = {
.addStringOption(option => .addStringOption(option =>
option.setName('query') option.setName('query')
.setDescription('The URL or search term for the song/playlist') .setDescription('The URL or search term for the song/playlist')
.setRequired(true)) .setRequired(true)),
.addStringOption(option =>
option.setName('source')
.setDescription('Specify the search source (defaults to YouTube Music)')
.setRequired(false)
.addChoices(
{ name: 'YouTube Music', value: 'youtubemusic' },
{ name: 'YouTube', value: 'youtube' },
{ name: 'SoundCloud', value: 'soundcloud' }
)),
async execute(interaction, client) { async execute(interaction, client) {
await interaction.deferReply(); // Defer reply immediately await interaction.deferReply(); // Defer reply immediately
const member = interaction.member; const member = interaction.member;
const voiceChannel = member?.voice?.channel; const voiceChannel = member?.voice?.channel;
const query = interaction.options.getString('query'); const query = interaction.options.getString('query');
const source = interaction.options.getString('source'); // Get the source option
// 1. Check if user is in a voice channel // 1. Check if user is in a voice channel
if (!voiceChannel) { if (!voiceChannel) {
@ -77,32 +67,9 @@ module.exports = {
logger.info(`Moved player to voice channel ${voiceChannel.name} (${voiceChannel.id}) for play command.`); logger.info(`Moved player to voice channel ${voiceChannel.name} (${voiceChannel.id}) for play command.`);
} }
// 4. Determine search identifier based on query and source // 4. Search for tracks
let identifier;
const isUrl = query.startsWith('http://') || query.startsWith('https://');
if (isUrl) {
identifier = query; // Use URL directly
} else {
// Prepend search prefix based on source or default
switch (source) {
case 'youtube':
identifier = `ytsearch:${query}`;
break;
case 'soundcloud':
identifier = `scsearch:${query}`;
break;
case 'youtubemusic':
default: // Default to YouTube Music if source is 'youtubemusic' or not provided
identifier = `ytmsearch:${query}`;
break;
}
}
logger.debug(`Constructed identifier: ${identifier}`);
// 5. Search for tracks using the constructed identifier
const searchResults = await musicPlayer.search({ // Use the player instance from the client const searchResults = await musicPlayer.search({ // Use the player instance from the client
identifier: identifier, // Pass the constructed identifier query: query,
requester: interaction.user requester: interaction.user
}); });
@ -114,7 +81,7 @@ module.exports = {
return; return;
} }
// 6. Add track(s) to queue and create response embed // 5. Add track(s) to queue and create response embed
const responseEmbed = new EmbedBuilder().setColor('#0099ff'); const responseEmbed = new EmbedBuilder().setColor('#0099ff');
// Add first track (or all tracks if it's a playlist) // Add first track (or all tracks if it's a playlist)

View File

@ -55,19 +55,11 @@ class MusicPlayer {
// Play a track // Play a track
async play(track) { async play(track) {
this.current = track; this.current = track;
logger.debug(`Attempting to play track: ${track.info.title} (${track.info.uri}) in guild ${this.guild}`);
try {
// Start playback // Start playback
await this.connection.playTrack({ track: track.encoded }); await this.connection.playTrack({ track: track.encoded });
this.playing = true; this.playing = true;
logger.debug(`playTrack called successfully for: ${track.info.title}`);
} catch (playError) {
logger.error(`Error calling playTrack for ${track.info.title}: ${playError.message}`);
console.error(playError); // Log full error object
this.playing = false;
this.current = null;
// Maybe try skipping? Or just log and let the 'end' event handle it if it fires.
}
return this; return this;
}, },
@ -124,10 +116,8 @@ class MusicPlayer {
// Add a track to the queue or play it if nothing is playing // Add a track to the queue or play it if nothing is playing
async enqueue(track, immediate = false) { async enqueue(track, immediate = false) {
if (immediate || (!this.playing && !this.current)) { if (immediate || (!this.playing && !this.current)) {
logger.debug(`Enqueue: Playing immediately - ${track.info.title}`);
await this.play(track); await this.play(track);
} else { } else {
logger.debug(`Enqueue: Adding to queue - ${track.info.title}`);
this.queue.push(track); this.queue.push(track);
} }
return this; return this;
@ -186,15 +176,14 @@ class MusicPlayer {
}); });
connection.on('exception', (error) => { connection.on('exception', (error) => {
logger.error(`Track exception in guild ${player.guild}: ${error.message || 'Unknown error'}`); logger.error(`Track error in guild ${player.guild}: ${error.message}`);
console.error("Full track exception details:", error); // Log the full error object
const channel = this.client.channels.cache.get(player.textChannel); const channel = this.client.channels.cache.get(player.textChannel);
if (channel) { if (channel) {
channel.send(`An error occurred during playback: ${error.message || 'Unknown error'}`).catch(e => channel.send(`An error occurred while trying to play: ${player.current?.info?.title || 'the track'}.
logger.error(`Failed to send trackException message: ${e.message}`) Details: ${error.message || 'Unknown error'}`).catch(e =>
logger.error(`Failed to send trackError message: ${e.message}`)
); );
} }
// Attempt to skip to the next track on exception
player.skip(); player.skip();
}); });
@ -219,18 +208,28 @@ class MusicPlayer {
/** /**
* Search for tracks using Shoukaku * Search for tracks using Shoukaku
* @param {Object} options Options for the search * @param {Object} options Options for the search
* @param {string} options.identifier The pre-constructed search identifier (e.g., 'ytsearch:query', 'scsearch:query', or a URL) * @param {string} options.query The search query
* @param {string} options.requester The user who requested the track * @param {string} options.requester The user who requested the track
* @returns {Promise<Array>} Array of track objects * @returns {Promise<Array>} Array of track objects
*/ */
async search({ identifier, requester }) { // Accept identifier directly async search({ query, requester }) {
// Get the first available node // Get the first available node
const node = this.client.shoukaku.options.nodeResolver(this.client.shoukaku.nodes); const node = this.client.shoukaku.options.nodeResolver(this.client.shoukaku.nodes);
if (!node) throw new Error('No available Lavalink nodes!'); if (!node) throw new Error('No available Lavalink nodes!');
try { try {
// Perform the search using the provided identifier string // Determine search type and prepare the identifier string
logger.debug(`Performing search with identifier: ${identifier}`); let identifier;
if (query.startsWith('http')) {
// Direct URL
identifier = query;
} else {
// Search with prefix (Lavalink handles ytsearch/ytmsearch automatically with the plugin)
// identifier = `ytsearch:${query}`; // Prefix might not be needed with the plugin, let Lavalink decide
identifier = query; // Pass the raw query for non-URLs
}
// Perform the search using the identifier string
const result = await node.rest.resolve(identifier); const result = await node.rest.resolve(identifier);
if (!result || result.loadType === 'error' || result.loadType === 'empty') { if (!result || result.loadType === 'error' || result.loadType === 'empty') {
// Log the identifier for debugging if search fails // Log the identifier for debugging if search fails