Replaced the bot's audio handling system by removing Lavaplayer dependencies and migrating to a Lavalink v4 node. Updated `MusicManager`, `TrackScheduler`, and related classes to utilize the `LavalinkClient`. Refactored playlist and track loading logic, task integrations, and event dispatch to integrate with the remote node. Deprecated the custom Udio source, preserving references for a future plugin port. Documented configuration and deployment details for the new audio architecture.
8.7 KiB
Music
Package: me.bobert.netneutrality.music
Engine: a remote Lavalink v4 node driven by
dev.arbjerg:lavalink-client 3.4.0.
The music subsystem provides per-guild audio playback: searching/loading tracks from multiple sources, queueing, scheduling, and streaming audio into a Discord voice channel. Since the migration to Lavalink, decoding, source resolution and voice sending happen on the node, not in the bot process. The bot only sends REST/WebSocket commands and reacts to the node's events, which means new sources (YouTube, LavaSrc, …) are added by configuring server-side plugins rather than by changing Java code.
Key classes
| Class | Responsibility |
|---|---|
MusicServiceImpl |
Owns the shared LavalinkClient, registers the node, and dispatches its event flux. |
MusicManager |
Central per-guild coordinator; resolves the guild's Link and exposes a synchronous facade. |
QueueManager |
Holds the guild's track queue (LinkedBlockingDeque<TrackInfo>); enqueue/skip/shuffle. |
TrackScheduler |
Event sink for onTrackStart / onTrackEnd / onTrackException / onTrackStuck; caches the current track. |
TrackLoadingHandler |
AbstractAudioLoadResultHandler; handles track/playlist/search/no-match/failure results. |
TrackInfo |
Value object pairing a Lavalink Track with its requester (Member). |
SourceFinder |
Identifies the source/type of a query (YouTube, SoundCloud, HTTP, …). |
StuckTrackRecoveryHandler |
Retry-then-skip logic for tracks the node reports as stuck. |
MusicServiceImpl keeps a Map<String, MusicManager> of managers keyed by guild id, so each guild
has its own link, scheduler, and queue.
Startup
MusicServiceImpl is a priority-40 Lifecycle component. Its
constructor creates the LavalinkClient (the bot user id is derived from the token) and start()
registers the configured node. A node that is unreachable never prevents the bot from booting:
the client retries in the background and music commands reply with an error in the meantime.
Because the client must exist before JDA is built, App.initJDA() (priority 60) installs
new JDAVoiceUpdateListener(music.getClient()) as the gateway's voice dispatch interceptor. JDA's
AudioManager is therefore not used for playback; voice connections are opened and closed
through JDA#getDirectAudioController().
Settings
| Setting | Default | Meaning |
|---|---|---|
NETNEUTRALITY_LAVALINK_URI |
ws://lavalink:2333 |
WebSocket URI of the node |
NETNEUTRALITY_LAVALINK_PASSWORD |
— | Node password; music is disabled when missing |
NETNEUTRALITY_LAVALINK_NAME |
main |
Node name used in logs |
Audio pipeline (end-to-end)
/play query
│
▼
MusicManager.play(query, ctx)
│ SourceFinder identifies the source and builds the identifier
▼
link.loadItem(identifier).subscribe(new TrackLoadingHandler(...))
│
├─ ontrackLoaded ──▶ connect to voice (MusicUtils) ──▶ QueueManager.queue(track, member)
├─ onPlaylistLoaded / onSearchResultLoaded ──▶ install a LoadPlaylistTask (choice buttons)
├─ noMatches ──▶ reply "nothing found"
└─ loadFailed ──▶ reply / log error
│
▼
QueueManager: if nothing is playing, send createOrUpdatePlayer().setTrack(...); else append
│
▼
Lavalink node decodes and streams the audio to the Discord voice gateway
│
▼ (track ends)
node event ──▶ MusicServiceImpl dispatcher ──▶ TrackScheduler.onTrackEnd ──▶ QueueManager.startNextTrack()
- Event dispatch —
MusicServiceImplholds a single subscription per event type (TrackStartEvent,TrackEndEvent,TrackExceptionEvent,TrackStuckEvent,WebSocketClosedEvent) and routes each event to the owning guild'sTrackScheduler. All subscriptions are disposed instop(). - Synchronous facade — every Lavalink operation returns a reactive
Mono.MusicManagersubscribes internally so the 13 music commands keep their existing synchronous signatures; failures are reported throughLogHandlerand the attachedMusicChannelContext. - Current track — the node's player state is only readable via REST, so the last
TrackStartEventis cached on the scheduler and used bygetCurrentlyPlaying()and by the queue to decide whether playback must be started. - Requester tracking — each queued item is a
TrackInfo(track + requestingMember), used for now-playing/queue displays and vote-skip. Duplicate rejection compares the encoded track payload. - Playlists / search — playlist and search loads produce a
LoadPlaylistTaskwith per-result buttons (LoadTrackFromPlaylistTask), tying the music and task subsystems together. Playlist size is bounded by theMUSIC_MAXPLAYLISTSIZEguild setting. - Stuck tracks — the first
TrackStuckEventre-queues the track and seeks back to the last known position; a second consecutive event skips to the next track. - Output — playback messages are posted through a
MusicChannelContextso they land in the guild's configured music channel.
Running the node
docker-compose.yml defines a lavalink service (image ghcr.io/lavalink-devs/lavalink:4.2, the
minimum version that handles DAVE end-to-end encrypted voice) on the netneutrality network, with
lavalink/application.yml mounted read-only at /opt/Lavalink/application.yml. The bot depends on
it and the node depends on the existing ejs-api (yt-cipher) container.
docker compose up -d lavalink
docker compose logs -f lavalink # wait for "Lavalink is ready to accept connections"
Adding a server-side plugin
Plugins are declared in lavalink/application.yml and downloaded by the node on boot into the
netneutrality-lavalink-plugins volume — no bot rebuild is required:
lavalink:
plugins:
- dependency: "dev.lavalink.youtube:youtube-plugin:1.18.2"
snapshot: false
A plugin that is not on the default Lavalink repository can be pulled from any Maven repository by
adding repository: to the entry. The node currently runs a JitPack build of the SABR-enabled
YouTube fork (ashton045/youtube-source, branch
feat/sabr-support) — no local build of the fork is required, JitPack publishes a ready Lavalink
plugin jar:
lavalink:
plugins:
- dependency: "com.github.ashton045.youtube-source:youtube-plugin:17619654b1"
repository: "https://jitpack.io"
snapshot: false
The node logs Failed to check for updates for youtube-plugin: Not Found on boot; this is harmless,
JitPack publishes no maven-metadata.xml for commit-pinned versions.
Plugin-specific configuration goes under the top-level plugins: key. The YouTube plugin is
configured there with the remote cipher URL, poToken and visitorData, which used to live in
MusicServiceImpl.start().
Udio support (deferred)
Udio playback is temporarily unavailable. The previous implementation was a client-side
Lavaplayer AudioSourceManager, and Lavalink can only resolve sources that live on the server.
SourceFinder still recognises Udio URLs but returns no identifier, and MusicManager.play replies
that Udio is temporarily unsupported.
The original UdioAudioSourceManager, UdioAudioTrack and their test have been preserved verbatim
under docs/reference/udio-lavalink-plugin/ — they are not
compiled — as the reference for a future Lavalink server plugin port. That plugin is out of
scope of this migration and is tracked as a follow-up.
Guild lifecycle & cleanup
MusicPlayerAutoDisconnect(scheduled task) leaves voice when the channel is empty or the queue has run out; the connected channel is read from the bot's gateway voice state.MusicManagerCleanup(scheduled task) evicts unused per-guildMusicManagerinstances.- On shutdown,
MusicServiceImpl.stop()disposes the event subscriptions, clears the queues, destroys every player and closes the client (called fromApp.shutdown()).
Related commands
All music commands extend MusicCommand (guild
only + music role/channel enforced): PlayCommand, SkipCommand/VoteSkipCommand,
QueueCommand/ViewQueueCommand, ClearQueueCommand, ShuffleCommand, JoinCommand/LeaveCommand,
CurrentlyPlayingCommand, ResumeCommand/StopCommand, VolumeCommand, BitconnectCommand. See
commands.md.