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 "" } }