Moving From Mafic¶
This guide is for migrating from Mafic to SonoLink.
It walks through the main differences between SonoLink and Mafic, so you can get a feel for what changes and what stays familiar.
If you are not migrating an existing Mafic bot, start with Getting Started instead.
What stays familiar¶
Lavalink remains the backend.
SonoLink supports discord.py, py-cord, disnake, and nextcord via a custom
discord.VoiceProtocolfor voice integration — the same libraries Mafic supports.The main runtime objects are still a coordinator, nodes, players, tracks, playlists, and filters.
What changes¶
Mafic centers its API around a class-level NodePool and a Player.fetch_tracks search
flow. SonoLink replaces both with instance-based equivalents, introduces a structured settings
system, and adds features like autoplay, track history, and a node-level search cache that
have no equivalent in Mafic.
Concept mapping¶
Mafic |
SonoLink |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Automatic (see Node selection below) |
Connection lifecycle¶
In Mafic, you create a NodePool class-level pool and call NodePool.create_node with
the bot instance at the class level:
# Mafic
async def on_ready(self):
await NodePool.create_node(
bot=bot,
host="127.0.0.1",
port=2333,
password="youshallnotpass",
label="MAIN",
)
In SonoLink, the coordinator is an explicit instance you create and own. Nodes are
registered on it, and you call sonolink.Client.start() once your Discord client is
ready:
# SonoLink
sl_client = sonolink.Client(bot)
sl_client.create_node(
uri="http://localhost:2333",
password="youshallnotpass",
id="main",
)
async def setup_hook() -> None:
await sl_client.start()
Note
sonolink.Client.start() should be called in discord.Client.setup_hook()
(discord.py), discord.on_connect() (py-cord),
disnake.on_connect() (disnake), or nextcord.on_connect()
(nextcord) — not in on_ready.
Node selection¶
Mafic exposes NodePool.get_node with a pluggable Strategy system (VoiceRegion,
Region, Group, and custom StrategyCallable types) for controlling which node a
player is assigned to.
SonoLink does not expose a strategy API at this level. Node selection for a new player is
handled automatically. If you need to target a specific node, you can do so explicitly via
sonolink.Client.get_node():
node = sl_client.get_node(id="main")
player = node.create_player(...)
For most bots the automatic selection is sufficient and you will not need to call this directly.
Settings¶
SonoLink introduces a settings system with no Mafic equivalent. Settings are dataclass-like objects passed at node or player creation time, giving you structured control over behaviour that Mafic either did not support or left to manual implementation:
sonolink.models.InactivitySettings— controls how long a player waits before disconnecting when the channel is inactive, and what counts as inactive.sonolink.models.CacheSettings— configures the node-level LFU search result cache.sonolink.models.AutoPlaySettings— configures autoplay mode, search provider, discovery count, and seed limits.sonolink.models.HistorySettings— enables or limits the track history thatpreviousand autoplay depend on.
from sonolink.models.settings import AutoPlaySettings, CacheSettings, HistorySettings, InactivitySettings
from sonolink.gateway.enums import AutoPlayMode, InactivityMode
sl_client.create_node(
uri="http://localhost:2333",
password="youshallnotpass",
inactivity_settings=InactivitySettings(
timeout=300,
mode=InactivityMode.ALL_BOTS,
),
cache_settings=CacheSettings(
enabled=True,
max_items=1000,
),
)
Searching¶
In Mafic, searching is done on the player instance and returns a list of tracks directly:
# Mafic
tracks = await player.fetch_tracks(query, type=SearchType.YOUTUBE)
if not tracks:
return
track = tracks[0]
In SonoLink, searching is done on the client or node and returns a
sonolink.models.SearchResult wrapper that makes the result type explicit. The search
provider is passed as a sonolink.SearchProvider value rather than a SearchType:
# SonoLink
result = await sl_client.search_track(query)
if result.is_error() or result.is_empty() or result.result is None:
return
data = result.result
if isinstance(data, list):
track = data[0]
elif isinstance(data, sonolink.models.Playlist):
track = data.tracks[0]
else:
track = data
sonolink.models.SearchResult covers all possible outcomes: a single track, a
playlist, a search result list, an empty result, or an error — so you no longer need to
branch on the return type yourself.
Tracks and playlists¶
Mafic uses mafic.Track and mafic.Playlist. In SonoLink the individual track type is
sonolink.models.Playable, and sonolink.models.Playlist is the playlist
equivalent. SonoLink tracks additionally carry sonolink.models.Album and
sonolink.models.Artist metadata when the source provides it.
Players and voice connection¶
Connecting to a voice channel works the same way:
player = await voice_channel.connect(cls=sonolink.Player)
The cls argument accepts a sonolink.Player class or a pre-configured instance.
SonoLink automatically selects the correct internal adapter for your Discord library at
runtime via the SONOLINK_FRAMEWORK environment variable — you do not need to import
library-specific player classes yourself.
If you need to pre-configure a player before connecting:
player = sonolink.Player(
node=node,
volume=100,
)
await voice_channel.connect(cls=player)
Filters¶
Mafic uses individual filter classes (Karaoke, Timescale, etc.) applied one at a
time via player.add_filter:
# Mafic
await player.add_filter(mafic.Karaoke(level=0.5))
SonoLink groups all filter configuration into a single sonolink.models.Filters
object applied with sonolink.Player.set_filters(). The individual filter types
(Karaoke, Timescale, etc.) are the same, just passed as arguments to Filters:
# SonoLink
from sonolink.models import Filters, Karaoke
filters = Filters(karaoke=Karaoke(level=0.5))
await player.set_filters(filters, seek=True)
See Filters And Playback State for the full filter reference.
Errors and exceptions¶
Mafic organises its errors under a MaficException base with subclasses for HTTP errors,
player errors, and library compatibility issues. SonoLink has a separate exception hierarchy:
Mafic |
SonoLink |
|---|---|
|
(node selection is automatic; no direct equivalent) |
|
|
|
|
|
|
|
|
|
|
(no equivalent) |
|
(no equivalent) |
See Exceptions for the full exception reference.
Events¶
Mafic dispatches events through the Discord client with unprefixed names such as
on_track_start and on_node_ready. SonoLink uses the sonolink_ prefix and passes
a typed payload object alongside the player, making it easier to distinguish SonoLink events
from your bot’s own events:
Mafic |
SonoLink |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(no equivalent) |
See Events for the full event reference and payload types.
Autoplay and track history¶
Mafic has no autoplay or track history system. SonoLink introduces both.
Autoplay is configured through sonolink.models.AutoPlaySettings at player creation
time and toggled via sonolink.Player.autoplay, which accepts an
sonolink.AutoPlayMode value:
sonolink.AutoPlayMode.DISABLED— the player stops when the queue empties.sonolink.AutoPlayMode.PARTIAL— SonoLink manages queue progression autonomously but does not fill the queue with recommended tracks.sonolink.AutoPlayMode.ENABLED— when the queue empties, SonoLink discovers related tracks and fills the queue automatically.
Track history is enabled via sonolink.models.HistorySettings and exposed through
sonolink.Player.queue as a sonolink.History object. Autoplay uses history
as its seed, so the two features work together.
Warning
Autoplay requires history to be enabled. If sonolink.models.HistorySettings is
left at its default, autoplay will have no reference track to discover from.