Files
amayo/src/core/utils/musicMessages.ts

269 lines
9.0 KiB
TypeScript
Raw Normal View History

2025-11-24 16:16:01 -06:00
import { DisplayComponentV2Builder } from "../lib/displayComponents/builders/v2Builder";
import { LikeService } from "../services/LikeService";
import { PlaylistService } from "../services/PlaylistService";
2025-12-01 18:59:48 +00:00
import { MusicHistoryService } from "../services/MusicHistoryService";
import { isAutoplayEnabledForGuild } from "../../commands/messages/music/autoplay";
import { isShuffleEnabled, getRepeatMode, getRepeatModeLabel, getRepeatModeEmoji } from "../services/MusicStateService";
2025-11-24 16:16:01 -06:00
interface TrackInfo {
title: string;
author: string;
duration: number;
trackId: string;
thumbnail?: string;
}
/**
* Creates ComponentsV2 messages for "Now Playing" with interactive buttons and playlist SelectMenu
* Returns array of 2 containers: [info container, buttons+menu container]
*/
export async function createNowPlayingMessage(
trackInfo: TrackInfo,
queueLength: number,
userId: string,
2025-12-01 18:59:48 +00:00
guildId: string,
client: any // Add client parameter
2025-11-24 16:16:01 -06:00
) {
// Create consistent trackId from title and author (same format as music_like.ts)
const consistentTrackId = Buffer.from(
`${trackInfo.title}:${trackInfo.author}`
)
.toString("base64")
.substring(0, 50);
const isLiked = await LikeService.isTrackLiked(
userId,
guildId,
consistentTrackId
);
const durationText = formatDuration(trackInfo.duration);
// Container 1: Track info with thumbnail
const infoContainer = new DisplayComponentV2Builder().setAccentColor(
0x5865f2
); // Discord blurple
if (trackInfo.thumbnail) {
// Use section with accessory for thumbnail
infoContainer.addSection(
[
{
type: 10,
2025-12-01 18:59:48 +00:00
content: `**Añadido a la cola**\n${trackInfo.title}\n${trackInfo.author} • Duración: ${durationText}`,
2025-11-24 16:16:01 -06:00
},
],
{ type: 11, media: { url: trackInfo.thumbnail } }
);
} else {
infoContainer.addText(
2025-12-01 18:59:48 +00:00
`**Añadido a la cola**\n${trackInfo.title}\n${trackInfo.author} • Duración: ${durationText}`
2025-11-24 16:16:01 -06:00
);
}
// Container 2: "Now playing" text + buttons + SelectMenu
const buttonsContainer = new DisplayComponentV2Builder()
.setAccentColor(0x608beb) // Slightly different blue
.addText("**Ahora reproduciendo:**")
.addText(`${trackInfo.title} - ${trackInfo.author}`);
// Create buttons
const likeButton = DisplayComponentV2Builder.createButton(
isLiked ? "music_unlike" : "music_like",
2025-12-01 18:59:48 +00:00
isLiked ? "Te gusta" : "Like",
isLiked ? 1 : 2, // 2 = secondary (gray) when liked, 1 = primary (blue) when not liked
false,
2025-11-24 16:16:01 -06:00
{ name: "❤️" }
);
2025-12-01 18:59:48 +00:00
// Check shuffle state
const shuffleActive = isShuffleEnabled(guildId);
const shuffleButton = DisplayComponentV2Builder.createButton(
"music_shuffle",
"Shuffle",
shuffleActive ? 3 : 1, // 3 = success (green) when active, 1 = primary (blue) when inactive
false,
{ name: "🔀" }
);
// Check repeat mode
const repeatModeValue = getRepeatMode(guildId);
let repeatStyle = 1; // Default: blue
if (repeatModeValue === 'one') repeatStyle = 3; // Green for repeat one
if (repeatModeValue === 'all') repeatStyle = 4; // Red for repeat all
2025-11-24 16:16:01 -06:00
const repeatButton = DisplayComponentV2Builder.createButton(
"music_repeat",
2025-12-01 18:59:48 +00:00
getRepeatModeLabel(repeatModeValue),
repeatStyle,
false,
{ name: getRepeatModeEmoji(repeatModeValue) }
2025-11-24 16:16:01 -06:00
);
2025-12-01 18:59:48 +00:00
// Check if autoplay is enabled for this guild
const isAutoplayEnabled = isAutoplayEnabledForGuild(guildId);
2025-11-24 16:16:01 -06:00
const autoplayButton = DisplayComponentV2Builder.createButton(
"music_autoplay_toggle",
2025-12-01 18:59:48 +00:00
"Autoplay",
isAutoplayEnabled ? 3 : 1, // 3 = success (green) when enabled, 1 = primary (blue) when disabled
false,
2025-11-24 16:16:01 -06:00
{ name: "⏩" }
);
2025-12-01 18:59:48 +00:00
// Add Skip button
const skipButton = DisplayComponentV2Builder.createButton(
"music_skip",
"Skip",
2, // 2 = secondary (gray)
false,
{ name: "⏭️" }
);
// Add ActionRow with buttons (max 5 buttons per row)
2025-11-24 16:16:01 -06:00
buttonsContainer.addActionRow([
likeButton,
shuffleButton,
2025-12-01 18:59:48 +00:00
repeatButton,
2025-11-24 16:16:01 -06:00
autoplayButton,
2025-12-01 18:59:48 +00:00
skipButton,
2025-11-24 16:16:01 -06:00
]);
// Get user's playlists for SelectMenu
const playlists = await PlaylistService.getUserPlaylists(userId, guildId);
// Create SelectMenu options
const selectOptions = [
{
label: "Crear playlist nueva",
value: "create_new",
emoji: { name: "🔹" },
},
];
// Add existing playlists
playlists.forEach((playlist) => {
selectOptions.push({
label: playlist.name,
value: playlist.id,
emoji: { name: playlist.isDefault ? "❤️" : "📝" },
});
});
// Create SelectMenu
// Use only first 50 chars of trackId hash to stay under 100 char limit
const trackIdHash = trackInfo.trackId.substring(0, 50);
const selectMenu = DisplayComponentV2Builder.createSelectMenu(
`music_save_select:${trackIdHash}`,
"Guardar en playlist",
selectOptions
);
// Add SelectMenu in its own ActionRow
buttonsContainer.addActionRow([selectMenu]);
2025-12-01 18:59:48 +00:00
// Get user's listening history for second SelectMenu
try {
const history = await MusicHistoryService.getRecentHistory(userId, 50);
// Remove duplicates by trackId (keep most recent)
const uniqueTracksMap = new Map();
for (const track of history) {
// Skip tracks without title or author
if (!track.title || !track.author) continue;
const trackId = track.trackId || `${track.title}:${track.author}`;
if (!uniqueTracksMap.has(trackId)) {
uniqueTracksMap.set(trackId, track);
}
}
// Convert to array and take first 25 (Discord limit)
const uniqueTracks = Array.from(uniqueTracksMap.values()).slice(0, 25);
// Only create history menu if there are valid tracks
if (uniqueTracks.length > 0) {
// Create history select menu options with validation
const historyOptions = uniqueTracks
.filter(track => track.title && track.author) // Ensure valid data
.map(track => ({
label: track.title.substring(0, 100), // Discord limit
value: (track.trackId || `${track.title}:${track.author}`).substring(0, 100), // Discord limit
description: track.author.substring(0, 100),
emoji: { name: '🎵' }
}));
// Only add if we have valid options
if (historyOptions.length > 0) {
const historyMenu = DisplayComponentV2Builder.createSelectMenu(
'music_history_select',
'Reproducir del historial',
historyOptions
);
// Add history SelectMenu in its own ActionRow
buttonsContainer.addActionRow([historyMenu]);
}
}
} catch (error) {
console.error("Error loading history for select menu:", error);
// Continue without history menu if there's an error
}
// Add "Similar Songs" select menu (like YouTube's related videos)
try {
const { getSimilarTracks } = await import("../../commands/messages/music/autoplay.js");
const similarTracks = await getSimilarTracks(trackInfo, guildId, client);
if (similarTracks && similarTracks.length > 0) {
// Store similar tracks in Redis for the select menu handler
const { redis } = await import("../../core/database/redis.js");
const redisKey = `music:similar:${guildId}`;
await redis.set(redisKey, JSON.stringify(similarTracks), { EX: 3600 }); // Expire after 1 hour
console.log(`[musicMessages] Stored ${similarTracks.length} similar tracks in Redis for guild ${guildId}`);
// Create similar songs select menu options
const similarOptions = similarTracks
.slice(0, 15) // Limit to 15 similar tracks
.map(track => ({
label: track.info.title.substring(0, 100), // Discord limit
value: track.encoded.substring(0, 100), // Discord limit
description: track.info.author.substring(0, 100),
emoji: { name: '🔗' } // Link emoji for "related"
}));
if (similarOptions.length > 0) {
const similarMenu = DisplayComponentV2Builder.createSelectMenu(
'music_similar',
'🎵 Canciones similares',
similarOptions
);
// Add similar SelectMenu in its own ActionRow
buttonsContainer.addActionRow([similarMenu]);
}
}
} catch (error) {
console.error("Error loading similar tracks for select menu:", error);
// Continue without similar menu if there's an error
}
2025-11-24 16:16:01 -06:00
// Return both containers
return [infoContainer.toJSON(), buttonsContainer.toJSON()];
}
/**
* Format duration from milliseconds to MM:SS or HH:MM:SS
*/
function formatDuration(ms: number): string {
const seconds = Math.floor((ms / 1000) % 60);
const minutes = Math.floor((ms / (1000 * 60)) % 60);
const hours = Math.floor(ms / (1000 * 60 * 60));
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, "0")}:${String(
seconds
).padStart(2, "0")}`;
}
return `${minutes}:${String(seconds).padStart(2, "0")}`;
}