interaction state management, call ytdlp through /download function
This commit is contained in:
337
app/main.go
337
app/main.go
@@ -1,14 +1,133 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InteractionState holds the state for a specific interaction
|
||||||
|
type InteractionState struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadResult represents the result of an async download operation
|
||||||
|
type DownloadResult struct {
|
||||||
|
Success bool
|
||||||
|
Message string
|
||||||
|
URL string
|
||||||
|
Format string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// startAsyncDownload initiates a download in a goroutine and handles progress updates
|
||||||
|
func startAsyncDownload(s *discordgo.Session, i *discordgo.InteractionCreate, url, audioFormat, outputDir string) {
|
||||||
|
resultChan := make(chan DownloadResult, 1)
|
||||||
|
|
||||||
|
// Start download in goroutine
|
||||||
|
go func() {
|
||||||
|
defer close(resultChan)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Handle panic from downloadVideo
|
||||||
|
resultChan <- DownloadResult{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("Download failed: %v", r),
|
||||||
|
URL: url,
|
||||||
|
Format: audioFormat,
|
||||||
|
Error: fmt.Errorf("%v", r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call downloadVideo (it panics on error instead of returning error)
|
||||||
|
downloadVideo(outputDir, url, DownloadOptions{EmbedThumbnail: true, IncludeSubtitles: true})
|
||||||
|
|
||||||
|
// If we reach here, download was successful
|
||||||
|
resultChan <- DownloadResult{
|
||||||
|
Success: true,
|
||||||
|
Message: "Video Downloaded Successfully!",
|
||||||
|
URL: url,
|
||||||
|
Format: audioFormat,
|
||||||
|
Error: nil,
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Handle results asynchronously
|
||||||
|
go func() {
|
||||||
|
// First update the original ephemeral message with "Processing..."
|
||||||
|
_, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||||
|
Content: ptr("🔄 Processing download...\nURL: " + url + "\nAudio: " + audioFormat),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error updating interaction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := <-resultChan
|
||||||
|
|
||||||
|
if result.Success {
|
||||||
|
// Update ephemeral message with completion status
|
||||||
|
_, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||||
|
Content: ptr("✅ Download completed successfully!\nURL: " + result.URL + "\nAudio: " + result.Format),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error updating interaction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send non-ephemeral completion message
|
||||||
|
_, err = s.FollowupMessageCreate(i.Interaction, false, &discordgo.WebhookParams{
|
||||||
|
Content: "📥 Video downloaded: " + result.URL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error sending public completion message: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update ephemeral message with error
|
||||||
|
_, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||||
|
Content: ptr("❌ Download failed: " + result.Message + "\nURL: " + result.URL + "\nAudio: " + result.Format),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error updating interaction: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create string pointer
|
||||||
|
func ptr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global state management
|
||||||
|
var (
|
||||||
|
interactionStates = make(map[string]*InteractionState)
|
||||||
|
interactionStatesMutex = sync.RWMutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInteractionState(token string) *InteractionState {
|
||||||
|
interactionStatesMutex.RLock()
|
||||||
|
defer interactionStatesMutex.RUnlock()
|
||||||
|
return interactionStates[token]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setInteractionState(token string, state *InteractionState) {
|
||||||
|
interactionStatesMutex.Lock()
|
||||||
|
defer interactionStatesMutex.Unlock()
|
||||||
|
interactionStates[token] = state
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteInteractionState(token string) {
|
||||||
|
interactionStatesMutex.Lock()
|
||||||
|
defer interactionStatesMutex.Unlock()
|
||||||
|
delete(interactionStates, token)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
out_dir := os.Getenv("OUT_PATH")
|
out_dir := os.Getenv("OUT_PATH")
|
||||||
@@ -48,6 +167,140 @@ func main() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var componentHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
|
||||||
|
"video_select": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
// Update components
|
||||||
|
updatedComponents := []discordgo.MessageComponent{
|
||||||
|
discordgo.ActionsRow{
|
||||||
|
Components: []discordgo.MessageComponent{
|
||||||
|
discordgo.SelectMenu{
|
||||||
|
CustomID: "video_select",
|
||||||
|
Placeholder: "Choose a video format...",
|
||||||
|
MaxValues: 1,
|
||||||
|
Disabled: true,
|
||||||
|
Options: []discordgo.SelectMenuOption{
|
||||||
|
{
|
||||||
|
Label: "1920x1080 1080p (mp4, 1910kbps)",
|
||||||
|
Value: "1080p",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "1280x720 720p (mp4, 700kbps)",
|
||||||
|
Value: "720p",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "640x480 480p (mp4, 300kbps)",
|
||||||
|
Value: "480p",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
discordgo.ActionsRow{
|
||||||
|
Components: []discordgo.MessageComponent{
|
||||||
|
discordgo.SelectMenu{
|
||||||
|
CustomID: "audio_select",
|
||||||
|
Placeholder: "Choose an audio format...",
|
||||||
|
MaxValues: 1,
|
||||||
|
Disabled: false,
|
||||||
|
Options: []discordgo.SelectMenuOption{
|
||||||
|
{
|
||||||
|
Label: "Medium",
|
||||||
|
Value: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Low",
|
||||||
|
Value: "low",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseUpdateMessage,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "Now select an audio format:",
|
||||||
|
Components: updatedComponents,
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audio_select": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
// Get URL from interaction state with fallback search
|
||||||
|
var state *InteractionState
|
||||||
|
|
||||||
|
// First try current token
|
||||||
|
state = getInteractionState(i.Interaction.Token)
|
||||||
|
|
||||||
|
// If not found, try to find state associated with this interaction context
|
||||||
|
if state == nil {
|
||||||
|
interactionStatesMutex.RLock()
|
||||||
|
for token, s := range interactionStates {
|
||||||
|
if s != nil && s.URL != "" {
|
||||||
|
// Found a state, transfer it to current token
|
||||||
|
state = s
|
||||||
|
// Copy to current token and clean up old one
|
||||||
|
interactionStatesMutex.RUnlock()
|
||||||
|
setInteractionState(i.Interaction.Token, state)
|
||||||
|
deleteInteractionState(token)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
interactionStatesMutex.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "Error: No video URL found. Please start over with /download.",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get selected audio format
|
||||||
|
selectedValues := i.MessageComponentData().Values
|
||||||
|
audioFormat := "medium" // default
|
||||||
|
if len(selectedValues) > 0 {
|
||||||
|
audioFormat = selectedValues[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
response := ""
|
||||||
|
if state.URL != "" {
|
||||||
|
// Respond immediately to prevent timeout
|
||||||
|
response = "🚀 Starting download...\nURL: " + state.URL + "\nAudio: " + audioFormat + "\n\nYou'll receive an update when the download completes!"
|
||||||
|
|
||||||
|
// Start async download after responding
|
||||||
|
go func() {
|
||||||
|
// Small delay to ensure response is sent first
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
startAsyncDownload(s, i, state.URL, audioFormat, out_dir)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Clean up state after starting download
|
||||||
|
deleteInteractionState(i.Interaction.Token)
|
||||||
|
} else {
|
||||||
|
response = "I don't see a video here :("
|
||||||
|
}
|
||||||
|
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: response,
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
|
var commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
|
||||||
"download": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
"download": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
options := i.ApplicationCommandData().Options
|
options := i.ApplicationCommandData().Options
|
||||||
@@ -55,26 +308,100 @@ func main() {
|
|||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
optionMap[opt.Name] = opt
|
optionMap[opt.Name] = opt
|
||||||
}
|
}
|
||||||
response := ""
|
|
||||||
|
var url string
|
||||||
if option, ok := optionMap["url"]; ok {
|
if option, ok := optionMap["url"]; ok {
|
||||||
response = "It works! Your URL is: " + option.StringValue()
|
url = option.StringValue()
|
||||||
} else {
|
} else {
|
||||||
response = "It works!"
|
|
||||||
}
|
|
||||||
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
Data: &discordgo.InteractionResponseData{
|
Data: &discordgo.InteractionResponseData{
|
||||||
Content: response,
|
Content: "Error: No URL provided",
|
||||||
Flags: discordgo.MessageFlagsEphemeral,
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store URL in interaction state (we'll use the response message ID later)
|
||||||
|
// For now, store with the current token, then update after getting message ID
|
||||||
|
setInteractionState(i.Interaction.Token, &InteractionState{URL: url})
|
||||||
|
|
||||||
|
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "Select a video format:",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
Components: []discordgo.MessageComponent{
|
||||||
|
discordgo.ActionsRow{
|
||||||
|
Components: []discordgo.MessageComponent{
|
||||||
|
discordgo.SelectMenu{
|
||||||
|
CustomID: "video_select",
|
||||||
|
Placeholder: "Choose a video format...",
|
||||||
|
MaxValues: 1,
|
||||||
|
Options: []discordgo.SelectMenuOption{
|
||||||
|
{
|
||||||
|
Label: "1920x1080 1080p (mp4, 1910kbps)",
|
||||||
|
Value: "1080p",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "1280x720 720p (mp4, 700kbps)",
|
||||||
|
Value: "720p",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "640x480 480p (mp4, 300kbps)",
|
||||||
|
Value: "480p",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
discordgo.ActionsRow{
|
||||||
|
Components: []discordgo.MessageComponent{
|
||||||
|
discordgo.SelectMenu{
|
||||||
|
CustomID: "audio_select",
|
||||||
|
Placeholder: "Choose an audio format...",
|
||||||
|
MaxValues: 1,
|
||||||
|
Disabled: true,
|
||||||
|
Options: []discordgo.SelectMenuOption{
|
||||||
|
{
|
||||||
|
Label: "Medium",
|
||||||
|
Value: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Low",
|
||||||
|
Value: "low",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error: %v", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
switch i.Type {
|
||||||
|
case discordgo.InteractionApplicationCommand:
|
||||||
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
|
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
|
||||||
h(s, i)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Println("Adding commands")
|
log.Println("Adding commands")
|
||||||
|
|||||||
Reference in New Issue
Block a user