add format validation
Marco Andronaco andronacomarco@gmail.com
Thu, 21 Nov 2024 17:08:02 +0100
5 files changed,
146 insertions(+),
13 deletions(-)
M
go.mod
→
go.mod
@@ -1,9 +1,10 @@
module github.com/birabittoh/disgord -go 1.23 +go 1.23.2 require ( github.com/FoxeiZ/dca v0.0.0-20240420115706-e4859a963796 + github.com/birabittoh/myks v0.0.2 github.com/bwmarrin/discordgo v0.28.1 github.com/kkdai/youtube/v2 v2.10.1 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
M
go.sum
→
go.sum
@@ -2,6 +2,8 @@ github.com/FoxeiZ/dca v0.0.0-20240420115706-e4859a963796 h1:H8+3uNS2PWDRABx7QixGgJY+sa+GeUpwSN9QNFKiK/Y=
github.com/FoxeiZ/dca v0.0.0-20240420115706-e4859a963796/go.mod h1:+hXF1jadw9rDuEoyhAIMWH3acfzvPcqVQG6kKGqNrFY= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/birabittoh/myks v0.0.2 h1:EBukMUsAflwiqdNo4LE7o2WQdEvawty5ewCZWY+IXSU= +github.com/birabittoh/myks v0.0.2/go.mod h1:klNWaeUWm7TmhnBHBMt9vALwCHW11/Xw1BpCNkCx7hs= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
M
src/globals/utils.go
→
src/globals/utils.go
@@ -8,12 +8,17 @@ "net/http"
"net/url" "regexp" "strings" + "time" + "github.com/birabittoh/myks" "github.com/bwmarrin/discordgo" "github.com/kkdai/youtube/v2" ) -var searchPattern = regexp.MustCompile(`watch\?v\x3d([a-zA-Z0-9_-]{11})`) +var ( + searchPattern = regexp.MustCompile(`watch\?v\x3d([a-zA-Z0-9_-]{11})`) + searchKS = myks.New[string](12 * time.Hour) +) func GetVoiceChannelID(s *discordgo.Session, m *discordgo.MessageCreate) (response string, g *discordgo.Guild, voiceChannelID string) { if m.Member == nil {@@ -105,7 +110,14 @@ return defaultPrefix
} func Search(query string) (videoID string, err error) { - resp, err := http.Get("https://www.youtube.com/results?search_query=" + url.QueryEscape(query)) + escaped := url.QueryEscape(strings.ToLower(strings.TrimSpace(query))) + + cached, err := searchKS.Get(escaped) + if err == nil && cached != nil { + return *cached, nil + } + + resp, err := http.Get("https://www.youtube.com/results?search_query=" + escaped) if err != nil { return }@@ -121,6 +133,9 @@ if len(matches) == 0 {
err = errors.New("no video found") return } + + videoID = matches[0][1] + searchKS.Set(escaped, videoID, 11*time.Hour) return matches[0][1], nil }
M
src/music/queue.go
→
src/music/queue.go
@@ -77,13 +77,13 @@
q.nowPlaying = q.items[0] q.items = q.items[1:] - formats := q.nowPlaying.Formats.Type("audio") - if len(formats) == 0 { + format := getFormat(*q.nowPlaying) + if format == nil { logger.Debug("no formats with audio channels available for video " + q.nowPlaying.ID) return q.PlayNext() } - q.audioStream, err = NewAudio(formats[0].URL, q.vc) + q.audioStream, err = NewAudio(format.URL, q.vc) if err != nil { return }
M
src/music/video.go
→
src/music/video.go
@@ -1,22 +1,137 @@
package music import ( + "errors" + "fmt" + "net/http" + "regexp" + "strconv" "strings" + "time" gl "github.com/birabittoh/disgord/src/globals" + "github.com/birabittoh/myks" "github.com/kkdai/youtube/v2" ) +const ( + defaultCacheDuration = 6 * time.Hour +) + +var ( + expireRegex = regexp.MustCompile(`(?i)expire=(\d+)`) + ks = myks.New[youtube.Video](time.Hour) +) + +func getFormat(video youtube.Video) *youtube.Format { + formats := video.Formats.Type("audio") + for i, format := range formats { + if format.URL != "" { + return &formats[i] + } + } + + return nil +} + +func parseExpiration(url string) time.Duration { + expireString := expireRegex.FindStringSubmatch(url) + if len(expireString) < 2 { + return defaultCacheDuration + } + + expireTimestamp, err := strconv.ParseInt(expireString[1], 10, 64) + if err != nil { + return defaultCacheDuration + } + + return time.Until(time.Unix(expireTimestamp, 0)) +} + +func getFromYT(videoID string) (video *youtube.Video, err error) { + url := "https://youtu.be/" + videoID + + const maxRetries = 3 + const maxBytesToCheck = 1024 + var duration time.Duration + + for i := 0; i < maxRetries; i++ { + logger.Info("Requesting video", url, "attempt", i+1) + video, err = yt.GetVideo(url) + if err != nil || video == nil { + logger.Error("Error fetching video info:", err) + continue + } + + format := getFormat(*video) + if format == nil { + logger.Errorf("no audio formats available for video %s", videoID) + continue + } + + duration = parseExpiration(format.URL) + + resp, err := http.Get(format.URL) + if err != nil { + logger.Error("Error fetching video URL:", err) + continue + } + defer resp.Body.Close() + + if resp.ContentLength <= 0 { + logger.Error("Invalid video link, no content length...") + continue + } + + buffer := make([]byte, maxBytesToCheck) + n, err := resp.Body.Read(buffer) + if err != nil { + logger.Error("Error reading video content:", err) + continue + } + + if n > 0 { + logger.Info("Valid video link found.") + ks.Set(videoID, *video, duration) + return video, nil + } + + logger.Error("Invalid video link, content is empty...") + time.Sleep(1 * time.Second) + } + + err = fmt.Errorf("failed to fetch valid video after %d attempts", maxRetries) + return nil, err +} + +func getFromCache(videoID string) (video *youtube.Video, err error) { + video, err = ks.Get(videoID) + if err != nil { + return + } + + if video == nil { + err = errors.New("video should not be nil") + return + } + + return +} + func getVideo(args []string) (*youtube.Video, error) { - video, err := yt.GetVideo(args[0]) - if err == nil { - return video, nil + videoID, err := youtube.ExtractVideoID(args[0]) + if err != nil { + searchQuery := strings.Join(args, " ") + videoID, err = gl.Search(searchQuery) + if err != nil || videoID == "" { + return nil, err + } } - id, err := gl.Search(strings.Join(args, " ")) - if err != nil || id == "" { - return nil, err + video, err := getFromCache(videoID) + if err != nil { + return getFromYT(videoID) } - return yt.GetVideo(id) + return video, nil }