Skip to content

Commit

Permalink
feat: replace default search with Soundcloud, add support for proxy t…
Browse files Browse the repository at this point in the history
…o yt-dlp
  • Loading branch information
Trojan295 committed Oct 16, 2024
1 parent fdae332 commit d5846f6
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 54 deletions.
11 changes: 9 additions & 2 deletions cmd/airplay/airplay.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,16 @@ func main() {
logger.Fatal("failed to load envconfig", zap.Error(err))
}

logger.With(zap.String("store_type", cfg.Store.Type), zap.Any("yt-dlp", cfg.YtDlp)).Info("starting airplay")

storage = discord.NewInMemoryStorage()
youtubeFetcher = sources.NewYoutubeFetcher()

youtubeFetcherOpts := []sources.Option{}
if cfg.YtDlp.Proxy != "" {
youtubeFetcherOpts = append(youtubeFetcherOpts, sources.WithProxy(cfg.YtDlp.Proxy))
}
youtubeFetcher = sources.NewYoutubeFetcher(youtubeFetcherOpts...)

playlistGenerator := sources.NewChatGPTPlaylistGenerator(cfg.OpenAIToken)

handler := discord.NewInteractionHandler(ctx, cfg.DiscordToken, youtubeFetcher, playlistGenerator, storage, cfg).WithLogger(logger.Named("interactionHandler"))
Expand All @@ -65,7 +73,6 @@ func main() {
logger.Fatal("failed to create Discord session", zap.Error(err))
return
}

dg.AddHandler(handler.Ready)
dg.AddHandler(handler.GuildCreate)
dg.AddHandler(handler.GuildDelete)
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ go 1.22.0
toolchain go1.23.0

require (
github.com/bwmarrin/discordgo v0.28.1
github.com/bwmarrin/discordgo v0.28.2-0.20241006165315-247b6f7a76f9
github.com/kelseyhightower/envconfig v1.4.0
github.com/sashabaranov/go-openai v1.32.0
github.com/sashabaranov/go-openai v1.32.2
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302
)

require (
Expand All @@ -19,5 +20,4 @@ require (
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.26.0 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/bwmarrin/discordgo v0.27.2-0.20230922130345-1f0b57f11024 h1:fHuF+yROO
github.com/bwmarrin/discordgo v0.27.2-0.20230922130345-1f0b57f11024/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bwmarrin/discordgo v0.28.2-0.20241006165315-247b6f7a76f9 h1:21xn6O8YuFgE9jhQelqxn5qKRf0bKs9JmM6kSr3un94=
github.com/bwmarrin/discordgo v0.28.2-0.20241006165315-247b6f7a76f9/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
Expand All @@ -25,6 +27,10 @@ github.com/sashabaranov/go-openai v1.30.3 h1:TEdRP3otRXX2A7vLoU+kI5XpoSo7VUUlM/r
github.com/sashabaranov/go-openai v1.30.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.32.0 h1:Yk3iE9moX3RBXxrof3OBtUBrE7qZR0zF9ebsoO4zVzI=
github.com/sashabaranov/go-openai v1.32.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.32.1 h1:JmdOa6d+cQwvGpBJigQf+dq40Qc20b+1HcXRGVOmqFw=
github.com/sashabaranov/go-openai v1.32.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.32.2 h1:8z9PfYaLPbRzmJIYpwcWu6z3XU8F+RwVMF1QRSeSF2M=
github.com/sashabaranov/go-openai v1.32.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
Expand Down
24 changes: 6 additions & 18 deletions pkg/bot/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,20 +194,6 @@ func (p *GuildPlayer) GetPlayedSong() (*PlayedSong, error) {
return p.state.GetCurrentSong()
}

func (p *GuildPlayer) JoinVoiceChannel(channelID, textChannelID string) {
p.triggerCh <- Trigger{
Command: "join",
VoiceChannelID: &channelID,
TextChannelID: &textChannelID,
}
}

func (p *GuildPlayer) LeaveVoiceChannel() {
p.triggerCh <- Trigger{
Command: "leave",
}
}

func (p *GuildPlayer) Run(ctx context.Context) error {
currentSong, err := p.state.GetCurrentSong()
if err != nil {
Expand Down Expand Up @@ -313,15 +299,16 @@ func (p *GuildPlayer) playPlaylist(ctx context.Context) error {
return fmt.Errorf("while poping first song: %w", err)
}

logger := p.logger.With(zap.String("title", song.Title), zap.String("url", song.URL))
logger.Debug("picking next song")

if err := p.state.SetCurrentSong(&PlayedSong{Song: *song}); err != nil {
return fmt.Errorf("while setting current song: %w", err)
}

var songCtx context.Context
songCtx, p.songCtxCancel = context.WithCancel(ctx)

logger := p.logger.With(zap.String("title", song.Title), zap.String("url", song.URL))

playMsgID, err := p.session.SendPlayMessage(textChannel, &PlayMessage{
Song: song,
})
Expand All @@ -334,7 +321,7 @@ func (p *GuildPlayer) playPlaylist(ctx context.Context) error {
return fmt.Errorf("while getting DCA data from song %v: %w", song, err)
}

logger.Debug("sending audio stream")
logger.Debug("sending audio")
if err := p.session.SendAudio(songCtx, opusCh, func(d time.Duration) {
if err := p.state.SetCurrentSong(&PlayedSong{Song: *song, Position: d}); err != nil {
logger.Error("failed to set current song position", zap.Error(err))
Expand All @@ -347,14 +334,15 @@ func (p *GuildPlayer) playPlaylist(ctx context.Context) error {
return fmt.Errorf("while sending audio data: %w", err)
}

logger.Debug("finished sending audio")

if err := p.session.EditPlayMessage(textChannel, playMsgID, &PlayMessage{Song: song, Position: song.Duration}); err != nil {
logger.Error("failed to edit message", zap.Error(err))
}

if err := p.state.SetCurrentSong(nil); err != nil {
return fmt.Errorf("while setting current song: %w", err)
}
logger.Debug("stopped playing")

time.Sleep(250 * time.Millisecond)
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ type Config struct {
PerGuildCommands bool `default:"false"`

Store StoreConfig

YtDlp YtDlpConfig
}

type StoreConfig struct {
Type string `default:"memory"`
File FileStoreConfig
}

type YtDlpConfig struct {
Proxy string `default:""`
}

type FileStoreConfig struct {
Dir string `default:"./playlist"`
}
Expand Down
15 changes: 8 additions & 7 deletions pkg/discord/interactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import (

type GuildID string

type SongLookuper interface {
type SongProvider interface {
LookupSongs(ctx context.Context, input string) ([]*bot.Song, error)
GetAudio(ctx context.Context, song *bot.Song) (<-chan []byte, error)
}

type PlaylistGenerator interface {
Expand All @@ -37,21 +38,21 @@ type InteractionHandler struct {
guildPlayers map[GuildID]*bot.GuildPlayer

playlistGenerator PlaylistGenerator
songLookuper SongLookuper
songProvider SongProvider
storage InteractionStorage

cfg *config.Config // TODO: replace with a playlist store, which supports multiple guilds

logger *zap.Logger
}

func NewInteractionHandler(ctx context.Context, discordToken string, songLookuper SongLookuper, playlistGenerator PlaylistGenerator, storage InteractionStorage, cfg *config.Config) *InteractionHandler {
func NewInteractionHandler(ctx context.Context, discordToken string, songLookuper SongProvider, playlistGenerator PlaylistGenerator, storage InteractionStorage, cfg *config.Config) *InteractionHandler {
handler := &InteractionHandler{
ctx: ctx,
discordToken: discordToken,
guildPlayers: make(map[GuildID]*bot.GuildPlayer),
playlistGenerator: playlistGenerator,
songLookuper: songLookuper,
songProvider: songLookuper,
storage: storage,
cfg: cfg,
logger: zap.NewNop(),
Expand Down Expand Up @@ -146,7 +147,7 @@ func (handler *InteractionHandler) PlaySong(s *discordgo.Session, ic *discordgo.
})

go func(ic *discordgo.InteractionCreate, vs *discordgo.VoiceState) {
songs, err := handler.songLookuper.LookupSongs(handler.ctx, input)
songs, err := handler.songProvider.LookupSongs(handler.ctx, input)
if err != nil {
logger.Info("failed to lookup song metadata", zap.Error(err), zap.String("input", input))
FollowupMessageCreate(handler.logger, s, ic.Interaction, &discordgo.WebhookParams{
Expand Down Expand Up @@ -261,7 +262,7 @@ func (handler *InteractionHandler) CreatePlaylist(s *discordgo.Session, ic *disc
songs := make([]*bot.Song, 0, len(playlist.Playlist))

for _, input := range playlist.Playlist {
ss, err := handler.songLookuper.LookupSongs(handler.ctx, input)
ss, err := handler.songProvider.LookupSongs(handler.ctx, input)
if err != nil {
logger.Info("failed to lookup song metadata", zap.Error(err), zap.String("input", input))
continue
Expand Down Expand Up @@ -533,7 +534,7 @@ func (handler *InteractionHandler) setupGuildPlayer(guildID GuildID) *bot.GuildP

playlistStore := config.GetPlaylistStore(handler.cfg, string(guildID))

player := bot.NewGuildPlayer(handler.ctx, voiceChat, string(guildID), playlistStore, sources.GetAudio).WithLogger(handler.logger.With(zap.String("guildID", string(guildID))))
player := bot.NewGuildPlayer(handler.ctx, voiceChat, string(guildID), playlistStore, handler.songProvider.GetAudio).WithLogger(handler.logger.With(zap.String("guildID", string(guildID))))
return player
}

Expand Down
11 changes: 9 additions & 2 deletions pkg/discord/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

var (
MessageUserNotInVoiceChannel = "🤷🏽 You are not in a voice channel. Join a voice channel to play a song."
MessageUserNotInVoiceChannel = "🤷 You are not in a voice channel. Join a voice channel to play a song."
MessageTooLargePlaylist = "😨 You cannot request a playlist longer than 20 songs."
MessageFailedGeneratePlaylist = "😨 Failed to generate playlist."
)
Expand Down Expand Up @@ -52,7 +52,10 @@ func GenerateFailedToFindSong(input string, member *discordgo.Member) *discordgo
}

func GeneratePlayingSongEmbed(message *bot.PlayMessage) *discordgo.MessageEmbed {
progressBar := generateProgressBar(float64(message.Position)/float64(message.Song.Duration), 20)
progressBar := ""
if message.Song.Duration > 0 {
progressBar = generateProgressBar(float64(message.Position)/float64(message.Song.Duration), 20)
}

embed := &discordgo.MessageEmbed{
Title: fmt.Sprintf("▶️ %s", message.Song.GetHumanName()),
Expand Down Expand Up @@ -110,6 +113,10 @@ func generateAddingSongEmbed(title, description string, requestor *discordgo.Mem
}

func generateProgressBar(progress float64, length int) string {
if length == 0 {
return ""
}

played := int(progress * float64(length))

progressBar := ""
Expand Down
17 changes: 0 additions & 17 deletions pkg/sources/sources.go

This file was deleted.

33 changes: 28 additions & 5 deletions pkg/sources/youtube.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,44 @@ const (

type YoutubeFetcher struct {
Logger *slog.Logger

proxy *string
}

func NewYoutubeFetcher() *YoutubeFetcher {
return &YoutubeFetcher{
type Option func(f *YoutubeFetcher)

func WithProxy(proxy string) Option {
return func(f *YoutubeFetcher) {
f.proxy = &proxy
}
}

func NewYoutubeFetcher(opts ...Option) *YoutubeFetcher {
f := &YoutubeFetcher{
Logger: slog.Default(),
}

for _, opt := range opts {
opt(f)
}

return f
}

func (s *YoutubeFetcher) LookupSongs(ctx context.Context, input string) ([]*bot.Song, error) {
ytDlpPrintColumns := []string{"title", "original_url", "is_live", "duration", "thumbnail", "thumbnails"}
printColumns := strings.Join(ytDlpPrintColumns, ",")

args := []string{"--print", printColumns, "--flat-playlist", "-U"}
args := []string{"--print", printColumns, "-U"}

if strings.HasPrefix(input, "https://") {
args = append(args, input)
} else {
args = append(args, fmt.Sprintf("ytsearch:%s", input))
args = append(args, fmt.Sprintf("scsearch:%s", input))
}

if s.proxy != nil {
args = append(args, "--proxy", *s.proxy)
}

ytCmd := exec.CommandContext(ctx, "yt-dlp", args...)
Expand Down Expand Up @@ -103,9 +123,12 @@ func (s *YoutubeFetcher) GetAudio(ctx context.Context, song *bot.Song) (<-chan [
reader, writer := io.Pipe()

go func() {

ytArgs := []string{"-U", "-x", "-o", "-", "--force-overwrites", "--http-chunk-size", "100K", "'" + song.URL + "'"}

if s.proxy != nil {
ytArgs = append(ytArgs, "--proxy", *s.proxy)
}

ffmpegArgs := []string{"-i", "pipe:0"}
if song.StartPosition > 0 {
ffmpegArgs = append(ffmpegArgs, "-ss", song.StartPosition.String())
Expand Down

0 comments on commit d5846f6

Please sign in to comment.