i Dont know what wrong i did but i wanna fix my button please help when i click it says failed
/**
Made by Sandesh Rai
Music events — replace now-playing canvas/components with a
ComponentsV2 "PLAYED" summary when a track ends and nothing is queued.
Updated: icon-only buttons (no labels) — Pause/Next/Stop + Shuffle/Fav/Lyrics
Exports:
-
- setupMusicEvents(client)
-
- _internal_placePlayedSummary: placePlayedSummary(channel, player, finishedTrack, { force })
Notes:
-
- The handler will skip sending the summary if player._suppressPlayed === true.
This lets your prefix
,stopcommand suppress automatic event-driven summariesto avoid duplicates. (Optional: set that flag before destroying player.)
*/
const {
ContainerBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
MediaGalleryItemBuilder,
SeparatorBuilder,
SeparatorSpacingSize,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags,
ComponentType,
AttachmentBuilder
} = require('discord.js');
const { MusicCard } = require('../helpers/MusicCard');
const Favorite = require('../../database/models/Favorite');
const config = require('../config');
const musicCard = new MusicCard();
const log = {
debug: (msg, ...args) => { if (config.DEBUG) console.debug('[MUSIC_EVENTS]', msg, ...args); },
info: (msg, ...args) => { if (config.DEBUG) console.log('[MUSIC_EVENTS]', msg, ...args); },
warn: (msg, ...args) => console.warn('[MUSIC_EVENTS]', msg, ...args),
error: (msg, ...args) => console.error('[MUSIC_EVENTS]', msg, ...args)
};
const favoriteCache = new Map();
const FAVORITE_CACHE_TTL = Number(config.FAVORITE_CACHE_TTL_MS || 30_000);
function cacheFavoriteKey(userId, identifier) { return ${userId}:${identifier}; }
async function isTrackFavorited(userId, identifier) {
if (!userId || !identifier) return false;
const key = cacheFavoriteKey(userId, identifier);
const entry = favoriteCache.get(key);
if (entry && (Date.now() - entry.ts) < FAVORITE_CACHE_TTL) return entry.value;
try {
const fav = await Favorite.findOne({ where: { userId, identifier } });
const exists = !!fav;
favoriteCache.set(key, { ts: Date.now(), value: exists });
return exists;
} catch (err) {
log.warn('Favorite lookup failed', err?.message || err);
return false;
}
}
function textContainer(text) { return new ContainerBuilder().addTextDisplayComponents(new TextDisplayBuilder().setContent(text)); }
/**
ICON-ONLY button rows
Row 1: Pause/Resume (⏯️) | Next (⏭️) | Stop (⏹️)
Row 2: Shuffle (🔀) | Favorite (💖) | Lyrics (🎤)
We keep customIds the same as existing handlers so no logic changes are needed.
*/
function getFirstControlButtonRow(isPaused, disabled = false, autoplayEnabled = false) {
return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('music_pause_resume')
.setEmoji('⏯️')
.setStyle(ButtonStyle.Secondary)
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId('music_skip')
.setEmoji('⏭️')
.setStyle(ButtonStyle.Secondary)
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId('music_stop')
.setEmoji('⏹️')
.setStyle(ButtonStyle.Danger)
.setDisabled(disabled)
);
}
function getSecondControlButtonRow(disabled = false) {
return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('music_shuffle')
.setEmoji('🔀')
.setStyle(ButtonStyle.Secondary)
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId('music_favorite_add')
.setEmoji('💖')
.setStyle(ButtonStyle.Primary)
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId('music_lyrics')
.setEmoji('🎤')
.setStyle(ButtonStyle.Secondary)
.setDisabled(disabled)
);
}
function buildNowPlayingContainer(track, player, hasAttachment) {
const text = ## Now Playing\n[${track.info?.title || 'Unknown Title'}](${track.info?.uri || ''})\n\n;
const container = new ContainerBuilder();
if (hasAttachment) {
container.addMediaGalleryComponents(new MediaGalleryBuilder().addItems([new MediaGalleryItemBuilder().setURL('attachment://nowplaying.png')]));
} else {
const art = track.info?.artworkUrl || track.info?.image;
if (art) container.addMediaGalleryComponents(new MediaGalleryBuilder().addItems([new MediaGalleryItemBuilder().setURL(art)]));
}
container.addTextDisplayComponents(new TextDisplayBuilder().setContent(text));
container
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true))
.addActionRowComponents(getFirstControlButtonRow(player.isPaused, false, !!player.autoplayEnabled))
.addActionRowComponents(getSecondControlButtonRow(false))
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
return container;
}
/**
- Robust queue detection (handles Collections, Arrays, wrappers)
*/
function queueHasItems(player) {
try {
if (!player) return false;
const q = player.queue;
if (!q) return false;
if (typeof q.size === 'number') return q.size > 0;
if (typeof q.length === 'number') return q.length > 0;
if (Array.isArray(q) && q.length > 0) return true;
if (typeof q.peek === 'function') return !!q.peek();
if (typeof q.get === 'function') return !!q.get(0);
if (typeof q[Symbol.iterator] === 'function') {
for (const _ of q) return true;
return false;
}
return false;
} catch (e) {
log.debug('queueHasItems error', e?.message || e);
return false;
}
}
/**
Compare two tracks for "same track" heuristics.
Use identifier > uri > title fallback.
*/
function isSameTrack(a, b) {
try {
if (!a || !b) return false;
const ai = a.info || a;
const bi = b.info || b;
if (!ai || !bi) return false;
if (ai.identifier && bi.identifier && ai.identifier === bi.identifier) return true;
if (ai.uri && bi.uri && ai.uri === bi.uri) return true;
if (ai.title && bi.title && ai.title === bi.title) return true;
return false;
} catch (e) {
return false;
}
}
/**
Check whether the given track appears to be fully played based on position/duration.
Returns true when position >= duration - epsilon.
*/
function trackAppearsFinished(player, track) {
try {
if (!player || !track) return false;
const info = (track.info || track);
if (!info) return false;
const total = Number(info.length || info.duration || info.lengthMs || 0);
const pos = Number.isFinite(player.position) ? Number(player.position) : (Number(player._position || 0));
if (!total || total <= 0) return false; // can't decide for live/unknown
// epsilon 1.5s (1500 ms)
return pos >= Math.max(0, total - 1500);
} catch (e) {
return false;
}
}
/**
Build a prettier ComponentsV2 "PLAYED" summary container.
Layout:
-
- Header: "🎵 PLAYED" (big)
-
- Divider
-
- (optional) artwork media gallery if available
-
- Bold song title + link
-
- Requester line (mention)
-
- Time line (played at)
-
- Divider
*/
function buildPlayedContainerNice(track) {
const title = track?.info?.title || track?.title || 'Unknown Title';
const uri = track?.info?.uri || track?.uri || '';
const artwork = track?.info?.artworkUrl || track?.info?.image || null;
const requesterId =
(track?.info?.requester && track.info.requester.id) ||
(track?.requester && track.requester.id) ||
(typeof track?.info?.requester === 'string' && track.info.requester) ||
null;
// Prefer a provided requestedAt timestamp, else use now
const requestedAtCandidate = track?.info?.requestedAt || track?.requestedAt || track?.info?.timestamp || Date.now();
let requestedAtText = '';
try {
const d = new Date(requestedAtCandidate);
if (!isNaN(d)) requestedAtText = d.toLocaleString();
else requestedAtText = new Date().toLocaleString();
} catch (e) {
requestedAtText = new Date().toLocaleString();
}
const header = 🎵 PLAYED;
const titleLine = **[${title}](${uri})**;
const requesterLine = requesterId ? **Played by** <@${requesterId}> : **Played by** unknown;
const timeLine = **Time:** ${requestedAtText};
const c = new ContainerBuilder();
// Header
c.addTextDisplayComponents(new TextDisplayBuilder().setContent(# ${header}));
// Divider
c.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
// Artwork (if available) — show as a small media gallery above the text
if (artwork) {
try {
c.addMediaGalleryComponents(new MediaGalleryBuilder().addItems([new MediaGalleryItemBuilder().setURL(artwork)]));
// spacer
c.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(false));
} catch (e) {
// ignore artwork errors
}
}
// Song title (bold + link) and small spacer
c.addTextDisplayComponents(new TextDisplayBuilder().setContent(titleLine));
// Divider between title and metadata
c.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
// Requester and time lines
c.addTextDisplayComponents(new TextDisplayBuilder().setContent(${requesterLine}\n${timeLine}));
// Final small divider to finish nicely
c.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
return c;
}
/**
Send or edit the played summary in place of the nowPlayingMessage.
Parameters:
-
- channel: text channel object
-
- player: poru player object
-
- finishedTrack: optional track object that finished
-
- opts: { force: boolean } -> if force=true, ignore player._suppressPlayed
Behavior:
-
- If the player has a nowPlayingMessage and it's editable -> edit it to the PLAYED summary (preferred).
-
- Else if deletable -> delete it then send a new summary.
-
- Else send a new summary.
*/
async function placePlayedSummary(channel, player, finishedTrack, opts = {}) {
if (!channel || !channel.send) return;
try {
// respect suppression flag unless forced
if (player && player._suppressPlayed && !opts.force) {
log.debug('placePlayedSummary: suppressed by player._suppressPlayed flag, skipping');
return;
}
const track = finishedTrack || player?._lastPlayedTrack || player?.currentTrack || {};
const container = buildPlayedContainerNice(track);
// Try to edit existing nowPlayingMessage in-place
const np = player?.nowPlayingMessage;
if (np && np.editable) {
try {
await np.edit({ components: [container] });
player.nowPlayingMessage = null;
return;
} catch (e) {
log.debug('edit nowPlayingMessage -> played failed, will try delete/send', e?.message || e);
}
}
// If edit failed or not possible -> delete message if deletable, then send summary
if (np && np.deletable) {
try { await np.delete().catch(() => {}); } catch (e) { /* ignore */ }
}
// send fresh played summary
await channel.send({ components: [container], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2 }).catch(e => log.debug('send played summary failed', e?.message || e));
} catch (e) {
log.warn('placePlayedSummary failed', e?.message || e);
} finally {
if (player) player.nowPlayingMessage = null;
}
}
/**
Main end-of-track check:
-
- wait and retry (backoff) to avoid racing next track
-
- if no queue & player not playing -> replace now-playing with PLAYED summary
Important: This function will skip sending the summary if player._suppressPlayed === true.
If you call your
,stopprefix command and want to send the PLAYED summary from that command,set player._suppressPlayed = true BEFORE destroying the player (to prevent events double-sending),
then call this module's exported _internal_placePlayedSummary(...) manually from the command
(or call with { force: true }).
*/
async function handleMaybeSendPlayedSummary(client, player, finishedTrack) {
try {
// initial tiny delay to let poru settle
await new Promise(r => setTimeout(r, 700));
if (!player || !player.guildId) {
log.debug('handleMaybe: no player/guild');
return;
}
// if suppression flag present, respect it (no automatic event-driven PLAYED message)
if (player._suppressPlayed) {
log.debug('handleMaybe: player._suppressPlayed set -> skipping automatic summary');
// clear suppression now to avoid permanent suppression on later natural ends
player._suppressPlayed = false;
return;
}
const channel = client.channels.cache.get(player.textChannel);
if (!channel) {
log.debug('handleMaybe: text channel missing', player.textChannel);
return;
}
const maxAttempts = 4;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const hasQueue = queueHasItems(player);
const currentTrack = player.currentTrack || null;
const playingFlag = Boolean(player.isPlaying) || Boolean(player.playing) || Boolean(player._playing) || false;
log.debug('handleMaybe (attempt)', { attempt, hasQueue, currentTrackExists: !!currentTrack, playingFlag });
// If queue exists, bail — next track should play
if (hasQueue) {
log.debug('handleMaybe: queue detected, skipping summary');
return;
}
// If no currentTrack -> safe to send summary
if (!currentTrack) {
log.debug('handleMaybe: currentTrack cleared & no queue -> sending summary');
if (player.buttonCollector) { try { player.buttonCollector.stop('ended'); } catch (e) {} player.buttonCollector = null; }
await placePlayedSummary(channel, player, finishedTrack);
return;
}
// currentTrack exists. If it is the same as finishedTrack AND player not playing, treat it as finished.
if (finishedTrack && isSameTrack(currentTrack, finishedTrack) && !playingFlag) {
// Extra check: if position suggests finished, accept it too.
const appearsFinished = trackAppearsFinished(player, finishedTrack);
if (appearsFinished || !playingFlag) {
log.debug('handleMaybe: currentTrack equals finishedTrack & player not playing -> treating as finished');
if (player.buttonCollector) { try { player.buttonCollector.stop('ended'); } catch (e) {} player.buttonCollector = null; }
await placePlayedSummary(channel, player, finishedTrack);
return;
}
}
// If currentTrack exists but is a different track -> next started, bail
if (currentTrack && (!finishedTrack || !isSameTrack(currentTrack, finishedTrack))) {
log.debug('handleMaybe: currentTrack present and differs -> assumed next track started, skipping summary');
return;
}
// otherwise state is flaky (maybe playingFlag true but currentTrack same). Backoff then retry.
const backoffMs = 300 + attempt * 300;
await new Promise(r => setTimeout(r, backoffMs));
}
// final conservative check
const finalHasQueue = queueHasItems(player);
const finalCurrentTrack = player.currentTrack || null;
if (!finalCurrentTrack && !finalHasQueue) {
log.debug('handleMaybe: final check OK -> sending summary');
if (player.buttonCollector) { try { player.buttonCollector.stop('ended'); } catch (e) {} player.buttonCollector = null; }
await placePlayedSummary(channel, player, finishedTrack);
return;
}
log.debug('handleMaybe: final check failed - no summary sent', { finalHasQueue, finalCurrentTrackExists: !!finalCurrentTrack });
} catch (err) {
log.error('handleMaybeSendPlayedSummary error', err?.message || err);
}
}
function setupMusicEvents(client) {
client.poru.on('trackStart', async (player, track) => {
const channel = client.channels.cache.get(player.textChannel);
if (!channel) return;
player._lastPlayedTrack = track;
if (!player._autoplayHistory) player._autoplayHistory = new Set();
const identifier = track.info?.identifier || track.identifier;
if (identifier) player._autoplayHistory.add(identifier);
// cleanup previous message/collectors (we remove old nowPlaying on new track)
try { if (player.nowPlayingMessage && player.nowPlayingMessage.deletable) await player.nowPlayingMessage.delete().catch(() => {}); } catch {}
player.nowPlayingMessage = null;
if (player.buttonCollector) { try { player.buttonCollector.stop('newTrack'); } catch {} player.buttonCollector = null; }
// create music card attachment if configured
let musicCardAttachment = null;
if (config.MUSIC.ARTWORK_STYLE === 'MusicCard') {
try {
const guild = channel.guild;
const guildIcon = guild?.iconURL ? guild.iconURL({ extension: 'png', size: 128 }) : null;
const imageBuffer = await musicCard.generateNowPlayingCard({ track, position: player.position || 0, isLiked: false, guildName: guild?.name, guildIcon, player });
if (imageBuffer) musicCardAttachment = new AttachmentBuilder(imageBuffer, { name: 'nowplaying.png' });
} catch (e) { log.warn('MusicCard generation failed', e?.message || e); }
}
try {
const container = buildNowPlayingContainer(track, player, !!musicCardAttachment);
const messageOptions = { components: [container], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2 };
if (musicCardAttachment) messageOptions.files = [musicCardAttachment];
const message = await channel.send(messageOptions);
player.nowPlayingMessage = message;
const COLLECTOR_TIMEOUT_MS = Number(config.MUSIC_COMPONENT_COLLECTOR_MS || 10 * 60 * 1000);
const filter = (i) => i.isButton() && i.message.id === message.id && i.guildId === player.guildId && i.customId.startsWith('music_');
const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, filter, time: COLLECTOR_TIMEOUT_MS });
player.buttonCollector = collector;
collector.on('collect', async (interaction) => {
if (!interaction.member?.voice?.channelId || interaction.member.voice.channelId !== player.voiceChannel) {
return interaction.reply({ components: [textContainer('You must be in the same voice channel as the bot to use playback controls.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
}
try {
switch (interaction.customId) {
case 'music_pause_resume': {
player.pause(!player.isPaused);
const state = player.isPaused ? 'Music paused.' : 'Music resumed.';
await interaction.reply({ components: [textContainer(state)], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
if (player.nowPlayingMessage && player.nowPlayingMessage.editable) {
const updatedContainer = buildNowPlayingContainer(track, player, !!musicCardAttachment);
await player.nowPlayingMessage.edit({ components: [updatedContainer] }).catch(() => {});
}
break;
}
case 'music_skip': {
if (!player.currentTrack) return interaction.reply({ components: [textContainer('Nothing to skip.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
player.skip();
await interaction.reply({ components: [textContainer('Skipped the current track.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
break;
}
case 'music_stop': {
// when user presses the stop button, clear queue + destroy player as before
player.queue?.clear?.();
// set suppress flag so event handlers don't auto-send again
try { player._suppressPlayed = true; } catch {}
try { player.destroy(); } catch (e) { log.warn('player.destroy() failed', e?.message || e); }
// Replace/delete the nowPlaying message immediately and send played summary (best-effort)
try {
const ch = client.channels.cache.get(player.textChannel);
if (ch) await placePlayedSummary(ch, player, player._lastPlayedTrack).catch(() => {});
} catch (e) { log.debug('placePlayedSummary in button stop failed', e?.message || e); }
await interaction.reply({ components: [textContainer('Stopped playback and cleared the queue.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
break;
}
case 'music_shuffle': {
if (!player.queue || player.queue.length === 0) return interaction.reply({ components: [textContainer('The queue is empty.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
player.queue.shuffle();
await interaction.reply({ components: [textContainer('Queue shuffled.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
break;
}
case 'music_favorite_add': {
const cmd = client.commands.get('favoriteadd');
if (cmd) await cmd.execute(interaction);
else await interaction.reply({ components: [textContainer('Favorite command not available.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2, ephemeral: true });
break;
}
case 'music_lyrics': {
// If you already have a lyrics command for interactions, call it here.
// We'll attempt to call the slash-style 'lyrics' command if registered.
const interactionExec = async () => {
try {
await interaction.deferReply({ ephemeral: true }).catch(() => {});
const cmd = client.commands.get('lyrics') || client.commands.get('lyric');
if (cmd && typeof cmd.execute === 'function') {
// many slash handlers expect an Interaction; we pass the interaction directly
await cmd.execute(interaction).catch(() => {
// fallback reply if command failed
interaction.editReply({ components: [textContainer('Lyrics: command failed to run.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2 }).catch(() => {});
});
} else {
await interaction.editReply({ components: [textContainer('Lyrics handler not available.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2 }).catch(() => {});
}
} catch (err) {
log.debug('lyrics button handler error', err?.message || err);
try { await interaction.editReply({ components: [textContainer('Error fetching lyrics.')], flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2 }); } catch {}
}
};
await interactionExec();
break;
}
default:
break;
}
} catch (e) {
log.error('collector interaction error', e?.message || e);
}
});
} catch (e) {
log.error('trackStart send error', e?.message || e);
}
});
// Primary: poru 'trackEnd' -> run summary check
client.poru.on('trackEnd', async (player, track) => {
log.debug('poru.trackEnd received', { guildId: player?.guildId });
await handleMaybeSendPlayedSummary(client, player, track);
});
// Fallbacks: catch other possible finish events
const fallbackEvents = ['trackFinish', 'playerStop', 'playerEnd', 'finish', 'trackException', 'trackStuck'];
for (const ev of fallbackEvents) {
client.poru.on(ev, async (player, track) => {
log.debug(`poru.${ev} received`, { guildId: player?.guildId });
await handleMaybeSendPlayedSummary(client, player, track);
});
}
// queueEnd: if autoplay disabled, run the summary check
client.poru.on('queueEnd', async (player) => {
try {
if (!player.autoplayEnabled) {
log.debug('poru.queueEnd received and autoplay disabled; running summary check');
await handleMaybeSendPlayedSummary(client, player, player._lastPlayedTrack);
return;
}
log.debug('poru.queueEnd received (autoplay enabled) - skipping summary');
} catch (e) {
log.error('queueEnd handler error', e?.message || e);
}
});
client.poru.on('playerDestroy', (player) => {
if (player.buttonCollector) { try { player.buttonCollector.stop(); } catch (e) {} player.buttonCollector = null; }
if (player._autoplayHistory) { try { player._autoplayHistory.clear(); } catch (e) {} }
// last-chance summary attempt: send played summary for last played track after tiny delay
try {
const chId = player?.textChannel;
if (chId) {
setTimeout(async () => {
try {
const ch = client.channels.cache.get(chId);
if (ch) await placePlayedSummary(ch, player, player._lastPlayedTrack).catch(() => {});
} catch (e) { /* ignore */ }
}, 250);
}
} catch (e) { /* ignore */ }
});
}
module.exports = { setupMusicEvents, _internal_placePlayedSummary: placePlayedSummary };