summaryrefslogtreecommitdiff
path: root/utils
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 /utils
parentf884a0fe47f7403850de650d3be3733420ac5986 (diff)
downloadai-6b486844a18fe8bf9b4ca4bdd3c44f45094e03e5.tar.xz
ai-6b486844a18fe8bf9b4ca4bdd3c44f45094e03e5.zip
refactor; add logging; add disconnect; supress opus warnings
Diffstat (limited to 'utils')
-rw-r--r--utils/music/search.go25
-rw-r--r--utils/music/voice.go274
2 files changed, 132 insertions, 167 deletions
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
-// }
-// }
-// }