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.VoiceProtocol for 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

mafic.NodePool

sonolink.Client

mafic.Node

sonolink.Node

mafic.Player

sonolink.Player

player.fetch_tracks(query, type=SearchType.YOUTUBE)

sonolink.Client.search_track() / sonolink.Node.search_track()

mafic.Track

sonolink.models.Playable

mafic.Playlist

sonolink.models.Playlist

mafic.SearchType

sonolink.SearchProvider

mafic.Filter

sonolink.models.Filters

player.add_filter(filter)

sonolink.Player.set_filters()

mafic.Strategy / mafic.NodePool selection

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:

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

NoNodesAvailable

(node selection is automatic; no direct equivalent)

PlayerNotConnected

sonolink.WebSocketError

HTTPException

sonolink.HTTPException

HTTPBadRequest

sonolink.HTTPException (check status)

HTTPUnauthorized

sonolink.InvalidNodePassword

HTTPNotFound

sonolink.NodeURINotFound

(no equivalent)

sonolink.QueueEmpty

(no equivalent)

sonolink.HistoryEmpty

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:

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:

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.

Useful references