A modern, async, fully-typed Lavalink v4 client for Python Discord bots.
Project description
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file waterlink-1.0.7.tar.gz.
File metadata
- Download URL: waterlink-1.0.7.tar.gz
- Upload date:
- Size: 66.8 kB
- Tags: Source
- Uploaded using 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
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b75c127d1765457ca0c68327e7372bcf9cd04e6d4ee0d258ceb1e12a6885c13
|
|
| MD5 |
44108ca7501cc70dc1984d2c8dc3f494
|
|
| BLAKE2b-256 |
195fc3e01f48ef0ad081854fb84e823fa5393683ff237c87ee97a805af4cb837
|
File details
Details for the file waterlink-1.0.7-py3-none-any.whl.
File metadata
- Download URL: waterlink-1.0.7-py3-none-any.whl
- Upload date:
- Size: 64.2 kB
- Tags: Python 3
- Uploaded using 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
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe49e285c84db6e4149f9c730a47d82d53c0666c8e36d26177f1670db117ee84
|
|
| MD5 |
d630634026c00174a90385e7c2d47813
|
|
| BLAKE2b-256 |
adc2d314aa80bc388f2f1eb0ddf4802c82ee2ecc166254673e80469b8401dba5
|