Files
FernandoJVideira d7edfea45e feat: integrate music commands with Lavalink support
- Added new music commands: play, queue, and volume.
- Implemented music management using disgolink for Lavalink integration.
- Updated bot initialization to include Lavalink host and password.
- Enhanced interaction handling for music commands, requiring DJ role for usage.
- Introduced now playing message with interactive buttons for controlling playback.
- Updated dependencies in go.mod for disgolink and snowflake.
2026-03-18 03:47:42 +00:00

180 lines
4.7 KiB
Go

package music
import (
"fmt"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/disgoorg/disgolink/v3/lavalink"
"github.com/disgoorg/snowflake/v2"
)
func nowPlayingComponents(disabled bool, repeatSong bool, repeatQueue bool, paused bool) []discordgo.MessageComponent {
repeatSongStyle := discordgo.SecondaryButton
if repeatSong {
repeatSongStyle = discordgo.SuccessButton
}
repeatQueueStyle := discordgo.SecondaryButton
if repeatQueue {
repeatQueueStyle = discordgo.SuccessButton
}
return []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.Button{
Style: discordgo.SecondaryButton,
Emoji: &discordgo.ComponentEmoji{Name: map[bool]string{true: "▶️", false: "⏸️"}[paused]},
CustomID: "music:toggle_pause",
Disabled: disabled,
},
discordgo.Button{
Style: discordgo.SecondaryButton,
Emoji: &discordgo.ComponentEmoji{Name: "⏭️"},
CustomID: "music:skip",
Disabled: disabled,
},
discordgo.Button{
Style: repeatSongStyle,
Emoji: &discordgo.ComponentEmoji{Name: "🔂"},
CustomID: "music:repeat_song",
Disabled: disabled,
},
discordgo.Button{
Style: repeatQueueStyle,
Emoji: &discordgo.ComponentEmoji{Name: "🔁"},
CustomID: "music:repeat_queue",
Disabled: disabled,
},
discordgo.Button{
Style: discordgo.DangerButton,
Emoji: &discordgo.ComponentEmoji{Name: "✖️"},
CustomID: "music:stop",
Disabled: disabled,
},
},
},
}
}
func disableNowPlaying(textChannelID, messageID string) {
if manager == nil || textChannelID == "" || messageID == "" {
return
}
disabled := nowPlayingComponents(true, false, false, false)
_, _ = manager.session.ChannelMessageEditComplex(&discordgo.MessageEdit{
Channel: textChannelID,
ID: messageID,
Components: &disabled,
})
}
func postNowPlaying(guildID, textChannelID string, entry TrackEntry) {
if manager == nil || textChannelID == "" {
return
}
manager.mu.Lock()
gp, ok := manager.players[guildID]
var oldMsgID string
if ok {
oldMsgID = gp.NowPlayingMsgID
}
manager.mu.Unlock()
if oldMsgID != "" {
disableNowPlaying(textChannelID, oldMsgID)
}
repeatSong, repeatQueue := repeatFlags(guildID)
paused, volume := pausedAndVolume(guildID)
info := entry.Track.Info
title := info.Title
embed := &discordgo.MessageEmbed{
Author: &discordgo.MessageEmbedAuthor{
Name: info.Author,
IconURL: sourceIconURL(info.SourceName),
},
Title: title,
Color: 0x2B2D31, // matches Discord dark-ish embed accent
Fields: []*discordgo.MessageEmbedField{
{Name: "Duration", Value: formatDuration(info.Length, info.IsStream), Inline: true},
{Name: "Volume", Value: fmt.Sprintf("%d%%", volume), Inline: true},
{Name: "Requested by", Value: entry.RequestedBy, Inline: true},
},
}
if info.URI != nil && *info.URI != "" {
embed.URL = *info.URI
}
if art := artworkURL(info); art != "" {
embed.Thumbnail = &discordgo.MessageEmbedThumbnail{URL: art}
}
msg, err := manager.session.ChannelMessageSendComplex(textChannelID, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{embed},
Components: nowPlayingComponents(false, repeatSong, repeatQueue, paused),
})
if err != nil {
return
}
manager.mu.Lock()
// Avoid calling getOrCreateGuildPlayer while holding manager.mu (it also locks).
gp = manager.players[guildID]
if gp == nil {
gp = &guildPlayer{
Player: manager.client.Player(snowflake.MustParse(guildID)),
Queue: make([]TrackEntry, 0),
}
manager.players[guildID] = gp
}
gp.TextChannelID = textChannelID
gp.NowPlayingMsgID = msg.ID
manager.mu.Unlock()
}
func artworkURL(info lavalink.TrackInfo) string {
if info.ArtworkURL != nil && *info.ArtworkURL != "" {
return *info.ArtworkURL
}
// Lavalink sets Identifier for YouTube videos; use standard thumbnail as fallback.
if strings.EqualFold(info.SourceName, "youtube") && info.Identifier != "" {
return "https://img.youtube.com/vi/" + info.Identifier + "/hqdefault.jpg"
}
return ""
}
func formatDuration(d lavalink.Duration, isStream bool) string {
if isStream {
return "Live"
}
secs := d.Seconds()
if secs < 0 {
secs = 0
}
h := secs / 3600
m := (secs % 3600) / 60
s := secs % 60
if h > 0 {
return fmt.Sprintf("%d:%02d:%02d", h, m, s)
}
return fmt.Sprintf("%d:%02d", m, s)
}
func sourceIconURL(source string) string {
switch strings.ToLower(strings.TrimSpace(source)) {
case "youtube":
// Stable PNG favicon (Discord doesn't render .ico/.svg reliably in embeds)
return "https://www.google.com/s2/favicons?sz=64&domain=youtube.com"
case "soundcloud":
return "https://www.google.com/s2/favicons?sz=64&domain=soundcloud.com"
default:
return ""
}
}