diff --git a/pkg/bot/player.go b/pkg/bot/player.go index 4090ad1..6230964 100644 --- a/pkg/bot/player.go +++ b/pkg/bot/player.go @@ -22,12 +22,15 @@ type Trigger struct { type Song struct { Type string - Title string - URL string - Playable bool + Title string + URL string + Playable bool + ThumbnailURL *string Duration time.Duration StartPosition time.Duration + + RequestedBy *string } func (s *Song) GetHumanName() string { @@ -132,9 +135,11 @@ func (p *GuildPlayer) SendMessage(message string) { } } -func (p *GuildPlayer) AddSong(textChannelID, voiceChannelID *string, s *Song) error { - if err := p.state.AppendSong(s); err != nil { - return fmt.Errorf("while appending song: %w", err) +func (p *GuildPlayer) AddSong(textChannelID, voiceChannelID *string, songs ...*Song) error { + for _, song := range songs { + if err := p.state.AppendSong(song); err != nil { + return fmt.Errorf("while appending song: %w", err) + } } go func() { @@ -347,6 +352,10 @@ func (p *GuildPlayer) playPlaylist(ctx context.Context) error { return fmt.Errorf("while sending audio data: %w", err) } + if err := p.session.EditPlayMessage(textChannel, playMsgID, &PlayMessage{Song: song, Position: song.Duration}); err != nil { + logger.Error("failed to edit message", zap.Error(err)) + } + if err := p.state.SetCurrentSong(nil); err != nil { return fmt.Errorf("while setting current song: %w", err) } diff --git a/pkg/discord/doc.go b/pkg/discord/doc.go new file mode 100644 index 0000000..cedcdb7 --- /dev/null +++ b/pkg/discord/doc.go @@ -0,0 +1,4 @@ +package discord + +// TODO: add links to songs, when showing playlist (generated or YouTube playlist) +// TODO: use functional options to make creating messages more flexible diff --git a/pkg/discord/interactions.go b/pkg/discord/interactions.go index 7236ac1..ed985e1 100644 --- a/pkg/discord/interactions.go +++ b/pkg/discord/interactions.go @@ -9,6 +9,7 @@ import ( "github.com/Trojan295/discord-airplay/pkg/bot" "github.com/Trojan295/discord-airplay/pkg/config" "github.com/Trojan295/discord-airplay/pkg/sources" + "github.com/Trojan295/discord-airplay/pkg/utils" "github.com/bwmarrin/discordgo" "go.uber.org/zap" ) @@ -65,7 +66,7 @@ func (handler *InteractionHandler) WithLogger(l *zap.Logger) *InteractionHandler } func (handler *InteractionHandler) Ready(s *discordgo.Session, event *discordgo.Ready) { - if err := s.UpdateGameStatus(0, "πŸ•ΊπŸ’ƒ /air"); err != nil { + if err := s.UpdateGameStatus(0, fmt.Sprintf("πŸ•ΊπŸ’ƒ /%s", handler.cfg.CommandPrefix)); err != nil { handler.logger.Error("failed to update game status", zap.Error(err)) } } @@ -115,88 +116,77 @@ func (handler *InteractionHandler) PlaySong(s *discordgo.Session, ic *discordgo. input := optionMap["input"].StringValue() - for _, vs := range g.VoiceStates { - if vs.UserID == ic.Member.User.ID { - InteractionRespond(handler.logger, s, ic.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "⏳ Adding song...", - }, + vs := getUsersVoiceState(g, ic.Member.User) + if vs == nil { + InteractionRespondMessage(handler.logger, s, ic.Interaction, MessageUserNotInVoiceChannel) + } + + InteractionRespond(handler.logger, s, ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{GenerateAddingSongEmbed(input, ic.Member)}, + }, + }) + + go func(ic *discordgo.InteractionCreate, vs *discordgo.VoiceState) { + songs, err := handler.songLookuper.LookupSongs(handler.ctx, input) + if err != nil { + logger.Info("failed to lookup song metadata", zap.Error(err), zap.String("input", input)) + FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ + Embeds: []*discordgo.MessageEmbed{GenerateFailedToAddSongEmbed(input, ic.Member)}, }) + return + } - go func() { - songs, err := handler.songLookuper.LookupSongs(handler.ctx, input) - if err != nil { - logger.Info("failed to lookup song metadata", zap.Error(err), zap.String("input", input)) - FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ - Content: "😨 Failed to add song", - }) - return - } + memberName := getMemberName(ic.Member) + for i := range songs { + songs[i].RequestedBy = &memberName + } - if len(songs) == 0 { - FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ - Content: "😨 Could not find any playable songs", - }) - return - } + if len(songs) == 0 { + FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ + Embeds: []*discordgo.MessageEmbed{GenerateFailedToFindSong(input, ic.Member)}, + }) + return + } - if len(songs) == 1 { - song := songs[0] - metadata := song - - if err := player.AddSong(&ic.ChannelID, &vs.ChannelID, song); err != nil { - logger.Info("failed to add song", zap.Error(err), zap.String("input", input)) - FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ - Content: "😨 Failed to add song", - }) - return - } - - FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ - Embeds: []*discordgo.MessageEmbed{ - { - Title: "Added song", - Fields: []*discordgo.MessageEmbedField{ - { - Name: "Name", - Value: metadata.GetHumanName(), - }, - { - Name: "Duration", - Value: metadata.Duration.String(), - }, - }, - }, - }, - }) - } else { - handler.storage.SaveSongList(ic.ChannelID, songs) - - FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ - Content: fmt.Sprintf("πŸ‘€ The song is part of a playlist, which contains %d songs. What should I do?", len(songs)), - Components: []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.SelectMenu{ - CustomID: "add_song_playlist", - Options: []discordgo.SelectMenuOption{ - {Label: "Add song", Value: "song", Emoji: discordgo.ComponentEmoji{Name: "🎡"}}, - {Label: "Add whole playlist", Value: "playlist", Emoji: discordgo.ComponentEmoji{Name: "🎢"}}, - }, - }, - }, - }, - }, - }) - } - }() + if len(songs) == 1 { + song := songs[0] + if err := player.AddSong(&ic.ChannelID, &vs.ChannelID, song); err != nil { + logger.Info("failed to add song", zap.Error(err), zap.String("input", input)) + FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ + Embeds: []*discordgo.MessageEmbed{GenerateFailedToAddSongEmbed(input, ic.Member)}, + }) + return + } + + FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ + Embeds: []*discordgo.MessageEmbed{GenerateAddedSongEmbed(song, ic.Member)}, + }) return } - } - InteractionRespondMessage(handler.logger, s, ic.Interaction, "🀷🏽 You are not in a voice channel. Join a voice channel to play a song.") + handler.storage.SaveSongList(ic.ChannelID, songs) + + FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ + Embeds: []*discordgo.MessageEmbed{GenerateAskAddPlaylistEmbed(songs, ic.Member)}, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "add_song_playlist", + Options: []discordgo.SelectMenuOption{ + {Label: "Add song", Value: "song", Emoji: discordgo.ComponentEmoji{Name: "🎡"}}, + {Label: "Add whole playlist", Value: "playlist", Emoji: discordgo.ComponentEmoji{Name: "🎢"}}, + }, + }, + }, + }, + }, + }) + + }(ic, vs) } func (handler *InteractionHandler) CreatePlaylist(s *discordgo.Session, ic *discordgo.InteractionCreate, opt *discordgo.ApplicationCommandInteractionDataOption) { @@ -225,25 +215,17 @@ func (handler *InteractionHandler) CreatePlaylist(s *discordgo.Session, ic *disc } if length > 20 { - InteractionRespondMessage(handler.logger, s, ic.Interaction, "😨 You cannot request a playlist longer than 20 songs.") + InteractionRespondMessage(handler.logger, s, ic.Interaction, MessageTooLargePlaylist) return } - var voiceState *discordgo.VoiceState - - for _, vs := range g.VoiceStates { - if vs.UserID == ic.Member.User.ID { - voiceState = vs - break - } - } - - if voiceState == nil { - InteractionRespondMessage(handler.logger, s, ic.Interaction, "🀷🏽 You are not in a voice channel. Join a voice channel to play a song.") + vs := getUsersVoiceState(g, ic.Member.User) + if vs == nil { + InteractionRespondMessage(handler.logger, s, ic.Interaction, MessageUserNotInVoiceChannel) return } - go func() { + go func(ic *discordgo.InteractionCreate, vs *discordgo.VoiceState) { playlist, err := handler.playlistGenerator.GeneratePlaylist(handler.ctx, &sources.PlaylistParams{ Description: description, Length: int(length), @@ -251,42 +233,41 @@ func (handler *InteractionHandler) CreatePlaylist(s *discordgo.Session, ic *disc if err != nil { logger.Info("failed to generate playlist", zap.Error(err)) FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{ - Content: "😨 Failed to generate playlist", + Content: MessageFailedGeneratePlaylist, }) return } logger.Debug("generated playlist", zap.Any("songs", playlist.Playlist)) - resposeMessage := strings.Builder{} + memberName := getMemberName(ic.Member) + songs := make([]*bot.Song, 0, len(playlist.Playlist)) for _, input := range playlist.Playlist { - songs, err := handler.songLookuper.LookupSongs(handler.ctx, input) + ss, err := handler.songLookuper.LookupSongs(handler.ctx, input) if err != nil { logger.Info("failed to lookup song metadata", zap.Error(err), zap.String("input", input)) continue } - if len(songs) == 0 { + if len(ss) == 0 { continue } - song := songs[0] - if err := player.AddSong(&ic.ChannelID, &voiceState.ChannelID, song); err != nil { - logger.Info("failed to add song", zap.Error(err), zap.String("input", input)) - continue - } + song := ss[0] + song.RequestedBy = &memberName + + songs = append(songs, song) + } - resposeMessage.WriteString(fmt.Sprintf("- %s (%s)\n", song.Title, song.Duration)) + if err := player.AddSong(&ic.ChannelID, &vs.ChannelID, songs...); err != nil { + logger.Info("failed to add songs", zap.Error(err)) } FollowupMessageCreate(logger, s, ic.Interaction, &discordgo.WebhookParams{ - Content: playlist.Intro, - Embeds: []*discordgo.MessageEmbed{ - {Title: "Songs:", Description: resposeMessage.String()}, - }, + Embeds: []*discordgo.MessageEmbed{GeneratePlaylistAdded(playlist.Intro, songs, ic.Member)}, }) - }() + }(ic, vs) InteractionRespond(logger, s, ic.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, @@ -347,7 +328,35 @@ func (handler *InteractionHandler) AddSongOrPlaylist(s *discordgo.Session, ic *d handler.logger.Info("failed to add song", zap.Error(err), zap.String("input", song.URL)) InteractionRespondMessage(handler.logger, s, ic.Interaction, "😨 Failed to add song") } else { - InteractionRespondMessage(handler.logger, s, ic.Interaction, fmt.Sprintf("βž• Added **%s** - %s to playlist", song.Title, song.URL)) + embed := &discordgo.MessageEmbed{ + Author: &discordgo.MessageEmbedAuthor{ + Name: "Added to queue", + }, + Title: song.GetHumanName(), + URL: song.URL, + Footer: &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Requested by %s", *song.RequestedBy), + }, + Fields: []*discordgo.MessageEmbedField{ + { + Name: "Duration", + Value: utils.FmtDuration(song.Duration), + }, + }, + } + + if song.ThumbnailURL != nil { + embed.Thumbnail = &discordgo.MessageEmbedThumbnail{ + URL: *song.ThumbnailURL, + } + } + + InteractionRespond(handler.logger, s, ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + }, + }) } } @@ -369,7 +378,7 @@ func (handler *InteractionHandler) StopPlaying(s *discordgo.Session, ic *discord return } - InteractionRespondMessage(handler.logger, s, ic.Interaction, "⏹️ Stopped playing!") + InteractionRespondMessage(handler.logger, s, ic.Interaction, "⏹️ Stopped playing") } func (handler *InteractionHandler) SkipSong(s *discordgo.Session, ic *discordgo.InteractionCreate, acido *discordgo.ApplicationCommandInteractionDataOption) { @@ -520,3 +529,13 @@ func (handler *InteractionHandler) getGuildPlayer(guildID GuildID) *bot.GuildPla return player } + +func getUsersVoiceState(guild *discordgo.Guild, user *discordgo.User) *discordgo.VoiceState { + for _, vs := range guild.VoiceStates { + if vs.UserID == user.ID { + return vs + } + } + + return nil +} diff --git a/pkg/discord/messages.go b/pkg/discord/messages.go new file mode 100644 index 0000000..e421558 --- /dev/null +++ b/pkg/discord/messages.go @@ -0,0 +1,125 @@ +package discord + +import ( + "fmt" + "strings" + "time" + + "github.com/Trojan295/discord-airplay/pkg/bot" + "github.com/Trojan295/discord-airplay/pkg/utils" + "github.com/bwmarrin/discordgo" +) + +var ( + MessageUserNotInVoiceChannel = "🀷🏽 You are not in a voice channel. Join a voice channel to play a song." + MessageTooLargePlaylist = "😨 You cannot request a playlist longer than 20 songs." + MessageFailedGeneratePlaylist = "😨 Failed to generate playlist." +) + +func GenerateAddingSongEmbed(input string, member *discordgo.Member) *discordgo.MessageEmbed { + return generateAddingSongEmbed(input, "🎡 Adding song to queue...", member) +} + +func GenerateAddedSongEmbed(song *bot.Song, member *discordgo.Member) *discordgo.MessageEmbed { + embed := generateAddingSongEmbed(song.GetHumanName(), "🎡 Added to queue.", member) + embed.Fields = []*discordgo.MessageEmbedField{ + { + Name: "Duration", + Value: utils.FmtDuration(song.Duration), + }, + } + + if song.ThumbnailURL != nil { + embed.Thumbnail = &discordgo.MessageEmbedThumbnail{ + URL: *song.ThumbnailURL, + } + } + + return embed +} + +func GenerateAskAddPlaylistEmbed(songs []*bot.Song, requestor *discordgo.Member) *discordgo.MessageEmbed { + title := fmt.Sprintf("πŸ‘€ The song is part of a playlist, which contains %d songs. What should I do?", len(songs)) + return generateAddingSongEmbed(title, "", requestor) +} + +func GenerateFailedToAddSongEmbed(input string, member *discordgo.Member) *discordgo.MessageEmbed { + return generateAddingSongEmbed(input, "😨 Failed to add song.", member) +} + +func GenerateFailedToFindSong(input string, member *discordgo.Member) *discordgo.MessageEmbed { + return generateAddingSongEmbed(input, "😨 Could not find any playable songs.", member) +} + +func GeneratePlayingSongEmbed(message *bot.PlayMessage) *discordgo.MessageEmbed { + progressBar := generateProgressBar(float64(message.Position)/float64(message.Song.Duration), 20) + + embed := &discordgo.MessageEmbed{ + Title: fmt.Sprintf("▢️ %s", message.Song.GetHumanName()), + URL: message.Song.URL, + Description: fmt.Sprintf("%s\n%s / %s", progressBar, utils.FmtDuration(message.Position), utils.FmtDuration(message.Song.Duration)), + } + + if message.Song.ThumbnailURL != nil { + embed.Thumbnail = &discordgo.MessageEmbedThumbnail{ + URL: *message.Song.ThumbnailURL, + } + } + + if message.Song.RequestedBy != nil { + embed.Footer = &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Requested by %s", *message.Song.RequestedBy), + } + } + + return embed +} + +func GeneratePlaylistAdded(intro string, songs []*bot.Song, member *discordgo.Member) *discordgo.MessageEmbed { + descriptionBuilder := strings.Builder{} + duration := time.Duration(0) + + for _, song := range songs { + duration += song.Duration + descriptionBuilder.WriteString(fmt.Sprintf("1.️ %s (%s)\n", song.GetHumanName(), utils.FmtDuration(song.Duration))) + } + + title := fmt.Sprintf("🎡 %s", intro) + + embed := generateAddingSongEmbed(title, descriptionBuilder.String(), member) + embed.Fields = []*discordgo.MessageEmbedField{ + { + Name: "Duration", + Value: utils.FmtDuration(duration), + }, + } + + return embed +} + +func generateAddingSongEmbed(title, description string, requestor *discordgo.Member) *discordgo.MessageEmbed { + embed := &discordgo.MessageEmbed{ + Title: title, + Description: description, + Footer: &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Requested by %s", getMemberName(requestor)), + }, + } + + return embed +} + +func generateProgressBar(progress float64, length int) string { + played := int(progress * float64(length)) + + progressBar := "" + for i := 0; i < played; i++ { + progressBar += "β–¬" + } + progressBar += "πŸ”˜" + for i := played; i < length; i++ { + progressBar += "β–¬" + } + + return progressBar +} diff --git a/pkg/discord/utils.go b/pkg/discord/utils.go new file mode 100644 index 0000000..fb2b8ce --- /dev/null +++ b/pkg/discord/utils.go @@ -0,0 +1,11 @@ +package discord + +import "github.com/bwmarrin/discordgo" + +func getMemberName(member *discordgo.Member) string { + if member.Nick != "" { + return member.Nick + } + + return member.User.Username +} diff --git a/pkg/discord/voicechat.go b/pkg/discord/voicechat.go index 56bc753..b25e2b9 100644 --- a/pkg/discord/voicechat.go +++ b/pkg/discord/voicechat.go @@ -35,25 +35,7 @@ func (session *DiscordVoiceChatSession) SendMessage(channelID, message string) e func (session *DiscordVoiceChatSession) SendPlayMessage(channelID string, message *bot.PlayMessage) (string, error) { msg, err := session.discordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{ - { - Title: "▢️ Playing song", - Fields: []*discordgo.MessageEmbedField{ - { - Name: "Name", - Value: message.Song.GetHumanName(), - }, - { - Name: "Progress", - Value: fmt.Sprintf("%s / %s", fmtDuration(message.Position), fmtDuration(message.Song.Duration)), - }, - { - Name: "URL", - Value: message.Song.URL, - }, - }, - }, - }, + Embed: GeneratePlayingSongEmbed(message), }) if err != nil { return "", err @@ -66,25 +48,7 @@ func (session *DiscordVoiceChatSession) EditPlayMessage(channelID, messageID str _, err := session.discordSession.ChannelMessageEditComplex(&discordgo.MessageEdit{ ID: messageID, Channel: channelID, - Embeds: []*discordgo.MessageEmbed{ - { - Title: "▢️ Playing song", - Fields: []*discordgo.MessageEmbedField{ - { - Name: "Name", - Value: message.Song.GetHumanName(), - }, - { - Name: "Progress", - Value: fmt.Sprintf("%s / %s", fmtDuration(message.Position), fmtDuration(message.Song.Duration)), - }, - { - Name: "URL", - Value: message.Song.URL, - }, - }, - }, - }, + Embeds: []*discordgo.MessageEmbed{GeneratePlayingSongEmbed(message)}, }) return err } @@ -129,11 +93,3 @@ func (session *DiscordVoiceChatSession) SendAudio(ctx context.Context, reader io return nil } - -func fmtDuration(d time.Duration) string { - d = d.Round(time.Second) - m := d / time.Minute - d -= m * time.Minute - s := d / time.Second - return fmt.Sprintf("%02d:%02d", m, s) -} diff --git a/pkg/sources/chatgpt.go b/pkg/sources/chatgpt.go index b1222d6..9c08e6a 100644 --- a/pkg/sources/chatgpt.go +++ b/pkg/sources/chatgpt.go @@ -49,7 +49,7 @@ func (g *ChatGPTPlaylistGenerator) GeneratePlaylist(ctx context.Context, params Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleSystem, - Content: "I want you to act as a DJ. I will provide you with a description of a playlist and number of songs, and you will create it for me. You should output the list of songs, each in a new line with artist and title. Add also some nice introduction before the song list. Do not include any additional information or description, simply output: \n. - ", + Content: "I want you to act as a DJ. I will provide you with a description of a playlist and number of songs, and you will create it for me. You should output the list of songs, each in a new line with artist and title. Add also some nice introduction before the song list, max 250 characters. Do not include any additional information or description, simply output: <introduction>\n<song number>. <artist> - <title>", }, { Role: openai.ChatMessageRoleUser, diff --git a/pkg/sources/youtube.go b/pkg/sources/youtube.go index eff19d9..5a85591 100644 --- a/pkg/sources/youtube.go +++ b/pkg/sources/youtube.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "encoding/json" "fmt" "io" "log" @@ -13,20 +14,25 @@ import ( "time" "github.com/Trojan295/discord-airplay/pkg/bot" + "golang.org/x/exp/slog" ) const ( downloadBuffer = 100 * 1024 // 100 KiB ) -type YoutubeFetcher struct{} +type YoutubeFetcher struct { + Logger *slog.Logger +} func NewYoutubeFetcher() *YoutubeFetcher { - return &YoutubeFetcher{} + return &YoutubeFetcher{ + Logger: slog.Default(), + } } func (s *YoutubeFetcher) LookupSongs(ctx context.Context, input string) ([]*bot.Song, error) { - ytDlpPrintColumns := []string{"title", "original_url", "is_live", "duration"} + ytDlpPrintColumns := []string{"title", "original_url", "is_live", "duration", "thumbnail", "thumbnails"} printColumns := strings.Join(ytDlpPrintColumns, ",") args := []string{"--print", printColumns, "--flat-playlist", "-U"} @@ -54,12 +60,26 @@ func (s *YoutubeFetcher) LookupSongs(ctx context.Context, input string) ([]*bot. for i := 0; i < songCount; i++ { duration, _ := strconv.ParseFloat(ytOutLines[linesPerSong*i+3], 32) + var thumbnailURL *string = nil + if ytOutLines[linesPerSong*i+4] != "NA" { + thumbnailURL = &ytOutLines[linesPerSong*i+4] + } else if ytOutLines[linesPerSong*i+5] != "NA" { + thumbnail, err := getThumbnail(ytOutLines[linesPerSong*i+5]) + if err != nil { + s.Logger.Error("failed to get thumbnail", "error", err) + } + if thumbnail != nil { + thumbnailURL = &thumbnail.URL + } + } + song := &bot.Song{ - Type: "yt-dlp", - Title: ytOutLines[linesPerSong*i], - URL: ytOutLines[linesPerSong*i+1], - Playable: ytOutLines[linesPerSong*i+2] == "False" || ytOutLines[3*i+2] == "NA", - Duration: time.Second * time.Duration(duration), + Type: "yt-dlp", + Title: ytOutLines[linesPerSong*i], + URL: ytOutLines[linesPerSong*i+1], + Playable: ytOutLines[linesPerSong*i+2] == "False" || ytOutLines[3*i+2] == "NA", + ThumbnailURL: thumbnailURL, + Duration: time.Second * time.Duration(duration), } if !song.Playable { continue @@ -104,3 +124,31 @@ func (s *YoutubeFetcher) GetDCAData(ctx context.Context, song *bot.Song) (io.Rea return reader, nil } + +type thumnail struct { + URL string `json:"url"` + Preference int `json:"preference"` +} + +func getThumbnail(thumnailsStr string) (*thumnail, error) { + thumnailsStr = strings.ReplaceAll(thumnailsStr, "'", "\"") + + var thumbnails []thumnail + if err := json.Unmarshal([]byte(thumnailsStr), &thumbnails); err != nil { + return nil, err + } + + if len(thumbnails) == 0 { + return nil, nil + } + + tn := &thumbnails[0] + for i := range thumbnails { + t := thumbnails[i] + if t.Preference > tn.Preference { + tn = &t + } + } + + return tn, nil +} diff --git a/pkg/utils/fmt.go b/pkg/utils/fmt.go new file mode 100644 index 0000000..e7436e6 --- /dev/null +++ b/pkg/utils/fmt.go @@ -0,0 +1,22 @@ +package utils + +import ( + "fmt" + "time" +) + +func FmtDuration(d time.Duration) string { + d = d.Round(time.Second) + + h := d / time.Hour + d -= h * time.Hour + m := d / time.Minute + d -= m * time.Minute + s := d / time.Second + + if h > 0 { + return fmt.Sprintf("%02d:%02d:%02d", h, m, s) + } + + return fmt.Sprintf("%02d:%02d", m, s) +}