Plugin distribution

How Vivi downloads, verifies, updates and removes plugins, and why it uses mpv script packages today and WASM later.

What stays built in

A small core ships inside the binary: Music Visualizer, Skip Intro & Credits, Lyrics, Subtitle Search, Auto-tagger, Free Music and Network Streams. They draw Slint UI (visualizer canvas, Skip button, karaoke overlay, search and preview dialogs) that a script cannot render; the two music plugins also write tags with lofty. The ListenBrainz Scrobbler needs no UI, so it is a downloadable package. Discord Rich Presence and Remote Control are packages too: a Lua script plus a small native helper per platform that talks to mpv over its JSON IPC.

Keys: the Auto-tagger works without keys (MusicBrainz search by tags or file name, Cover Art Archive); an optional free AcoustID application key adds audio-fingerprint matching. Free Music searches the Internet Archive without a key; Jamendo needs a free client ID. The scrobbler needs your ListenBrainz user token. MusicBrainz is queried at most once per second with a descriptive User-Agent.

Everything else is a downloadable package, so the app stays small and plugins update on their own schedule.

Options considered

mpv Lua/JS packages — now

mpv already embeds Lua and JavaScript, so packages add no binary size. Full player API, key bindings, OSD overlays, audio/video filters, mp.options settings. No sandbox and mpv cannot unload a script, so disable/update take effect after a restart.

WASM (Extism/wasmtime) — later

Real sandbox with enforced permissions and a host API (player control, allow-listed HTTP, settings, declarative UI messages drawn by Slint). Costs ~10–20 MB and a stable API to maintain. The kind field reserves wasm, so catalog and installer will not change.

Native libraries — rejected

Rust has no stable ABI (a rebuild per Vivi version, compiler and platform) and native code can neither be sandboxed nor kept from crashing the player.

Package format

A zip named <id>-<version>.zip with manifest.toml at its root and the entry script <id>.lua:

id = "night-mode"          # lowercase-with-dashes
name = "Night Mode"
version = "1.0.0"          # semver
description = "..."
author = "Vivi"
kind = "mpv-lua"           # mpv-lua | mpv-js (wasm reserved)
entry = "night-mode.lua"
min_vivi = "0.8.0"
icon = "night"
lucide = "moon"
permissions = []           # network | filesystem | subprocess

[[settings]]
key = "strength"
title = "Strength"
type = "choice"            # toggle | choice | text | secret | action
options = ["Medium", "Light", "Strong"]

Settings appear in Settings → Extensions and reach the script as script-opts (<id>-<key>), updated live; action settings arrive as script messages. Permissions are shown before install; with mpv scripts they are reviewed declarations, not a sandbox.

Catalog

index.json (from plugins/index.json) lists built-in plugins and packages. A package entry carries version, min_vivi, permissions, size, the url of a GitHub release asset (tag plugin-<id>-v<version>) and its sha256. Zips are deterministic, so the committed sha256 equals what CI uploads. A minisign signature field is reserved.

Publishing

The Plugins workflow checks Lua syntax, verifies index.json is current, creates a release for every new plugin-<id>-v<version> (never marked “latest”) and re-downloads each asset to verify its sha256. The docs workflow publishes the catalog. See Package & publish.

Install, update, rollback

Full design notes: docs/plugins-architecture.md.