Discord js v14.22 i get error when i click to button
02:24 14 Nov 2025
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 ,stop command suppress automatic event-driven summaries

  • to 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 ,stop prefix 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 };

discord.js