From dff524cb6e1422c5f4f992cd7d933e196bcc2271 Mon Sep 17 00:00:00 2001 From: topi314 Date: Tue, 14 May 2024 23:37:25 +0200 Subject: [PATCH] add lavalyrics live lyrics support --- commands/lyrics.go | 52 ++++++++++++++++++++++++ commands/music.go | 14 +++++++ commands/play.go | 6 ++- handlers/lavalink.go | 73 ---------------------------------- handlers/lavalyrics.go | 82 ++++++++++++++++++++++++++++++++++++++ handlers/sponsorblock.go | 85 ++++++++++++++++++++++++++++++++++++++++ lavalinkbot/queue.go | 29 ++++++++++++-- main.go | 14 ++++++- 8 files changed, 276 insertions(+), 79 deletions(-) create mode 100644 handlers/lavalyrics.go create mode 100644 handlers/sponsorblock.go diff --git a/commands/lyrics.go b/commands/lyrics.go index bcc4ff4..4337389 100644 --- a/commands/lyrics.go +++ b/commands/lyrics.go @@ -88,3 +88,55 @@ func (c *Commands) Lyrics(data discord.SlashCommandInteractionData, e *handler.C }) return err } + +func (c *Commands) LiveLyricsSubscribe(_ discord.SlashCommandInteractionData, e *handler.CommandEvent) error { + player := c.Lavalink.ExistingPlayer(*e.GuildID()) + if player == nil { + return e.CreateMessage(discord.MessageCreate{ + Content: "No player found", + Flags: discord.MessageFlagEphemeral, + }) + } + + if err := e.DeferCreateMessage(false); err != nil { + return err + } + + if err := lavalyrics.SubscribeLyrics(e.Ctx, player.Node().Rest(), player.Node().SessionID(), *e.GuildID()); err != nil { + _, err = e.UpdateInteractionResponse(discord.MessageUpdate{ + Content: json.Ptr(fmt.Sprintf("failed to get subscribe to live lyrics: %s", err)), + }) + return err + } + + _, err := e.UpdateInteractionResponse(discord.MessageUpdate{ + Content: json.Ptr(fmt.Sprintf("subscribed to live lyrics")), + }) + return err +} + +func (c *Commands) LiveLyricsUnsubscribe(_ discord.SlashCommandInteractionData, e *handler.CommandEvent) error { + player := c.Lavalink.ExistingPlayer(*e.GuildID()) + if player == nil { + return e.CreateMessage(discord.MessageCreate{ + Content: "No player found", + Flags: discord.MessageFlagEphemeral, + }) + } + + if err := e.DeferCreateMessage(false); err != nil { + return err + } + + if err := lavalyrics.UnsubscribeLyrics(e.Ctx, player.Node().Rest(), player.Node().SessionID(), *e.GuildID()); err != nil { + _, err = e.UpdateInteractionResponse(discord.MessageUpdate{ + Content: json.Ptr(fmt.Sprintf("failed to unsubscribe from live lyrics: %s", err)), + }) + return err + } + + _, err := e.UpdateInteractionResponse(discord.MessageUpdate{ + Content: json.Ptr(fmt.Sprintf("unsubscribed from live lyrics")), + }) + return err +} diff --git a/commands/music.go b/commands/music.go index 97653d2..339ee47 100644 --- a/commands/music.go +++ b/commands/music.go @@ -463,6 +463,20 @@ var music = discord.SlashCommandCreate{ }, }, }, + discord.ApplicationCommandOptionSubCommandGroup{ + Name: "live-lyrics", + Description: "Subscribes or unsubscribes to live lyrics", + Options: []discord.ApplicationCommandOptionSubCommand{ + { + Name: "subscribe", + Description: "Subscribes to live lyrics", + }, + { + Name: "unsubscribe", + Description: "Unsubscribes from live lyrics", + }, + }, + }, }, } diff --git a/commands/play.go b/commands/play.go index 5ec0fbd..3bc1f74 100644 --- a/commands/play.go +++ b/commands/play.go @@ -259,6 +259,8 @@ func (c *Commands) Play(data discord.SlashCommandInteractionData, e *handler.Com track, tracks = tracks[0], tracks[1:] } + c.MusicQueue.Add(*e.GuildID(), e.Channel().ID(), tracks...) + playCtx, playCancel := context.WithTimeout(e.Ctx, 10*time.Second) defer playCancel() if err = player.Update(playCtx, lavalink.WithTrack(track)); err != nil { @@ -267,9 +269,9 @@ func (c *Commands) Play(data discord.SlashCommandInteractionData, e *handler.Com }) return err } - } - if len(tracks) > 0 { + } else { c.MusicQueue.Add(*e.GuildID(), e.Channel().ID(), tracks...) } + return nil } diff --git a/handlers/lavalink.go b/handlers/lavalink.go index c3d7c18..1314273 100644 --- a/handlers/lavalink.go +++ b/handlers/lavalink.go @@ -10,7 +10,6 @@ import ( "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgolink/v3/disgolink" "github.com/disgoorg/disgolink/v3/lavalink" - "github.com/disgoorg/sponsorblock-plugin" "github.com/lavalink-devs/lavalink-bot/commands" "github.com/lavalink-devs/lavalink-bot/internal/res" "github.com/topi314/tint" @@ -139,75 +138,3 @@ func (h *Handlers) OnUnknownEvent(p disgolink.Player, event lavalink.UnknownEven func (h *Handlers) OnUnknownMessage(p disgolink.Player, event lavalink.UnknownMessage) { slog.Info("unknown message", slog.String("op", string(event.Op())), slog.String("data", string(event.Data))) } - -func (h *Handlers) OnSegmentsLoaded(p disgolink.Player, event sponsorblock.SegmentsLoadedEvent) { - channelID := h.MusicQueue.ChannelID(p.GuildID()) - if channelID == 0 { - return - } - - content := "Segments loaded:\n" - for i, segment := range event.Segments { - line := fmt.Sprintf("%d. %s: %s - %s\n", i+1, segment.Category, res.FormatDuration(segment.Start), res.FormatDuration(segment.End)) - if len(content)+len(line) > 2000 { - content += "..." - break - } - content += line - } - if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ - Content: content, - AllowedMentions: &discord.AllowedMentions{}, - }); err != nil { - slog.Error("failed to send message", tint.Err(err)) - } -} - -func (h *Handlers) OndSegmentSkipped(p disgolink.Player, event sponsorblock.SegmentSkippedEvent) { - channelID := h.MusicQueue.ChannelID(p.GuildID()) - if channelID == 0 { - return - } - if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ - Content: fmt.Sprintf("Segment skipped: %s: %s - %s", event.Segment.Category, res.FormatDuration(event.Segment.Start), res.FormatDuration(event.Segment.End)), - AllowedMentions: &discord.AllowedMentions{}, - }); err != nil { - slog.Error("failed to send message", tint.Err(err)) - } -} - -func (h *Handlers) OnChaptersLoaded(p disgolink.Player, event sponsorblock.ChaptersLoadedEvent) { - channelID := h.MusicQueue.ChannelID(p.GuildID()) - if channelID == 0 { - return - } - - content := "Chapters loaded:\n" - for i, chapter := range event.Chapters { - line := fmt.Sprintf("%d. %s: %s - %s\n", i+1, chapter.Name, res.FormatDuration(chapter.Start), res.FormatDuration(chapter.End)) - if len(content)+len(line) > 2000 { - content += "..." - break - } - content += line - } - if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ - Content: content, - AllowedMentions: &discord.AllowedMentions{}, - }); err != nil { - slog.Error("failed to send message", tint.Err(err)) - } -} - -func (h *Handlers) OnChapterStarted(p disgolink.Player, event sponsorblock.ChapterStartedEvent) { - channelID := h.MusicQueue.ChannelID(p.GuildID()) - if channelID == 0 { - return - } - if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ - Content: fmt.Sprintf("Chapter started: %s: %s - %s", event.Chapter.Name, res.FormatDuration(event.Chapter.Start), res.FormatDuration(event.Chapter.End)), - AllowedMentions: &discord.AllowedMentions{}, - }); err != nil { - slog.Error("failed to send message", tint.Err(err)) - } -} diff --git a/handlers/lavalyrics.go b/handlers/lavalyrics.go new file mode 100644 index 0000000..1e88d3a --- /dev/null +++ b/handlers/lavalyrics.go @@ -0,0 +1,82 @@ +package handlers + +import ( + "fmt" + "log/slog" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgolink/v3/disgolink" + "github.com/disgoorg/json" + "github.com/disgoorg/lavalyrics-plugin" + "github.com/topi314/tint" +) + +func (h *Handlers) OnLyricsFound(p disgolink.Player, event lavalyrics.LyricsFoundEvent) { + channelID := h.MusicQueue.ChannelID(p.GuildID()) + if channelID == 0 { + return + } + + lyricsMessageID := h.MusicQueue.LyricsMessageID(p.GuildID()) + if lyricsMessageID == 0 { + msg, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ + Content: "Lyrics found", + }) + if err != nil { + slog.Error("failed to send message", tint.Err(err)) + return + } + h.MusicQueue.SetLyricsMessageID(p.GuildID(), msg.ID) + return + } + + if _, err := h.Client.Rest().UpdateMessage(channelID, lyricsMessageID, discord.MessageUpdate{ + Content: json.Ptr("Lyrics found"), + }); err != nil { + slog.Error("failed to update message", tint.Err(err)) + } +} + +func (h *Handlers) OnLyricsNotFound(p disgolink.Player, event lavalyrics.LyricsNotFoundEvent) { + channelID := h.MusicQueue.ChannelID(p.GuildID()) + if channelID == 0 { + return + } + + lyricsMessageID := h.MusicQueue.LyricsMessageID(p.GuildID()) + if lyricsMessageID == 0 { + msg, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ + Content: "Lyrics not found", + }) + if err != nil { + slog.Error("failed to send message", tint.Err(err)) + return + } + h.MusicQueue.SetLyricsMessageID(p.GuildID(), msg.ID) + return + } + + if _, err := h.Client.Rest().UpdateMessage(channelID, lyricsMessageID, discord.MessageUpdate{ + Content: json.Ptr("Lyrics not found"), + }); err != nil { + slog.Error("failed to update message", tint.Err(err)) + } +} + +func (h *Handlers) OnLyricsLine(p disgolink.Player, event lavalyrics.LyricsLineEvent) { + channelID := h.MusicQueue.ChannelID(p.GuildID()) + if channelID == 0 { + return + } + + lyricsMessageID := h.MusicQueue.LyricsMessageID(p.GuildID()) + if lyricsMessageID == 0 { + return + } + + if _, err := h.Client.Rest().UpdateMessage(channelID, lyricsMessageID, discord.MessageUpdate{ + Content: json.Ptr(fmt.Sprintf("Line(`%s`): %s", event.Line.Timestamp, event.Line.Line)), + }); err != nil { + slog.Error("failed to update message", tint.Err(err)) + } +} diff --git a/handlers/sponsorblock.go b/handlers/sponsorblock.go new file mode 100644 index 0000000..a5122e3 --- /dev/null +++ b/handlers/sponsorblock.go @@ -0,0 +1,85 @@ +package handlers + +import ( + "fmt" + "log/slog" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgolink/v3/disgolink" + "github.com/disgoorg/sponsorblock-plugin" + "github.com/lavalink-devs/lavalink-bot/internal/res" + "github.com/topi314/tint" +) + +func (h *Handlers) OnSegmentsLoaded(p disgolink.Player, event sponsorblock.SegmentsLoadedEvent) { + channelID := h.MusicQueue.ChannelID(p.GuildID()) + if channelID == 0 { + return + } + + content := "Segments loaded:\n" + for i, segment := range event.Segments { + line := fmt.Sprintf("%d. %s: %s - %s\n", i+1, segment.Category, res.FormatDuration(segment.Start), res.FormatDuration(segment.End)) + if len(content)+len(line) > 2000 { + content += "..." + break + } + content += line + } + if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ + Content: content, + AllowedMentions: &discord.AllowedMentions{}, + }); err != nil { + slog.Error("failed to send message", tint.Err(err)) + } +} + +func (h *Handlers) OndSegmentSkipped(p disgolink.Player, event sponsorblock.SegmentSkippedEvent) { + channelID := h.MusicQueue.ChannelID(p.GuildID()) + if channelID == 0 { + return + } + if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ + Content: fmt.Sprintf("Segment skipped: %s: %s - %s", event.Segment.Category, res.FormatDuration(event.Segment.Start), res.FormatDuration(event.Segment.End)), + AllowedMentions: &discord.AllowedMentions{}, + }); err != nil { + slog.Error("failed to send message", tint.Err(err)) + } +} + +func (h *Handlers) OnChaptersLoaded(p disgolink.Player, event sponsorblock.ChaptersLoadedEvent) { + channelID := h.MusicQueue.ChannelID(p.GuildID()) + if channelID == 0 { + return + } + + content := "Chapters loaded:\n" + for i, chapter := range event.Chapters { + line := fmt.Sprintf("%d. %s: %s - %s\n", i+1, chapter.Name, res.FormatDuration(chapter.Start), res.FormatDuration(chapter.End)) + if len(content)+len(line) > 2000 { + content += "..." + break + } + content += line + } + if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ + Content: content, + AllowedMentions: &discord.AllowedMentions{}, + }); err != nil { + slog.Error("failed to send message", tint.Err(err)) + } +} + +func (h *Handlers) OnChapterStarted(p disgolink.Player, event sponsorblock.ChapterStartedEvent) { + channelID := h.MusicQueue.ChannelID(p.GuildID()) + if channelID == 0 { + return + } + if _, err := h.Client.Rest().CreateMessage(channelID, discord.MessageCreate{ + Content: fmt.Sprintf("Chapter started: %s: %s - %s", event.Chapter.Name, res.FormatDuration(event.Chapter.Start), res.FormatDuration(event.Chapter.End)), + AllowedMentions: &discord.AllowedMentions{}, + }); err != nil { + slog.Error("failed to send message", tint.Err(err)) + } +} + diff --git a/lavalinkbot/queue.go b/lavalinkbot/queue.go index 9a8bbe4..263d8ab 100644 --- a/lavalinkbot/queue.go +++ b/lavalinkbot/queue.go @@ -28,9 +28,10 @@ type PlayerManager struct { } type queue struct { - tracks []lavalink.Track - mode RepeatMode - channelID snowflake.ID + tracks []lavalink.Track + mode RepeatMode + channelID snowflake.ID + lyricsMessageID snowflake.ID } func (q *PlayerManager) Get(guildID snowflake.ID) (RepeatMode, []lavalink.Track) { @@ -62,6 +63,28 @@ func (q *PlayerManager) ChannelID(guildID snowflake.ID) snowflake.ID { return qu.channelID } +func (q *PlayerManager) SetLyricsMessageID(guildID snowflake.ID, messageID snowflake.ID) { + q.mu.Lock() + defer q.mu.Unlock() + + qu, ok := q.queues[guildID] + if !ok { + return + } + qu.lyricsMessageID = messageID +} + +func (q *PlayerManager) LyricsMessageID(guildID snowflake.ID) snowflake.ID { + q.mu.Lock() + defer q.mu.Unlock() + + qu, ok := q.queues[guildID] + if !ok { + return 0 + } + return qu.lyricsMessageID +} + func (q *PlayerManager) Add(guildID snowflake.ID, channelID snowflake.ID, tracks ...lavalink.Track) { q.mu.Lock() defer q.mu.Unlock() diff --git a/main.go b/main.go index 61d4d5f..5737327 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/disgoorg/disgo/handler" "github.com/disgoorg/disgo/handler/middleware" "github.com/disgoorg/disgolink/v3/disgolink" + "github.com/disgoorg/lavalyrics-plugin" "github.com/disgoorg/sponsorblock-plugin" "github.com/google/go-github/v52/github" "github.com/lavalink-devs/lavalink-bot/commands" @@ -62,6 +63,10 @@ func main() { r.SlashCommand("/tts", cmds.TTS) r.Autocomplete("/play", cmds.PlayAutocomplete) r.SlashCommand("/lyrics", cmds.Lyrics) + r.Route("/live-lyrics", func(r handler.Router) { + r.SlashCommand("/subscribe", cmds.LiveLyricsSubscribe) + r.SlashCommand("/unsubscribe", cmds.LiveLyricsUnsubscribe) + }) r.Group(func(r handler.Router) { r.Use(cmds.RequirePlayer) @@ -112,8 +117,12 @@ func main() { } sponsorblockPlugin := sponsorblock.New() + lavalyricsPlugin := lavalyrics.New() if b.Lavalink = disgolink.New(b.Client.ApplicationID(), - disgolink.WithPlugins(sponsorblockPlugin), + disgolink.WithPlugins( + sponsorblockPlugin, + lavalyricsPlugin, + ), disgolink.WithListenerFunc(hdlr.OnTrackStart), disgolink.WithListenerFunc(hdlr.OnTrackEnd), disgolink.WithListenerFunc(hdlr.OnTrackException), @@ -124,6 +133,9 @@ func main() { disgolink.WithListenerFunc(hdlr.OndSegmentSkipped), disgolink.WithListenerFunc(hdlr.OnChaptersLoaded), disgolink.WithListenerFunc(hdlr.OnChapterStarted), + disgolink.WithListenerFunc(hdlr.OnLyricsFound), + disgolink.WithListenerFunc(hdlr.OnLyricsNotFound), + disgolink.WithListenerFunc(hdlr.OnLyricsLine), ); err != nil { slog.Error("failed to create disgolink client", tint.Err(err)) os.Exit(-1)