waterlink
A modern, fully-typed, async Lavalink v4 client for Python Discord bots.
waterlink wraps Lavalink's REST and websocket protocol behind a clean,
Pythonic API: node pooling with load-aware routing, a rich queue engine,
typed audio filters, autoplay, crossfade, state persistence across
restarts, and helpers for popular Lavalink plugins — all with full type
hints and zero required dependencies beyond aiohttp.
It auto-detects whichever Discord library you're already using — discord.py, py-cord, nextcord, or disnake — so you don't install anything extra for voice support.
Features
- Lavalink v4 native — REST + websocket built against the current protocol, including session resuming.
- Multi-node pooling with pluggable routing strategies (lowest load, round robin, region-aware).
- Multi-library support — auto-detects discord.py, py-cord, nextcord, or disnake.
- Rich queue engine — history, shuffle, dedupe, track/queue loop modes, priority insertion.
- Typed audio filters — equalizer, timescale, karaoke, tremolo, vibrato, rotation, distortion, channel mix, low-pass, all validated.
- Autoplay — keeps audio flowing with a pluggable "related track" strategy once the queue empties.
- Source-aware metadata — for YouTube results, shows the uploading channel as the artist instead of guessing a name out of the title text; for Spotify/Apple Music/Deezer/etc. it trusts the platform's real artist metadata as-is. Opt-in per client or per search call.
- Crossfade — smooth client-side volume ramping across track transitions.
- State persistence — snapshot and restore queues/players across bot restarts (JSON file backend included, or bring your own).
- Plugin helpers — typed convenience wrappers for LavaSrc and SponsorBlock.
- Observability — structured logging, an in-process metrics collector (with Prometheus text export), and a watchdog for stalled playback/stale nodes.
- Reconnect-resilient — automatically resyncs voice state and resumes playback if a node's websocket drops and reconnects without a resumed Lavalink session, instead of silently going quiet.
- Fully typed — ships a
py.typedmarker; passesmypy --strict.
Installation
pip install waterlink[discordpy]
# or: waterlink[pycord] / waterlink[nextcord] / waterlink[disnake]
waterlink only requires aiohttp itself — the extras above just also
install a supported Discord library if you don't already have one.
You'll also need a running Lavalink v4 server.
Quick start
import discord
from discord.ext import commands
import waterlink
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
client: waterlink.WaterlinkClient | None = None
@bot.event
async def on_ready():
global client
client = waterlink.WaterlinkClient(bot=bot)
await client.add_node(host="localhost", port=2333, password="youshallnotpass")
print(f"waterlink ready, using {client.library_name}")
@bot.command()
async def play(ctx: commands.Context, *, query: str):
if ctx.author.voice is None:
return await ctx.send("Join a voice channel first.")
player = await client.connect(ctx.guild.id, ctx.author.voice.channel.id)
result = await client.search(query)
if isinstance(result, waterlink.TrackResult):
track = result.track
elif isinstance(result, waterlink.SearchTracksResult) and result.tracks:
track = result.tracks[0]
else:
return await ctx.send("No results found.")
await player.enqueue(track.with_requester(ctx.author.id))
await ctx.send(f"Queued **{track.title}**")
bot.run("YOUR_TOKEN")
See examples/ for a fuller bot with queue management,
filters, autoplay, and persistence wired up.
Core concepts
Node pool
node = await client.add_node(
name="main",
host="lavalink.example.com",
port=443,
password="secret",
secure=True,
region="us-east",
)
Add as many nodes as you like; client.connect() picks the best one
automatically (RoutingStrategy.LOWEST_LOAD by default).
Queue & playback
player = client.get_player(guild_id)
await player.enqueue(track)
await player.skip()
await player.pause()
await player.resume()
await player.seek(30_000)
player.set_loop_mode(waterlink.LoopMode.QUEUE)
Filters
chain = waterlink.FilterChain()
chain.set_timescale(waterlink.Timescale(speed=1.25, pitch=1.1))
chain.set_equalizer(waterlink.Equalizer.bass_boost())
await player.set_filters(chain)
Autoplay
autoplay = waterlink.AutoplayEngine(client.events)
autoplay.enable(guild_id)
Clean metadata
Different sources report the artist differently, so waterlink handles them differently instead of guessing with one heuristic for everything:
- YouTube: there's no real "artist" field, only a video title and an uploader/channel name — so the channel name is used as the artist. Title text is cleaned up for display but never mined to guess a performer's name (that's unreliable — a title segment could be a singer, a movie name, or a cast list, with no way to tell from text alone).
- Spotify, Apple Music, Deezer, and other tagged sources: these
already carry real artist metadata, so
track.authoris trusted as-is and never rewritten.
Enable automatic cleanup:
client = waterlink.WaterlinkClient(bot=bot, clean_metadata=True)
# or per call:
result = await client.search(query, clean=True)
track = result.tracks[0]
# YouTube result:
print(track.title) # "Tere Liye"
print(track.author) # "T-Series" <- the channel name
print(track.extra["raw_title"]) # original, untouched, if you need it
print(track.extra["raw_author"])
# Spotify result: author is left exactly as Spotify reported it.
You can also clean a single track directly:
cleaned = waterlink.clean_track(track)
TitleCleaner also accepts extra_noise_phrases if there are extra
marketing phrases (e.g. regional "New Song 2026"-style tags) you want
stripped from displayed YouTube titles:
cleaner = waterlink.TitleCleaner(extra_noise_phrases=("official channel",))
cleaned = cleaner.clean_track(track)
Events
@client.events.on(waterlink.TrackStartEvent)
async def on_track_start(event: waterlink.TrackStartEvent):
print(f"Now playing {event.track.title} in guild {event.player.guild_id}")
Persistence
backend = waterlink.JSONFileBackend("state/")
snapshot = waterlink.PlayerSnapshot.capture(player)
await backend.save(snapshot)
Documentation
Full API reference and guides live in docs/. Highlights:
Contributing
Contributions are welcome — see CONTRIBUTING.md.
git clone https://github.com/yup-console/waterlink
cd waterlink
pip install -e ".[dev]"
pytest
License
MIT — see LICENSE.
Metadata
Release files for waterlink 1.0.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| waterlink-1.0.7.tar.gz | 66.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| waterlink-1.0.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 131.1 kB
Release files / waterlink-1.0.7.tar.gz
| Download URL | waterlink-1.0.7.tar.gz |
|---|---|
| Size | 66.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5b75c127d1765457ca0c68327e7372bcf9cd04e6d4ee0d258ceb1e12a6885c13
|
|
BLAKE2b-256 checksum How to use checksums |
195fc3e01f48ef0ad081854fb84e823fa5393683ff237c87ee97a805af4cb837
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/3.8.0 pkginfo/1.12.1.2 readme-renderer/42.0 requests/2.31.0 requests-toolbelt/1.0.0 urllib3/2.7.0 tqdm/4.68.3 importlib-metadata/7.2.1 keyring/25.7.0 rfc3986/2.0.0 colorama/0.4.6 CPython/3.13.13
|
Release files / waterlink-1.0.7-py3-none-any.whl
| Download URL | waterlink-1.0.7-py3-none-any.whl |
|---|---|
| Size | 64.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe49e285c84db6e4149f9c730a47d82d53c0666c8e36d26177f1670db117ee84
|
|
BLAKE2b-256 checksum How to use checksums |
adc2d314aa80bc388f2f1eb0ddf4802c82ee2ecc166254673e80469b8401dba5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/3.8.0 pkginfo/1.12.1.2 readme-renderer/42.0 requests/2.31.0 requests-toolbelt/1.0.0 urllib3/2.7.0 tqdm/4.68.3 importlib-metadata/7.2.1 keyring/25.7.0 rfc3986/2.0.0 colorama/0.4.6 CPython/3.13.13
|