Compare commits
3 Commits
253f369a89
...
9e02e50693
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e02e50693 | |||
| 9d7ff5e7e7 | |||
| 3ba230e6e9 |
@ -1,4 +1,4 @@
|
|||||||
FROM node:18-alpine
|
FROM node:23-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@ -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: true
|
bandcamp: false
|
||||||
soundcloud: true
|
soundcloud: false
|
||||||
twitch: true
|
twitch: false
|
||||||
vimeo: true
|
vimeo: false
|
||||||
nico: true
|
nico: false
|
||||||
http: true # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address.
|
http: false # 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
|
||||||
|
|||||||
@ -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
|
# Mount local plugins directory into the container with SELinux label
|
||||||
- ./plugins:/plugins:ro
|
- ./plugins:/plugins:ro,Z
|
||||||
# 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
|
||||||
|
|||||||
@ -9,13 +9,23 @@ 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) {
|
||||||
@ -67,9 +77,32 @@ 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. Search for tracks
|
// 4. Determine search identifier based on query and source
|
||||||
|
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
|
||||||
query: query,
|
identifier: identifier, // Pass the constructed identifier
|
||||||
requester: interaction.user
|
requester: interaction.user
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,7 +114,7 @@ module.exports = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Add track(s) to queue and create response embed
|
// 6. 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)
|
||||||
|
|||||||
@ -55,11 +55,19 @@ 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}`);
|
||||||
// Start playback
|
try {
|
||||||
await this.connection.playTrack({ track: track.encoded });
|
// Start playback
|
||||||
this.playing = true;
|
await this.connection.playTrack({ track: track.encoded });
|
||||||
|
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;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -116,8 +124,10 @@ 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;
|
||||||
@ -176,14 +186,15 @@ class MusicPlayer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
connection.on('exception', (error) => {
|
connection.on('exception', (error) => {
|
||||||
logger.error(`Track error in guild ${player.guild}: ${error.message}`);
|
logger.error(`Track exception in guild ${player.guild}: ${error.message || 'Unknown error'}`);
|
||||||
|
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 while trying to play: ${player.current?.info?.title || 'the track'}.
|
channel.send(`An error occurred during playback: ${error.message || 'Unknown error'}`).catch(e =>
|
||||||
Details: ${error.message || 'Unknown error'}`).catch(e =>
|
logger.error(`Failed to send trackException message: ${e.message}`)
|
||||||
logger.error(`Failed to send trackError message: ${e.message}`)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Attempt to skip to the next track on exception
|
||||||
player.skip();
|
player.skip();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -208,28 +219,18 @@ 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.query The search query
|
* @param {string} options.identifier The pre-constructed search identifier (e.g., 'ytsearch:query', 'scsearch:query', or a URL)
|
||||||
* @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({ query, requester }) {
|
async search({ identifier, requester }) { // Accept identifier directly
|
||||||
// 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 {
|
||||||
// Determine search type and prepare the identifier string
|
// Perform the search using the provided identifier string
|
||||||
let identifier;
|
logger.debug(`Performing search with identifier: ${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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user