lullaby

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 25 Imported by: 0

README

lullaby

Go Reference CI

Music for Discord bots written in Go with disgo. lullaby plays songs in voice channels from inside your bot's own process, with no Lavalink server, JVM or separate node to run. A YouTube link starts playing in about 0.2 s, a queue plays without gaps, and a playing server takes about 60 MB, ffmpeg included.

music, err := lullaby.New(lullaby.Config{Voice: client.VoiceManager, DAVE: dave})
track, place, err := music.Play(ctx, lullaby.Request{GuildID: guildID, ChannelID: voiceChannelID, Query: "snooze sza"})

lullaby runs in Rem's Little Helper, a Discord bot, on a Raspberry Pi 5, and was split out of it to be used on its own. It's young: until v1, the API may still change between minor versions.

Features

  • Songs from a name or a link. A name is searched on YouTube Music. Links can be from YouTube, SoundCloud, Spotify, Apple Music, Deezer or Yandex Music. The last four don't give bots their songs, so lullaby reads the song's name, artists and length there, and plays the same song from YouTube Music, with the original's cover.
  • Suggestions while someone types: from memory, from an earlier search narrowed down to the words typed since, or from YouTube Music. Picking one plays it with YouTube Music's title, artist and cover.
  • A queue in every server, without gaps. The next song is ready, with its ffmpeg running, before the one playing ends, and takes over in the same 20 ms. A song can go to the front of the queue. Pause, skip and stop act at once.
  • Ready before Enter. While someone is still typing, lullaby can join their voice channel, warm up the connection to YouTube, start an ffmpeg and look up a pasted link.
  • A backup. With yt-dlp, links from other sites play too, and yt-dlp takes over when YouTube or SoundCloud change something. During a YouTube bot check, it can sign in with a spare account.
  • Encrypted voice. Discord requires DAVE, which lullaby gets from dave-go, in pure Go, so your bot stays one static binary.
  • Yours to show. The engine tells your bot what happens: a song started, ended, broke off or was skipped. Your bot shows that however it likes. Rem's Little Helper posts a card for each song, with buttons.

How it compares

On a Raspberry Pi 5, against Lavalink 4.2.2 with youtube-source's newest build, both sending to the same stand-in Discord voice server. BENCHMARKS.md has the songs, the method and every number.

lullaby Lavalink
A YouTube link to its first audio, median 0.22 s 0.45 s
The first song after starting about 0.2 s 1.8 s
Memory while playing 63 MB, with ffmpeg 474 MB, with the cipher server it needs for YouTube
Memory idle 18 MB 400 MB
CPU while playing, of one core 6.6% 7.0%
Late packets none none

Install

go get github.com/windrunner21/lullaby

You need Go 1.26 or newer, disgo v0.19.6, and ffmpeg, which encodes every song to Opus: sudo apt install ffmpeg, or brew install ffmpeg.

The backup is optional: yt-dlp, with Bun up to 1.3.14 to run YouTube's JavaScript, since yt-dlp doesn't support newer Bun yet (curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14"). Without them, everything plays but other sites' links, with no backup.

lullaby is tested on Linux. On systems other than Unix, stopping yt-dlp may leave the programs it started running.

Quick start

dave := lullaby.NewDAVE()
client, err := disgo.New(token,
	bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildVoiceStates)),
	bot.WithCacheConfigOpts(cache.WithCaches(cache.FlagVoiceStates)), // to know who's in which channel
	bot.WithVoiceManagerConfigOpts(voice.WithDaveSessionCreateFunc(dave.Create)),
	// Song titles come from strangers: one saying "@everyone" pings nobody.
	bot.WithRestConfigOpts(rest.WithDefaultAllowedMentions(discord.AllowedMentions{
		Parse: []discord.AllowedMentionType{}, Roles: []snowflake.ID{}, Users: []snowflake.ID{},
	})),
)
if err != nil {
	return err
}
music, err := lullaby.New(lullaby.Config{Voice: client.VoiceManager, DAVE: dave, Events: myEvents})
if err != nil {
	return err // no ffmpeg, say
}
music.Start(ctx) // warms up the connections, so the first song doesn't wait for them

// In /play, with the asker's voice channel:
track, place, err := music.Play(ctx, lullaby.Request{GuildID: guildID, ChannelID: voiceChannelID, Query: query})

examples/bot is a whole music bot in about 250 lines: /play with suggestions, /skip and /stop. Run it with DISCORD_TOKEN=... go run ./examples/bot. The package docs have more examples.

Using the engine

What it does
New(Config), Start(ctx) Make the engine, then start it, once, with your bot's lifetime. When ctx is done, the engine stops the music in every server and leaves voice
Play(ctx, Request) Joins the voice channel, finds the song and starts ffmpeg all at once, or puts the song in the queue. Returns the *Track and its place in line, 0 when it plays now. ctx only bounds the wait: the song plays on after Play returns. Once the engine is in voice in a server, it stays in its channel, so check who may ask before you call Play
Suggest(ctx, words) Songs for autocomplete, at most MaxSuggestions. A link gets none, and is looked up ahead instead
JoinEarly, GetReady Get ready for someone typing: join their channel, warm up YouTube, start an ffmpeg
Pause, Skip, Stop, StopSong Control the music. StopSong stops only while a given song plays, so an old song's buttons can't stop a newer one
Playing, UpNext, Channel What's playing, what's next and how many after it, and where
Moved, Disconnected Pass on the bot's own voice state changes, when someone moves it or disconnects it

A Track is one song: Song() gives its title, artist, length and link, Played() how far it has played, Cover() its best picture, and Meta whatever your bot gave Request.Meta, like who asked and where.

Your Events hear what happens: Started when a song from the queue starts by itself, Ended when one is over by itself or skipped (with who skipped it, or why it broke off), Left when the engine leaves voice by itself, after nothing played or a song stayed paused for a while, and QueueChanged when a song that won't start is passed over. A song you stop isn't told about: Stop and Disconnected return it. The engine never calls events while holding its own lock, so they may call it back, and your bot may call the engine while holding a lock its events take. They can run at the same time, for different servers or for one.

What the engine hands you is yours: Song(), UpNext and Suggest return copies. Errors and logs have the links in them redacted, since a link's query can hold a token.

The engine leaves a channel 2 minutes after the music stops, 30 seconds after JoinEarly if no song comes, and after a song has been paused for 10 minutes. A queue holds up to MaxQueue (100) songs.

Config
Field Default
Voice disgo's voice manager, set up with DAVE.Create needed
DAVE the voice connections' encryption sessions, from NewDAVE needed
Events hears what happens to the music nobody hears
HTTP asks the sites for songs and downloads them; yt-dlp and ffmpeg do their own networking NewClient(), which keeps connections open between songs
Logger where the engine logs slog.Default()
FFmpeg ffmpeg's path, or its name on PATH; New fails without it "ffmpeg"
YTDLP, Bun yt-dlp's and Bun's paths or names, for the backup no backup
Cache where yt-dlp keeps what it learned about YouTube's JavaScript yt-dlp's own
Cookies a spare YouTube account's cookies.txt, for bot checks; needs YTDLP never signing in
AllowPrivateLinks let songs come from the bot host's own network, like a music server at home; see below off
Errors

Play can fail with these, to check with errors.Is and explain to whoever asked:

Error Means
lullaby.ErrNotFound a search found nothing
lullaby.ErrJoin joining the voice channel failed
lullaby.ErrFull the queue has MaxQueue songs
lullaby.ErrStopped the music was stopped before the song could start
songs.ErrUnplayable private, gone, blocked here, only a preview, far too big to be a song, or a link to the bot host's own network
songs.ErrPlaylist a playlist or channel link; lullaby plays one song at a time
songs.ErrNoMatch a Spotify, Apple Music, Deezer or Yandex Music song that isn't on YouTube Music
songs.ErrNoYTDLP a link from another site, without yt-dlp
songs.ErrNoAudio YouTube gave no plain audio link for the song, and there's no yt-dlp to try instead
songs.ErrBotCheck YouTube wants a sign-in to prove the bot isn't a bot; see below
Packages
Package
lullaby The engine
lullaby/songs The Song type, the errors above, and helpers for YouTube and SoundCloud links
internal/... How songs are found and played: the sites' clients, ffmpeg and the downloads. Internal, so they can change as the sites do without breaking your bot
cmd/voicebench The benchmark against Lavalink
examples/bot A small music bot

In a public server, anyone can paste a link. By default, lullaby refuses links to the bot host's own network, like http://192.168.0.1/ or http://localhost:2333/, with songs.ErrUnplayable, so a stranger can't make the bot fetch its owner's router or a cloud's metadata service. Its own requests connect only to public addresses: through an *http.Transport, like NewClient's, they're checked as they connect, redirects included; through another kind of transport, each request's host is looked up and checked first, which can't promise where it then connects; behind a proxy, only the proxy's address is checked. A refused address isn't asked again, and isn't handed to yt-dlp or ffmpeg instead. Links handed to yt-dlp, or to ffmpeg for a playlist lullaby doesn't read itself, are checked first, but those programs resolve names, follow redirects and download on their own, without lullaby's limits, so for them it's a best effort: run the bot where it can't reach what it shouldn't if that matters. Set Config.AllowPrivateLinks to play from a server of your own at home.

lullaby also keeps strangers' links from costing much: yt-dlp's answer, playlists and their pieces have sizes far above any song's, and yt-dlp runs with --ignore-config, so what it does is up to lullaby, not config files on the machine. yt-dlp gets the link after --, so a link can't pass it options.

Tips for speed

  • Call JoinEarly and GetReady from autocomplete, with the typist's voice channel, and use Suggest for the choices. By the time they press Enter, the bot is in voice, ffmpeg is running, and a pasted link is found.
  • Tell Discord to wait while Play works, not before: start DeferCreateMessage in a goroutine, call Play, then wait for the defer before editing the answer. Each answer to Discord can take 0.2–0.4 s, longer than finding a YouTube song. examples/bot does this.
  • Keep one engine for every server. It keeps its connections to the sites open between songs, remembers searches for 10 minutes, and shares lookups between people asking for the same song. If you pass your own Config.HTTP, keep its connections open for minutes, as NewClient does, and base it on an *http.Transport: with another kind of transport, lullaby has to look up each request's host itself before sending it, one more DNS lookup each time.

Keeping YouTube working

lullaby asks YouTube for songs as its iPhone app does, the way youtube-source does for Lavalink, which gets plain audio links without running YouTube's JavaScript. Now and then YouTube stops accepting the app version lullaby says it is (iosVersion in internal/sources/youtube.go). Songs then play through yt-dlp, and the version in youtube-source's Ios.java usually fixes it: update lullaby.

YouTube judges the internet address asking. After a few hundred automated requests in a day, it can ask every request from that address to "Sign in to confirm you're not a bot", for hours, and lullaby returns songs.ErrBotCheck. Test against YouTube gently.

Signing in during a bot check

With a spare account's cookies in Config.Cookies, lullaby signs in through yt-dlp while YouTube asks, and goes back to not signing in 15 minutes later. Songs take a few seconds longer while it's signed in. Use a spare account, never your own: Google can block accounts used like this.

  1. Make a new Google account. In Chrome or Chromium, install Get cookies.txt LOCALLY and turn on Allow in Incognito in its details. In Firefox, use cookies.txt.
  2. Open one private window, and sign in to YouTube there with the spare account.
  3. In the same tab, go to https://www.youtube.com/robots.txt, and export the cookies with the extension as youtube-cookies.txt.
  4. Close the private window, and don't open YouTube with that account again. YouTube renews the cookies of every session left open, and the exported ones stop working.
  5. Run chmod 600 youtube-cookies.txt, and give its path as Config.Cookies, with yt-dlp in Config.YTDLP. The bot needs to write to it too: yt-dlp saves the cookies YouTube renews.

The file signs in to the account, like a password, so keep it to yourself and out of git. If the log says the cookies stopped working, export them again the same way.

Playing music from YouTube in Discord may go against YouTube's terms. Use lullaby for your own servers and friends.

Contributing

CONTRIBUTING.md has how the engine works, what we learned the hard way about YouTube, SoundCloud, DAVE and disgo, and how to test. make check before you send a change.

Credits

The iOS client approach follows youtube-source (MIT). yt-dlp is the backup. dave-go and godave provide DAVE, and disgo the rest of Discord.

License

MIT

Documentation

Overview

Package lullaby plays music in Discord voice channels for bots built on disgo: songs one after another in each server, without a gap between them. It finds songs on YouTube, YouTube Music, SoundCloud and the music services, has ffmpeg encode them to Opus, and sends them through disgo's voice package, encrypted with dave-go. Package songs has the Song type and the errors songs fail with. How the music shows, like a bot's song cards, is up to the app, which Events tells what happens.

Times in the comments were measured on a Raspberry Pi 5 with 8 GB, "the Pi", on a home connection.

Example

A bot sets disgo up with DAVE's sessions and voice states in its cache, makes the engine, and plays songs in the voice channel of whoever asks. examples/bot in the repository is a whole bot.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/disgoorg/disgo"
	"github.com/disgoorg/disgo/bot"
	"github.com/disgoorg/disgo/cache"
	"github.com/disgoorg/disgo/discord"
	"github.com/disgoorg/disgo/gateway"
	"github.com/disgoorg/disgo/rest"
	"github.com/disgoorg/disgo/voice"
	"github.com/disgoorg/snowflake/v2"

	"github.com/windrunner21/lullaby"
)

func main() {
	ctx := context.Background()
	dave := lullaby.NewDAVE()
	client, err := disgo.New(os.Getenv("DISCORD_TOKEN"),
		bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildVoiceStates)),
		bot.WithCacheConfigOpts(cache.WithCaches(cache.FlagVoiceStates)),
		bot.WithVoiceManagerConfigOpts(voice.WithDaveSessionCreateFunc(dave.Create)),
		// Song titles come from strangers: one saying "@everyone" pings nobody.
		bot.WithRestConfigOpts(rest.WithDefaultAllowedMentions(discord.AllowedMentions{
			Parse: []discord.AllowedMentionType{}, Roles: []snowflake.ID{}, Users: []snowflake.ID{},
		})),
	)
	if err != nil {
		log.Fatal(err)
	}
	music, err := lullaby.New(lullaby.Config{Voice: client.VoiceManager, DAVE: dave})
	if err != nil {
		log.Fatal(err)
	}
	music.Start(ctx) // warms up the connections, so the first song doesn't wait for them

	// In a /play handler, with the guild and the asker's voice channel:
	var guildID, voiceChannelID snowflake.ID
	track, place, err := music.Play(ctx, lullaby.Request{GuildID: guildID, ChannelID: voiceChannelID, Query: "snooze sza"})
	switch {
	case err != nil:
		fmt.Println("couldn't play it:", err)
	case place > 0:
		fmt.Printf("%s is number %d in line\n", track.Song().Title, place)
	default:
		fmt.Println("playing", track.Song().Title)
	}
}

Index

Examples

Constants

View Source
const (

	// MaxSuggestions is how many songs Suggest suggests at most.
	MaxSuggestions = 10

	// MaxQueue is how many songs can wait in a server's queue.
	MaxQueue = 100
)

Variables

View Source
var (
	// ErrNotFound is returned by Play when a search finds no song.
	ErrNotFound = errors.New("no song found")
	// ErrJoin is returned by Play when joining the voice channel failed.
	ErrJoin = errors.New("couldn't join voice")
	// ErrFull is returned by Play when the queue has MaxQueue songs.
	ErrFull = errors.New("the queue is full")
	// ErrStopped is returned by Play when the music was stopped before the
	// song could start.
	ErrStopped = errors.New("the music stopped")
)

Functions

func NewClient

func NewClient() *http.Client

NewClient returns an HTTP client made for the engine, which the app can share. It's a copy of http.DefaultTransport, TLS settings included, that keeps connections open between songs and resumes TLS sessions with the audio servers. If the app has replaced http.DefaultTransport with a transport of its own, say for tracing, the client uses that as it is.

Types

type Config

type Config struct {
	// Voice is disgo's voice manager, set up with DAVE's sessions:
	// bot.WithVoiceManagerConfigOpts(voice.WithDaveSessionCreateFunc(dave.Create)).
	Voice voice.Manager
	// DAVE keeps the voice connections' encryption sessions, which the
	// engine waits on so the start of a song isn't lost.
	DAVE *DAVE
	// Events hears what happens to the music. Nil hears nothing.
	Events Events
	// HTTP asks the sites for songs and downloads them. Nil is NewClient's.
	// Unless AllowPrivateLinks, the engine uses a copy: an *http.Transport is
	// copied, with connections of its own and the engine's dialer in place of
	// its dial hooks; another kind of transport has each request's host
	// looked up and checked before it's sent. yt-dlp, and ffmpeg reading a
	// playlist it was left, do their own networking.
	HTTP *http.Client
	// Logger is where the engine logs. Nil is slog.Default().
	Logger *slog.Logger

	// FFmpeg is ffmpeg's path, or its name on PATH, which encodes every
	// song. Empty is "ffmpeg". New fails when it can't find it.
	FFmpeg string
	// YTDLP is yt-dlp's path or name: the backup for YouTube and SoundCloud,
	// and the way to every other site. Empty is without it.
	YTDLP string
	// Bun is Bun's path or name, which yt-dlp runs YouTube's JavaScript
	// with. Empty leaves it to yt-dlp.
	Bun string
	// Cache is where yt-dlp keeps what it learned about YouTube's JavaScript.
	// Empty is yt-dlp's own.
	Cache string
	// Cookies is a cookies.txt file from a spare YouTube account, which
	// yt-dlp signs in with during a bot check, so it needs YTDLP too. Empty
	// is never signing in.
	Cookies string

	// AllowPrivateLinks lets songs come from the bot host's own network,
	// like a music server at home. Off, links to private addresses fail with
	// songs.ErrUnplayable, so a stranger's link can't make the bot fetch its
	// owner's router or a cloud's metadata service. Through an
	// *http.Transport, the engine's requests are checked as they connect,
	// redirects included; through another kind of transport, each request's
	// host is checked first, which can't promise where it then connects.
	// Links handed to yt-dlp, or to ffmpeg to read itself, are checked
	// first, but those programs resolve and follow redirects on their own.
	AllowPrivateLinks bool
}

Config is what an Engine works with. Voice and DAVE are needed; the rest have defaults.

type DAVE

type DAVE struct {
	// contains filtered or unexported fields
}

DAVE keeps the end-to-end encryption session of each server's voice connection, which Discord has required since 2026-03-01. Songs wait while a session is being set up, so their start isn't sent unencrypted and dropped.

func NewDAVE

func NewDAVE() *DAVE

NewDAVE returns a DAVE with no sessions yet.

func (*DAVE) Create

func (d *DAVE) Create(logger *slog.Logger, userID godave.UserID, callbacks godave.Callbacks) godave.Session

Create makes the session for a new voice connection. It's the godave.SessionCreateFunc for disgo's voice manager, which passes the connection in as callbacks.

type End

type End struct {
	// SkippedBy is who skipped it, if anyone.
	SkippedBy snowflake.ID
	// Err says why it broke off, if it did.
	Err error
}

End is how a song came to be over.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine plays music in any number of servers.

func New

func New(cfg Config) (*Engine, error)

New returns an engine. It fails without Config.Voice or Config.DAVE, or when it can't find ffmpeg, or yt-dlp or Bun when they're given.

func (*Engine) Channel

func (e *Engine) Channel(guildID snowflake.ID) (snowflake.ID, bool)

Channel returns the voice channel the player is in, or on its way to, in a server.

func (*Engine) Disconnected

func (e *Engine) Disconnected(guildID snowflake.ID) (*Track, bool)

Disconnected stops the music in a server after someone took the player out of voice. It returns the song that was playing, if any, and whether it stopped: a player still joining is left to finish.

func (*Engine) GetReady

func (e *Engine) GetReady()

GetReady gets the rest ready for someone typing a song's name, beside joining voice: an ffmpeg for their song, and a connection to YouTube's API, which Go closes when it's been unused for a while.

func (*Engine) JoinEarly

func (e *Engine) JoinEarly(guildID, channelID snowflake.ID)

JoinEarly starts joining a voice channel for someone typing a song's name, so the player is there by the time they pick one. Joining took about 1.1 s, which came before the first song's first sound. If no song comes, it leaves after 30 seconds. It does nothing while the player is in voice in the server already.

func (*Engine) Moved

func (e *Engine) Moved(guildID, channelID snowflake.ID)

Moved follows the player to the voice channel someone moved it to.

func (*Engine) Pause

func (e *Engine) Pause(guildID snowflake.ID, t *Track) (paused, ok bool)

Pause pauses t, or lets it go on if it's paused, and reports whether it's paused now. ok is false when t isn't playing anymore.

func (*Engine) Play

func (e *Engine) Play(ctx context.Context, r Request) (*Track, int, error)

Play plays a song in a voice channel, or puts it in the queue when one is playing already. It returns the song and its place in the queue, or 0 when it plays now. It joins the channel, finds the song and starts ffmpeg all at once. ctx only bounds the wait: once Play returns, the song plays on until it's over or the engine stops, and a lookup or join given up on goes on for whoever asks next.

func (*Engine) Playing

func (e *Engine) Playing(guildID snowflake.ID) (*Track, bool)

Playing returns the song playing in a server, if any.

func (*Engine) Skip

func (e *Engine) Skip(guildID, by snowflake.ID) bool

Skip ends the song playing in a server, and the next one takes over. It reports whether a song was playing.

func (*Engine) Start

func (e *Engine) Start(ctx context.Context) error

Start warms up the connections to YouTube and finds SoundCloud's API key, so the first song doesn't wait for them. ctx is the engine's lifetime: once it's done, the engine stops the music in every server and leaves voice, without telling Events, and Play fails with ErrStopped. Start can be called once; an engine that's never started lives until the program ends.

func (*Engine) Stop

func (e *Engine) Stop(guildID snowflake.ID) (*Track, bool)

Stop stops the music in a server and leaves voice, in the background. It returns the song that was playing, if any, and whether the player was in voice there, or on its way.

func (*Engine) StopSong

func (e *Engine) StopSong(guildID snowflake.ID, t *Track) bool

StopSong is Stop, but only while t is playing, so an old song's buttons can't stop a newer one. It reports whether it stopped.

func (*Engine) Suggest

func (e *Engine) Suggest(ctx context.Context, query string) []songs.Song

Suggest finds songs for someone typing a song's name, as fast as it can: from memory, from an earlier search narrowed down to the words typed since, or from YouTube Music. A link gets no suggestions; it's looked up while they're still choosing instead, so Play finds it ready. What's suggested is remembered, since picking a suggestion only sends its link.

Example

Autocomplete suggests songs while someone types, and gets ready for the one they'll pick: it joins their voice channel, warms up YouTube and starts an ffmpeg, so the song starts as soon as they press Enter.

package main

import (
	"github.com/disgoorg/disgo/discord"
	"github.com/disgoorg/disgo/handler"
	"github.com/disgoorg/snowflake/v2"

	"github.com/windrunner21/lullaby"
)

func main() {
	var music *lullaby.Engine
	autocomplete := func(e *handler.AutocompleteEvent, voiceChannelID snowflake.ID) error {
		music.JoinEarly(*e.GuildID(), voiceChannelID)
		music.GetReady()
		choices := []discord.AutocompleteChoice{}
		for _, s := range music.Suggest(e.Ctx, e.Data.String("song")) {
			// The value is the song's link, which Play plays at once, with
			// YouTube Music's title and artist.
			choices = append(choices, discord.AutocompleteChoiceString{Name: s.Title + " · " + s.Artist, Value: s.Link})
		}
		return e.AutocompleteResult(choices)
	}
	_ = autocomplete
}

func (*Engine) UpNext

func (e *Engine) UpNext(guildID snowflake.ID) (songs.Song, int, bool)

UpNext returns the song next in line in a server and how many come after it, if any.

type Events

type Events interface {
	// Started is called when a song from the queue starts playing by itself,
	// after the one before it. A song Play starts at once isn't told about.
	Started(guildID snowflake.ID, t *Track)
	// Ended is called once a song that played is over by itself: it ran
	// out, broke off or was skipped. A song stopped isn't told about: Stop
	// and Disconnected return the song that was playing, and StopSong's
	// caller has it already. Nor is the end of the engine's lifetime.
	Ended(guildID snowflake.ID, t *Track, end End)
	// Left is called when the engine leaves voice by itself, because nothing
	// played, or a song stayed paused, for a while. playing is the song that
	// was playing, or nil.
	Left(guildID snowflake.ID, playing *Track)
	// QueueChanged is called when the queue changes by itself: a song that
	// won't start is passed over.
	QueueChanged(guildID snowflake.ID)
}

Events hears what happens to the music, so the app can show it. The engine never calls them while it holds its own lock, so they may call the engine, and the app may call the engine while holding a lock its events take. They can be called at the same time from different goroutines, for different servers or for one, so they must be safe for that.

Example

Events hears what happens to the music. A song's own data, like where it was asked for, rides along as Request.Meta and comes back as Track.Meta.

package main

import (
	"context"

	"github.com/disgoorg/disgo/bot"
	"github.com/disgoorg/disgo/discord"
	"github.com/disgoorg/snowflake/v2"

	"github.com/windrunner21/lullaby"
)

// cards posts a message when a song from the queue starts, in the text
// channel it was asked for in, which it keeps as the Track's Meta. The
// client is set up to ping nobody, as in Example.
type cards struct {
	client *bot.Client
}

func (c cards) Started(_ snowflake.ID, t *lullaby.Track) {
	channelID := t.Meta.(snowflake.ID)
	c.client.Rest.CreateMessage(channelID, discord.MessageCreate{Content: "Now playing " + t.Song().Title})
}

func (cards) Ended(snowflake.ID, *lullaby.Track, lullaby.End) {}
func (cards) Left(snowflake.ID, *lullaby.Track)               {}
func (cards) QueueChanged(snowflake.ID)                       {}

func main() {
	// With a client and DAVE set up as in Example:
	newMusic := func(client *bot.Client, dave *lullaby.DAVE) (*lullaby.Engine, error) {
		return lullaby.New(lullaby.Config{Voice: client.VoiceManager, DAVE: dave, Events: cards{client: client}})
	}
	// In /play, the text channel it was asked in rides along as Meta:
	play := func(ctx context.Context, music *lullaby.Engine, guildID, voiceChannelID, textChannelID snowflake.ID) error {
		_, _, err := music.Play(ctx, lullaby.Request{GuildID: guildID, ChannelID: voiceChannelID, Query: "snooze sza", Meta: textChannelID})
		return err
	}
	_, _ = newMusic, play
}

type Request

type Request struct {
	// GuildID is the server to play it in.
	GuildID snowflake.ID
	// ChannelID is the voice channel to play it in. If the engine is in voice
	// in the server already, it stays in its channel: the app decides who may
	// ask for songs there.
	ChannelID snowflake.ID
	// Query is a song's name, or a link.
	Query string
	// Next puts it at the front of the queue.
	Next bool
	// Meta is the Track's.
	Meta any
	// Asked is when it was asked for, which the log counts from. It may be
	// zero.
	Asked time.Time
}

Request is a song asked for.

type Track

type Track struct {
	// Meta is the app's own, like who asked for the song and where, from
	// Request.Meta. The engine never looks at it; an app that changes it
	// while others read it keeps it safe itself.
	Meta any
	// contains filtered or unexported fields
}

Track is a song in a server, playing or waiting its turn.

func (*Track) Cover

func (t *Track) Cover() string

Cover is the best picture for the song. A YouTube video's own is 4:3 with black bars, so a better one is found while the song starts, and waited for here if it isn't in yet, for up to a second.

func (*Track) Played

func (t *Track) Played() time.Duration

Played is how much of the song has played, exact to the frame.

func (*Track) Song

func (t *Track) Song() songs.Song

Song is the song: its title, artist, length, link and its own cover. It's a copy the caller may change.

Directories

Path Synopsis
cmd
voicebench command
Command voicebench compares lullaby with Lavalink the way Discord would hear them: both connect to a stand-in Discord voice server on the same machine, which notes when each one's audio arrives, how evenly it comes, and how much memory and CPU each engine takes.
Command voicebench compares lullaby with Lavalink the way Discord would hear them: both connect to a stand-in Discord voice server on the same machine, which notes when each one's audio arrives, how evenly it comes, and how much memory and CPU each engine takes.
examples
bot command
Command bot is a small music bot built on lullaby: /play a song's name or link, with suggestions while you type, /skip and /stop.
Command bot is a small music bot built on lullaby: /play a song's name or link, with suggestions while you type, /skip and /stop.
internal
audio
Package audio turns a song's audio into the 20 ms Opus frames Discord plays: it downloads the audio in pieces, has ffmpeg encode it, and hands the frames out without ever making Discord wait.
Package audio turns a song's audio into the 20 ms Opus frames Discord plays: it downloads the audio in pieces, has ffmpeg encode it, and hands the frames out without ever making Discord wait.
netguard
Package netguard keeps lullaby off the bot host's own network.
Package netguard keeps lullaby off the bot host's own network.
redact
Package redact hides what could give access in links before they're logged or handed back: query values, which can hold tokens like a private SoundCloud song's secret_token or a music server's API key, and passwords.
Package redact hides what could give access in links before they're logged or handed back: query values, which can hold tokens like a private SoundCloud song's secret_token or a music server's API key, and passwords.
sources
Package sources finds songs and their audio for lullaby: YouTube asked as its iPhone app does, YouTube Music's search, SoundCloud, yt-dlp as the backup, and Spotify, Apple Music, Deezer and Yandex Music songs matched on YouTube Music by name.
Package sources finds songs and their audio for lullaby: YouTube asked as its iPhone app does, YouTube Music's search, SoundCloud, yt-dlp as the backup, and Spotify, Apple Music, Deezer and Yandex Music songs matched on YouTube Music by name.
Package songs is what lullaby says about songs: a Song and its Audio, the errors a song can't be played with, and helpers for YouTube and SoundCloud links.
Package songs is what lullaby says about songs: a Song and its Audio, the errors a song can't be played with, and helpers for YouTube and SoundCloud links.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL