diff --git a/app/main.go b/app/main.go index 7e27aa6..c20dcbf 100644 --- a/app/main.go +++ b/app/main.go @@ -7,7 +7,6 @@ import ( "os/signal" "sort" "syscall" - "time" "github.com/bwmarrin/discordgo" ) @@ -142,54 +141,10 @@ func main() { setInteractionState(i.Interaction.Token, state) // Build audio format options - audioMenuOptions := []discordgo.SelectMenuOption{} - for _, aOpt := range state.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 - } - } + audioMenuOptions := buildAudioMenuOptions(state.FormatOptions.AudioOptions) // Build video format options (to keep them visible but disabled) - videoMenuOptions := []discordgo.SelectMenuOption{} - for _, vOpt := range state.FormatOptions.VideoOptions { - label := fmt.Sprintf("%s (%s", vOpt.Resolution, vOpt.Ext) - if vOpt.TBR != nil { - label += fmt.Sprintf(", %.0fkbps", *vOpt.TBR) - } - label += ")" - - if len(label) > 100 { - label = label[:97] + "..." - } - - videoMenuOptions = append(videoMenuOptions, discordgo.SelectMenuOption{ - Label: label, - Value: vOpt.FormatID, - }) - - if len(videoMenuOptions) >= 25 { - break - } - } + videoMenuOptions := buildVideoMenuOptions(state.FormatOptions.VideoOptions) // Update components - disable video select, enable audio select updatedComponents := []discordgo.MessageComponent{ @@ -292,14 +247,7 @@ func main() { // Respond immediately to prevent timeout response = fmt.Sprintf("%s **Starting download**", loading_emoji) - // Start async download after responding - go func() { - // Small delay to ensure response is sent first - time.Sleep(100 * time.Millisecond) - startAsyncDownload(s, i, state.Requester, state.URL, state.VideoFormatID, state.VideoFormatName, state.AudioFormatID, state.AudioFormatName, out_dir, temp_dir) - }() - - // Clean up state after starting download + // Clean up state before responding deleteInteractionState(i.Interaction.Token) } else { response = "I don't see a video here :(" @@ -314,6 +262,9 @@ func main() { if err != nil { log.Printf("Error: %v", err) } + if state.URL != "" { + go startAsyncDownload(s, i, state.Requester, state.URL, state.VideoFormatID, state.VideoFormatName, state.AudioFormatID, state.AudioFormatName, out_dir, temp_dir) + } }, } @@ -353,150 +304,7 @@ func main() { } // 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:"), - 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) - } - }() + go fetchAndShowFormats(s, i, url) }, "download video": func(s *discordgo.Session, i *discordgo.InteractionCreate) { data := i.ApplicationCommandData() @@ -511,10 +319,7 @@ func main() { }) return } - messageContent := targetMsg.Content - - var url string - url = extractURLFromString(messageContent) + url := extractURLFromString(targetMsg.Content) if url == "" { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, @@ -540,150 +345,7 @@ func main() { } // 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:"), - 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) - } - }() + go fetchAndShowFormats(s, i, url) }, "version": func(s *discordgo.Session, i *discordgo.InteractionCreate) { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ @@ -726,3 +388,144 @@ func main() { s.Close() } + +func buildVideoMenuOptions(videoOptions []VideoOption) []discordgo.SelectMenuOption { + opts := make([]discordgo.SelectMenuOption, 0, 25) + for _, vOpt := range videoOptions { + label := fmt.Sprintf("%s (%s", vOpt.Resolution, vOpt.Ext) + if vOpt.TBR != nil { + label += fmt.Sprintf(", %.0fkbps", *vOpt.TBR) + } + label += ")" + if len(label) > 100 { + label = label[:97] + "..." + } + opts = append(opts, discordgo.SelectMenuOption{ + Label: label, + Value: vOpt.FormatID, + }) + if len(opts) >= 25 { + break + } + } + return opts +} + +func buildAudioMenuOptions(audioOptions []AudioOption) []discordgo.SelectMenuOption { + opts := make([]discordgo.SelectMenuOption, 0, 25) + for _, aOpt := range audioOptions { + label := aOpt.Format + if aOpt.Language != nil { + label += fmt.Sprintf(" [%s]", *aOpt.Language) + } + if aOpt.TBR != nil { + label += fmt.Sprintf(" (%.0fkbps)", *aOpt.TBR) + } + if len(label) > 100 { + label = label[:97] + "..." + } + opts = append(opts, discordgo.SelectMenuOption{ + Label: label, + Value: aOpt.FormatID, + }) + if len(opts) >= 25 { + break + } + } + return opts +} + +func fetchAndShowFormats(s *discordgo.Session, i *discordgo.InteractionCreate, url string) { + 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(x, y int) bool { + heightX, heightY := 0, 0 + if formatOptions.VideoOptions[x].Height != nil { + heightX = *formatOptions.VideoOptions[x].Height + } + if formatOptions.VideoOptions[y].Height != nil { + heightY = *formatOptions.VideoOptions[y].Height + } + if heightX != heightY { + return heightX > heightY + } + tbrX, tbrY := 0.0, 0.0 + if formatOptions.VideoOptions[x].TBR != nil { + tbrX = *formatOptions.VideoOptions[x].TBR + } + if formatOptions.VideoOptions[y].TBR != nil { + tbrY = *formatOptions.VideoOptions[y].TBR + } + return tbrX > tbrY + }) + + // Sort audio formats: highest bitrate first + sort.Slice(formatOptions.AudioOptions, func(x, y int) bool { + tbrX, tbrY := 0.0, 0.0 + if formatOptions.AudioOptions[x].TBR != nil { + tbrX = *formatOptions.AudioOptions[x].TBR + } + if formatOptions.AudioOptions[y].TBR != nil { + tbrY = *formatOptions.AudioOptions[y].TBR + } + return tbrX > tbrY + }) + + videoMenuOptions := buildVideoMenuOptions(formatOptions.VideoOptions) + audioMenuOptions := buildAudioMenuOptions(formatOptions.AudioOptions) + + if len(videoMenuOptions) == 0 || len(audioMenuOptions) == 0 { + _, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Content: ptr("❌ No separate video/audio streams found for this URL. The source may only provide combined formats."), + }) + if err != nil { + log.Printf("Error updating interaction: %v", err) + } + return + } + + setInteractionState(i.Interaction.Token, &InteractionState{ + URL: url, + FormatOptions: formatOptions, + }) + + _, 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) + } +} diff --git a/app/misc.go b/app/misc.go index 5c20a56..0f63249 100644 --- a/app/misc.go +++ b/app/misc.go @@ -8,11 +8,10 @@ import ( 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)) +var urlPattern = regexp.MustCompile(`https?://\S+`) - return string(match) +func extractURLFromString(in_url string) string { + return string(urlPattern.Find([]byte(in_url))) } // Helper function to create string pointer