diff --git a/app/main.go b/app/main.go index 9986f4b..6b1225b 100644 --- a/app/main.go +++ b/app/main.go @@ -53,6 +53,18 @@ func main() { }, }, }, + { + Name: "download video", + Type: discordgo.MessageApplicationCommand, + IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ + discordgo.ApplicationIntegrationUserInstall, + }, + + Contexts: &[]discordgo.InteractionContextType{ + discordgo.InteractionContextBotDM, + discordgo.InteractionContextPrivateChannel, + }, + }, } var componentHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ @@ -448,6 +460,193 @@ func main() { FormatOptions: formatOptions, }) + // Update message with format selection menus + _, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Content: ptr("Select a video format:"), + Components: &[]discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "video_select", + Placeholder: "Choose a video format...", + MaxValues: 1, + Options: videoMenuOptions, + }, + }, + }, + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "audio_select", + Placeholder: "Choose an audio format...", + MaxValues: 1, + Disabled: true, + Options: audioMenuOptions, + }, + }, + }, + }, + }) + if err != nil { + log.Printf("Error updating interaction: %v", err) + } + }() + }, + "download video": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + data := i.ApplicationCommandData() + targetMsg, ok := data.Resolved.Messages[data.TargetID] + if !ok || targetMsg == nil { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Error: Could not find the target message", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + messageContent := targetMsg.Content + + var url string + url = extractURLFromString(messageContent) + if url == "" { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Error: No URL provided", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Send initial "fetching formats" response + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("%s Fetching available formats...", loading_emoji), + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + log.Printf("Error: %v", err) + return + } + + // Fetch formats asynchronously + go func() { + formatOptions, err := GetFormats(url) + if err != nil { + _, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Content: ptr("❌ Error fetching formats: " + err.Error()), + }) + if err != nil { + log.Printf("Error updating interaction: %v", err) + } + return + } + + // Sort video formats: highest resolution first, then by bitrate + sort.Slice(formatOptions.VideoOptions, func(i, j int) bool { + // Compare by height (descending) + heightI := 0 + if formatOptions.VideoOptions[i].Height != nil { + heightI = *formatOptions.VideoOptions[i].Height + } + heightJ := 0 + if formatOptions.VideoOptions[j].Height != nil { + heightJ = *formatOptions.VideoOptions[j].Height + } + + if heightI != heightJ { + return heightI > heightJ + } + + // If heights are equal, compare by TBR (descending) + tbrI := 0.0 + if formatOptions.VideoOptions[i].TBR != nil { + tbrI = *formatOptions.VideoOptions[i].TBR + } + tbrJ := 0.0 + if formatOptions.VideoOptions[j].TBR != nil { + tbrJ = *formatOptions.VideoOptions[j].TBR + } + + return tbrI > tbrJ + }) + + // Sort audio formats: highest bitrate first + sort.Slice(formatOptions.AudioOptions, func(i, j int) bool { + tbrI := 0.0 + if formatOptions.AudioOptions[i].TBR != nil { + tbrI = *formatOptions.AudioOptions[i].TBR + } + tbrJ := 0.0 + if formatOptions.AudioOptions[j].TBR != nil { + tbrJ = *formatOptions.AudioOptions[j].TBR + } + + return tbrI > tbrJ + }) + + // Build video format options for Discord select menu + videoMenuOptions := []discordgo.SelectMenuOption{} + for _, vOpt := range formatOptions.VideoOptions { + label := fmt.Sprintf("%s (%s", vOpt.Resolution, vOpt.Ext) + if vOpt.TBR != nil { + label += fmt.Sprintf(", %.0fkbps", *vOpt.TBR) + } + label += ")" + + // Discord has a 100 char limit on labels + if len(label) > 100 { + label = label[:97] + "..." + } + + videoMenuOptions = append(videoMenuOptions, discordgo.SelectMenuOption{ + Label: label, + Value: vOpt.FormatID, + }) + + // Discord has a limit of 25 options per select menu + if len(videoMenuOptions) >= 25 { + break + } + } + + // Build audio format options for Discord select menu + audioMenuOptions := []discordgo.SelectMenuOption{} + for _, aOpt := range formatOptions.AudioOptions { + label := aOpt.Format + if aOpt.Language != nil { + label += fmt.Sprintf(" [%s]", *aOpt.Language) + } + if aOpt.TBR != nil { + label += fmt.Sprintf(" (%.0fkbps)", *aOpt.TBR) + } + + // Discord has a 100 char limit on labels + if len(label) > 100 { + label = label[:97] + "..." + } + + audioMenuOptions = append(audioMenuOptions, discordgo.SelectMenuOption{ + Label: label, + Value: aOpt.FormatID, + }) + + // Discord has a limit of 25 options per select menu + if len(audioMenuOptions) >= 25 { + break + } + } + + // Store format options in interaction state + setInteractionState(i.Interaction.Token, &InteractionState{ + URL: url, + FormatOptions: formatOptions, + }) + // Update message with format selection menus _, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ Content: ptr("Select a video format:"), @@ -488,13 +687,7 @@ func main() { if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { h(s, i) } - if h, ok := componentHandlers[i.ApplicationCommandData().Name]; ok { - h(s, i) - } case discordgo.InteractionMessageComponent: - if h, ok := commandHandlers[i.MessageComponentData().CustomID]; ok { - h(s, i) - } if h, ok := componentHandlers[i.MessageComponentData().CustomID]; ok { h(s, i) } diff --git a/app/misc.go b/app/misc.go index b05f15c..5c20a56 100644 --- a/app/misc.go +++ b/app/misc.go @@ -3,10 +3,18 @@ package main import ( "fmt" "os" + "regexp" ) var loading_emoji = os.Getenv("LOADING_EMOJI") +func extractURLFromString(in_url string) string { + url_pattern := regexp.MustCompile(`https?://\S+`) + var match = url_pattern.Find([]byte(in_url)) + + return string(match) +} + // Helper function to create string pointer func ptr(s string) *string { return &s