summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-04-13 18:41:17 +0530
committerBobby <[email protected]>2025-04-13 18:41:17 +0530
commit6b486844a18fe8bf9b4ca4bdd3c44f45094e03e5 (patch)
treeb2655928e1fd60516619359ba214015084b05b9c
parentf884a0fe47f7403850de650d3be3733420ac5986 (diff)
downloadai-6b486844a18fe8bf9b4ca4bdd3c44f45094e03e5.tar.xz
ai-6b486844a18fe8bf9b4ca4bdd3c44f45094e03e5.zip
refactor; add logging; add disconnect; supress opus warnings
-rw-r--r--Makefile2
-rw-r--r--commands/commands.go4
-rw-r--r--commands/disconnect.go55
-rw-r--r--commands/helpers.go19
-rw-r--r--commands/play.go31
-rw-r--r--commands/play_autocomplete.go8
-rw-r--r--handlers/slashCommandHandler.go3
-rw-r--r--utils/music/search.go25
-rw-r--r--utils/music/voice.go274
9 files changed, 215 insertions, 206 deletions
diff --git a/Makefile b/Makefile
index 8417dbf..58101de 100644
--- a/Makefile
+++ b/Makefile
@@ -38,7 +38,7 @@ run:
dev:
$(call ensure_setup)
@echo "Running in development mode..."
- @go run $(MAIN_PATH) || true
+ @go run $(MAIN_PATH) 2>/dev/null || true
all: setup clean build run
diff --git a/commands/commands.go b/commands/commands.go
index 00f09f8..756c38f 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -17,5 +17,9 @@ var (
},
},
},
+ {
+ Name: "disconnect",
+ Description: "Disconnect the bot from the voice channel",
+ },
}
)
diff --git a/commands/disconnect.go b/commands/disconnect.go
new file mode 100644
index 0000000..94249c8
--- /dev/null
+++ b/commands/disconnect.go
@@ -0,0 +1,55 @@
+package commands
+
+import (
+ "ai/utils/music"
+ "fmt"
+
+ "github.com/bwmarrin/discordgo"
+)
+
+func Disconnect(s *discordgo.Session, i *discordgo.InteractionCreate) {
+ guildID := i.GuildID
+ userID := i.Member.User.ID
+
+ isSameVC, userChannelID := music.IsUserInSameVC(s, guildID, userID)
+
+ if userChannelID == "" {
+ respondWithError(s, i, "You must be in a voice channel to use this command.")
+ return
+ }
+
+ voice, exists := music.VoiceConnection[guildID]
+ if !exists {
+ respondWithError(s, i, "I'm not in a voice channel.")
+ return
+ }
+
+ if !isSameVC {
+ channel, err := s.Channel(voice.ChannelID)
+ if err == nil {
+ respondWithError(s, i, fmt.Sprintf("You must be in the same voice channel as me (**%s**) to use this command.", channel.Name))
+ } else {
+ respondWithError(s, i, "You must be in the same voice channel as me to use this command.")
+ }
+ return
+ }
+
+ channel, err := s.Channel(voice.ChannelID)
+ channelName := "voice channel"
+ if err == nil {
+ channelName = channel.Name
+ }
+
+ err = music.LeaveVoiceChannel(guildID)
+ if err != nil {
+ respondWithError(s, i, fmt.Sprintf("Error disconnecting from voice channel: %v", err))
+ return
+ }
+
+ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+ Type: discordgo.InteractionResponseChannelMessageWithSource,
+ Data: &discordgo.InteractionResponseData{
+ Content: fmt.Sprintf("✅ Disconnected from **%s**.", channelName),
+ },
+ })
+}
diff --git a/commands/helpers.go b/commands/helpers.go
new file mode 100644
index 0000000..19859db
--- /dev/null
+++ b/commands/helpers.go
@@ -0,0 +1,19 @@
+package commands
+
+import "github.com/bwmarrin/discordgo"
+
+func respondWithError(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
+ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+ Type: discordgo.InteractionResponseChannelMessageWithSource,
+ Data: &discordgo.InteractionResponseData{
+ Content: message,
+ Flags: discordgo.MessageFlagsEphemeral,
+ },
+ })
+}
+
+func updateResponse(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
+ s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
+ Content: &message,
+ })
+}
diff --git a/commands/play.go b/commands/play.go
index b8d39fd..6aac6c3 100644
--- a/commands/play.go
+++ b/commands/play.go
@@ -14,7 +14,6 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
options := i.ApplicationCommandData().Options
input := options[0].StringValue()
- // Special cases from autocomplete
if input == "min_chars" {
respondWithError(s, i, "Enter at least 3 characters to search.")
return
@@ -25,7 +24,6 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
- // Check if the user is in a voice channel BEFORE deferring response
guildID := i.GuildID
userID := i.Member.User.ID
@@ -36,7 +34,6 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
- // Check if bot is already in a voice channel but not the same as the user
voice, exists := music.VoiceConnection[guildID]
if exists && !isSameVC {
channel, err := s.Channel(voice.ChannelID)
@@ -48,12 +45,10 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
- // Now that we've checked all error conditions, defer the response
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
- // Process track selection or URL
var trackURL, trackID, trackTitle string
var sourceType types.SourceType
@@ -81,12 +76,10 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
trackID = ytTrack.ID
}
} else {
- // malformed selection
updateResponse(s, i, "❌ Invalid track selection. Please try again.")
return
}
} else {
- // Direct URL or search query
if music.IsYouTubeURL(input) {
trackInfo, err := music.GetYouTubeInfo(input)
if err != nil {
@@ -104,7 +97,6 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
- // Get YouTube equivalent
ytTrack, err := music.GetYouTubeForSpotify(trackInfo.Title, trackInfo.Artist)
if err != nil {
updateResponse(s, i, "❌ Error fetching YouTube equivalent for Spotify track.")
@@ -112,10 +104,9 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
}
trackURL = ytTrack.URL
trackID = ytTrack.ID
- trackTitle = ytTrack.Title
+ trackTitle = trackInfo.Title
sourceType = types.Spotify
} else {
- // treat as search query
results, err := music.Search(input, 1)
if err != nil || len(results) == 0 {
updateResponse(s, i, "❌ No results found for your search query.")
@@ -141,7 +132,6 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
}
}
- // Join voice channel
voice, err := music.JoinVoiceChannel(s, guildID, userChannelID)
if err != nil {
logger.Log(fmt.Sprintf("Failed to join voice channel: %v", err), types.LogOptions{
@@ -152,10 +142,8 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
- // Send "now playing" message first
updateResponse(s, i, fmt.Sprintf("🎵 Now playing: **%s**", trackTitle))
- // Play the track
go func() {
err := voice.PlayYouTube(trackURL, trackID)
if err != nil {
@@ -163,24 +151,7 @@ func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
Prefix: "Play Command",
Level: types.Error,
})
- // Also update the message to show the error to the user
updateResponse(s, i, fmt.Sprintf("❌ Error playing **%s**: %v", trackTitle, err))
}
}()
}
-
-func respondWithError(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
- s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
- Type: discordgo.InteractionResponseChannelMessageWithSource,
- Data: &discordgo.InteractionResponseData{
- Content: message,
- Flags: discordgo.MessageFlagsEphemeral,
- },
- })
-}
-
-func updateResponse(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
- s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
- Content: &message,
- })
-}
diff --git a/commands/play_autocomplete.go b/commands/play_autocomplete.go
index dee02d1..85613b3 100644
--- a/commands/play_autocomplete.go
+++ b/commands/play_autocomplete.go
@@ -40,7 +40,6 @@ func PlayAutocomplete(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
- // Search for tracks
results, err := music.Search(query, 10)
if err != nil {
logger.Log(fmt.Sprintf("Search error: %v", err), types.LogOptions{
@@ -62,15 +61,13 @@ func PlayAutocomplete(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
- // Create choices for autocomplete
choices := make([]*discordgo.ApplicationCommandOptionChoice, 0, 25)
for i, result := range results {
if i >= 25 {
- break // Discord limits to 25 choices
+ break
}
- // Format display name
var displayName string
if result.SourceType == types.YouTube {
displayName = fmt.Sprintf("▶️ %s - %s", result.Title, result.Artist)
@@ -78,12 +75,10 @@ func PlayAutocomplete(s *discordgo.Session, i *discordgo.InteractionCreate) {
displayName = fmt.Sprintf("🎵 %s - %s", result.Title, result.Artist)
}
- // Truncate name if needed
if len(displayName) > 100 {
displayName = displayName[:97] + "..."
}
- // Use just source type and ID as value to avoid length issues
valueStr := fmt.Sprintf("%s|%s|%s", result.SourceType, result.ID, result.URL)
if len(valueStr) > 100 {
valueStr = fmt.Sprintf("%s|%s", result.SourceType, result.ID)
@@ -102,7 +97,6 @@ func PlayAutocomplete(s *discordgo.Session, i *discordgo.InteractionCreate) {
})
}
- // Send response
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionApplicationCommandAutocompleteResult,
Data: &discordgo.InteractionResponseData{
diff --git a/handlers/slashCommandHandler.go b/handlers/slashCommandHandler.go
index ebcd0db..5739e72 100644
--- a/handlers/slashCommandHandler.go
+++ b/handlers/slashCommandHandler.go
@@ -8,6 +8,7 @@ import (
var (
SlashCommandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
- "play": commands.Play,
+ "play": commands.Play,
+ "disconnect": commands.Disconnect,
}
)
diff --git a/utils/music/search.go b/utils/music/search.go
index 3cef107..5607426 100644
--- a/utils/music/search.go
+++ b/utils/music/search.go
@@ -3,6 +3,7 @@ package music
import (
"ai/config"
"ai/types"
+ "ai/utils/logger"
"encoding/json"
"fmt"
"io"
@@ -73,13 +74,17 @@ func Search(query string, limit int) ([]types.MusicSearchResult, error) {
func SearchSpotify(query string, limit int) ([]types.MusicSearchResult, error) {
token, err := getSpotifyToken()
if err != nil {
- return nil, fmt.Errorf("spotify token error: %w", err)
+ logger.Log("Spotify token error: "+err.Error(), types.LogOptions{
+ Prefix: "Search",
+ Level: types.Error,
+ })
+ return nil, err
}
searchURL := fmt.Sprintf("https://api.spotify.com/v1/search?q=%s&type=track&limit=%d", url.QueryEscape(query), limit)
req, err := http.NewRequest("GET", searchURL, nil)
if err != nil {
- return nil, fmt.Errorf("request creation error: %w", err)
+ return nil, err
}
req.Header.Add("Authorization", "Bearer "+token)
@@ -87,18 +92,18 @@ func SearchSpotify(query string, limit int) ([]types.MusicSearchResult, error) {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
- return nil, fmt.Errorf("search request error: %w", err)
+ return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
- return nil, fmt.Errorf("response read error: %w", err)
+ return nil, err
}
var searchResponse types.SpotifySearchResponse
if err := json.Unmarshal(body, &searchResponse); err != nil {
- return nil, fmt.Errorf("json unmarshal error: %w", err)
+ return nil, err
}
results := []types.MusicSearchResult{}
@@ -114,7 +119,6 @@ func SearchSpotify(query string, limit int) ([]types.MusicSearchResult, error) {
thumbnailURL = item.Album.Images[0].URL
}
- // Format duration as mm:ss
durationSec := item.DurationMs / 1000
duration := fmt.Sprintf("%02d:%02d", durationSec/60, durationSec%60)
@@ -141,18 +145,18 @@ func SearchYouTube(query string, limit int) ([]types.MusicSearchResult, error) {
resp, err := http.Get(searchURL)
if err != nil {
- return nil, fmt.Errorf("search request error: %w", err)
+ return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
- return nil, fmt.Errorf("response read error: %w", err)
+ return nil, err
}
var searchResponse types.YouTubeSearchResponse
if err := json.Unmarshal(body, &searchResponse); err != nil {
- return nil, fmt.Errorf("json unmarshal error: %w", err)
+ return nil, err
}
results := []types.MusicSearchResult{}
@@ -165,7 +169,7 @@ func SearchYouTube(query string, limit int) ([]types.MusicSearchResult, error) {
Artist: item.Snippet.ChannelTitle,
URL: videoURL,
ID: item.ID.VideoID,
- Duration: "00:00", // YouTube API requires a separate call to get duration
+ Duration: "00:00",
Thumbnail: item.Snippet.Thumbnails.High.URL,
SourceType: types.YouTube,
})
@@ -357,7 +361,6 @@ func GetSpotifyInfo(spotifyURL string) (types.MusicSearchResult, error) {
parts := strings.Split(spotifyURL, "/")
trackID = parts[len(parts)-1]
- // Remove any query parameters
if strings.Contains(trackID, "?") {
trackID = strings.Split(trackID, "?")[0]
}
diff --git a/utils/music/voice.go b/utils/music/voice.go
index 961a8f5..ab0a3f2 100644
--- a/utils/music/voice.go
+++ b/utils/music/voice.go
@@ -1,6 +1,8 @@
package music
import (
+ "ai/types"
+ "ai/utils/logger"
"encoding/binary"
"fmt"
"io"
@@ -14,13 +16,12 @@ import (
)
const (
- channels int = 2 // 1 for mono, 2 for stereo
- frameRate int = 48000 // audio sampling rate
- frameSize int = 960 // size of each audio frame
- maxBytes int = (frameSize * 2) * 2 // max size of opus data
+ channels int = 2
+ frameRate int = 48000
+ frameSize int = 960
+ maxBytes int = (frameSize * 2) * 2
)
-// VoiceInstance represents a voice connection
type VoiceInstance struct {
GuildID string
ChannelID string
@@ -37,7 +38,6 @@ var (
VoiceMutex = &sync.Mutex{}
)
-// Stop stops the current playback
func (v *VoiceInstance) Stop() {
v.mu.Lock()
defer v.mu.Unlock()
@@ -45,9 +45,7 @@ func (v *VoiceInstance) Stop() {
if v.Playing {
select {
case v.StopChannel <- true:
- // Signal sent here
default:
- // Channel already has a signal
}
close(v.StopChannel)
v.StopChannel = make(chan bool, 1)
@@ -55,26 +53,23 @@ func (v *VoiceInstance) Stop() {
}
}
-// JoinVoiceChannel makes the bot join a voice channel
func JoinVoiceChannel(s *discordgo.Session, guildID, channelID string) (*VoiceInstance, error) {
VoiceMutex.Lock()
defer VoiceMutex.Unlock()
- // Check if already in a voice channel in this guild
if voice, exists := VoiceConnection[guildID]; exists {
return voice, nil
}
- // not in a voice channel, create a new one
vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true)
if err != nil {
- return nil, fmt.Errorf("failed to join voice channel: %w", err)
+ return nil, err
}
encoder, err := gopus.NewEncoder(frameRate, channels, gopus.Audio)
if err != nil {
vc.Disconnect()
- return nil, fmt.Errorf("failed to create opus encoder: %w", err)
+ return nil, err
}
voiceInstance := &VoiceInstance{
@@ -90,33 +85,27 @@ func JoinVoiceChannel(s *discordgo.Session, guildID, channelID string) (*VoiceIn
return voiceInstance, nil
}
-// LeaveVoiceChannel makes the bot leave a voice channel
func LeaveVoiceChannel(guildID string) error {
VoiceMutex.Lock()
defer VoiceMutex.Unlock()
voice, exists := VoiceConnection[guildID]
if !exists {
- return fmt.Errorf("not in a voice channel")
+ return nil
}
- // Stop current playback
voice.Stop()
- // Disconnect
err := voice.Connection.Disconnect()
if err != nil {
- return fmt.Errorf("failed to disconnect from voice channel: %w", err)
+ return err
}
- // remove from map
delete(VoiceConnection, guildID)
return nil
}
-// IsUserInSameVC checks if the user is in the same voice channel as the bot
func IsUserInSameVC(s *discordgo.Session, guildID, userID string) (bool, string) {
- // Voice state
guild, err := s.State.Guild(guildID)
if err != nil {
return false, ""
@@ -131,35 +120,35 @@ func IsUserInSameVC(s *discordgo.Session, guildID, userID string) (bool, string)
}
if userChannelID == "" {
- return false, "" // user not in a voice channel
+ return false, ""
}
- // Check if bot is in a voice channel
VoiceMutex.Lock()
defer VoiceMutex.Unlock()
voice, exists := VoiceConnection[guildID]
if !exists {
- return true, userChannelID // bot not in a voice channel, but no conflict
+ return true, userChannelID
}
return voice.ChannelID == userChannelID, userChannelID
}
-// PlayYouTube downloads and plays a YouTube video
func (v *VoiceInstance) PlayYouTube(videoURL, videoID string) error {
- fmt.Printf("Starting to play: %s (ID: %s)\n", videoURL, videoID)
+ logger.Log("Starting to play: "+videoURL, types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Info,
+ })
- // Create a new stop channel for this playback
var oldStopChan chan bool
v.mu.Lock()
- // If already playing, properly stop the previous playback
if v.Playing {
- fmt.Println("Stopping current playback before starting new one...")
- // Save the old channel to send the stop signal after we release the lock
+ logger.Log("Stopping current playback", types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Info,
+ })
oldStopChan = v.StopChannel
- // Create a new channel for the new playback
v.StopChannel = make(chan bool, 1)
} else {
v.StopChannel = make(chan bool, 1)
@@ -170,130 +159,165 @@ func (v *VoiceInstance) PlayYouTube(videoURL, videoID string) error {
stopChan := v.StopChannel
v.mu.Unlock()
- // Send stop signal to old channel if it exists
- // Do this outside the lock to avoid deadlock
if oldStopChan != nil {
- // Signal the old playback to stop
select {
case oldStopChan <- true:
- fmt.Println("Stop signal sent to previous playback")
+ logger.Log("Stop signal sent to previous playback", types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Debug,
+ })
default:
- fmt.Println("Could not send stop signal, channel might be full or closed")
+ logger.Log("Could not send stop signal", types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Debug,
+ })
}
- // Wait a moment for the previous playback to clean up
time.Sleep(100 * time.Millisecond)
}
- // Ensure temp directory exists
err := os.MkdirAll("./temp", 0755)
if err != nil {
- fmt.Printf("Error creating temp directory: %v\n", err)
- return fmt.Errorf("failed to create temp directory: %w", err)
+ logger.Log("Failed to create temp directory: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
+ return err
}
- // Create a unique filename
fileName := fmt.Sprintf("./temp/%s_%d.mp3", videoID, time.Now().Unix())
- fmt.Printf("Downloading to: %s\n", fileName)
+ logger.Log("Downloading to: "+fileName, types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Debug,
+ })
- // Use yt-dlp to download audio
- downloadCmd := exec.Command("yt-dlp", "-x", "--audio-format", "mp3",
+ downloadCmd := exec.Command("yt-dlp", "--no-warnings", "--quiet", "-x", "--audio-format", "mp3",
"--audio-quality", "0", "--no-playlist", "--output", fileName, videoURL)
- // Set up pipes to capture output for debugging
- downloadCmd.Stdout = os.Stdout
- downloadCmd.Stderr = os.Stderr
+ // Completely suppress output
+ devNull, _ := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
+ defer devNull.Close()
+ downloadCmd.Stdout = devNull
+ downloadCmd.Stderr = devNull
+
+ logger.Log("Starting download", types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Debug,
+ })
- fmt.Println("Starting download...")
err = downloadCmd.Run()
if err != nil {
- fmt.Printf("Download error: %v\n", err)
+ logger.Log("Download error: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
v.mu.Lock()
v.Playing = false
v.mu.Unlock()
- return fmt.Errorf("failed to download audio: %w", err)
+ return err
}
- fmt.Printf("Download complete, starting playback\n")
+ logger.Log("Download complete, starting playback", types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Info,
+ })
- // Check if file exists and get its size
fileInfo, err := os.Stat(fileName)
if err != nil {
- fmt.Printf("File stat error: %v\n", err)
+ logger.Log("File stat error: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
v.mu.Lock()
v.Playing = false
v.mu.Unlock()
- return fmt.Errorf("file stat error: %w", err)
+ return err
}
- fmt.Printf("File size: %d bytes\n", fileInfo.Size())
- // Ensure file gets deleted after playback
+ logger.Log(fmt.Sprintf("File size: %d bytes", fileInfo.Size()), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Debug,
+ })
+
defer os.Remove(fileName)
- // Make sure we're not already in a speaking state
+ err = v.playAudioFile(fileName, stopChan)
+ if err != nil {
+ logger.Log("Playback error: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
+ }
+
+ v.mu.Lock()
+ v.Playing = false
+ v.mu.Unlock()
+
+ return err
+}
+
+func (v *VoiceInstance) playAudioFile(filename string, stopChan chan bool) error {
v.Connection.Speaking(false)
time.Sleep(50 * time.Millisecond)
- // Set speaking status
- err = v.Connection.Speaking(true)
+ err := v.Connection.Speaking(true)
if err != nil {
- fmt.Printf("Speaking error: %v\n", err)
- v.mu.Lock()
- v.Playing = false
- v.mu.Unlock()
- return fmt.Errorf("speaking error: %w", err)
+ logger.Log("Speaking error: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
+ return err
}
defer v.Connection.Speaking(false)
- // Use ffmpeg for playback
- ffmpeg := exec.Command("ffmpeg", "-i", fileName, "-f", "s16le", "-ar", "48000", "-ac", "2", "pipe:1")
+ ffmpeg := exec.Command("ffmpeg", "-hide_banner", "-loglevel", "quiet", "-i", filename, "-f", "s16le", "-ar", "48000", "-ac", "2", "pipe:1")
ffmpegout, err := ffmpeg.StdoutPipe()
if err != nil {
- fmt.Printf("FFmpeg pipe error: %v\n", err)
- return fmt.Errorf("ffmpeg pipe error: %w", err)
+ logger.Log("FFmpeg pipe error: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
+ return err
}
- ffmpeg.Stderr = os.Stderr
+ ffmpeg.Stderr = nil
err = ffmpeg.Start()
if err != nil {
- fmt.Printf("FFmpeg start error: %v\n", err)
- return fmt.Errorf("ffmpeg start error: %w", err)
+ logger.Log("FFmpeg start error: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
+ return err
}
- // Store ffmpeg process for proper cleanup
ffmpegProcess := ffmpeg.Process
defer func() {
ffmpegProcess.Kill()
- ffmpeg.Wait() // Wait for the process to exit to avoid zombies
+ ffmpeg.Wait()
}()
- // Read and send loop
buf := make([]int16, frameSize*channels)
playbackDone := make(chan error, 1)
go func() {
for {
- // Read data from ffmpeg
err = binary.Read(ffmpegout, binary.LittleEndian, &buf)
if err == io.EOF || err == io.ErrUnexpectedEOF {
playbackDone <- nil
return
}
if err != nil {
- playbackDone <- fmt.Errorf("error reading from ffmpeg: %w", err)
+ playbackDone <- err
return
}
- // Encode with opus
opus, err := v.OpusEncoder.Encode(buf, frameSize, maxBytes)
if err != nil {
- playbackDone <- fmt.Errorf("opus encoding error: %w", err)
+ playbackDone <- err
return
}
- // Send to Discord
select {
case v.Connection.OpusSend <- opus:
- // Sent successfully
case <-stopChan:
playbackDone <- nil
return
@@ -301,87 +325,25 @@ func (v *VoiceInstance) PlayYouTube(videoURL, videoID string) error {
}
}()
- // Wait for playback to finish or stop signal
select {
case err := <-playbackDone:
if err != nil {
- fmt.Printf("Playback error: %v\n", err)
+ logger.Log("Playback error: "+err.Error(), types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Error,
+ })
} else {
- fmt.Println("Playback completed normally")
+ logger.Log("Playback completed", types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Success,
+ })
}
+ return err
case <-stopChan:
- fmt.Println("Playback stopped by request")
+ logger.Log("Playback stopped by request", types.LogOptions{
+ Prefix: "Music Player",
+ Level: types.Info,
+ })
+ return nil
}
-
- // Make sure to kill ffmpeg
- ffmpegProcess.Kill()
-
- v.mu.Lock()
- v.Playing = false
- v.mu.Unlock()
-
- return nil
}
-
-// func (v *VoiceInstance) playAudioFile(filename string, stopChan chan bool) error {
-// // Start ffmpeg to convert the file to PCM
-// ffmpeg := exec.Command("ffmpeg", "-i", filename, "-f", "s16le", "-ar", "48000", "-ac", "2", "pipe:1")
-// ffmpegout, err := ffmpeg.StdoutPipe()
-// if err != nil {
-// return fmt.Errorf("ffmpeg stdout error: %w", err)
-// }
-
-// ffmpeg.Stderr = os.Stderr
-// err = ffmpeg.Start()
-// if err != nil {
-// return fmt.Errorf("ffmpeg start error: %w", err)
-// }
-
-// // Make sure to kill ffmpeg when we're done
-// defer ffmpeg.Process.Kill()
-
-// // Set speaking status
-// err = v.Connection.Speaking(true)
-// if err != nil {
-// return fmt.Errorf("speaking error: %w", err)
-// }
-// defer v.Connection.Speaking(false)
-
-// // Create a buffer for reading from ffmpeg
-// buf := make([]int16, frameSize*channels)
-
-// // Read and send loop
-// for {
-// // Check if we've been asked to stop
-// select {
-// case <-stopChan:
-// return nil
-// default:
-// // Continue playing
-// }
-
-// // Read data from ffmpeg
-// err = binary.Read(ffmpegout, binary.LittleEndian, &buf)
-// if err == io.EOF || err == io.ErrUnexpectedEOF {
-// // End of file
-// return nil
-// }
-// if err != nil {
-// return fmt.Errorf("error reading from ffmpeg: %w", err)
-// }
-
-// // Encode with opus
-// opus, err := v.OpusEncoder.Encode(buf, frameSize, maxBytes)
-// if err != nil {
-// return fmt.Errorf("opus encoding error: %w", err)
-// }
-
-// // Send to Discord
-// select {
-// case v.Connection.OpusSend <- opus:
-// // Sent successfully
-// case <-stopChan:
-// return nil
-// }
-// }
-// }