- 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.
180 lines
4.7 KiB
Go
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 ""
|
|
}
|
|
}
|
|
|