Back to Mewsic

Mewsic Plugin API

v1.0.0-1 Documentation

Plugins are plain JavaScript files placed in Mewsic's plugin directory. They run inside the app's webview and have full access to the window.Mewsic global the moment the app has loaded.

// Basic plugin entry point
console.log("Hello from my plugin!", window.Mewsic.version);

Plugin Format

my-plugin.mewsic/
├── manifest.json   required
├── plugin.js       required
└── styles.css      optional

manifest.json

All fields except id, name, and version are optional.

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "A short description shown in the Plugins tab.",
  "author": "your-name",
  "homepage": "https://example.com",
  "permissions": ["library", "audio", "settings"]
}
Field Type Description
id string Unique identifier. Used for storage namespacing and sidebar IDs. Use kebab-case.
name string Display name shown in the Plugins view.
version string Semver string, e.g. "1.0.0".
description string Short description.
author string Your name or handle.
homepage string URL to your plugin's page or repo.
permissions string[] Informational only — tells users what the plugin touches. Not enforced.

plugin.js

Plain JavaScript — no bundler required. Wrap everything in an IIFE to avoid polluting globals.

(() => {
  const M = window.Mewsic;
  // your plugin code here
})();

styles.css

Any valid CSS. Gets injected into the app's <head> automatically when the plugin loads. You have access to all of Mewsic's CSS custom properties.

Css
/* Available CSS variables (sample) */
/*
  --accent            primary accent color
  --accent-dim        darker shade of accent
  --accent-bright     lighter shade of accent
  --accent-muted      accent at low opacity (backgrounds)
  --accent-glow       accent for glow/shadow effects
  --text-accent       accent-colored text
  --surface-base      app background
  --surface-raised    card background
  --surface-overlay   elevated panel background
  --surface-glass     translucent glass background
  --text-primary      main text
  --text-secondary    secondary text
  --text-muted        muted/hint text
  --border-subtle     faint divider
  --border-glass      glass border
*/

/* Example: add a glow to all playing cards */
.music-card.playing {
  box-shadow: 0 0 20px var(--accent-glow);
}

/* Example: custom scrollbar color */
::-webkit-scrollbar-thumb {
  background: var(--accent-muted);
}

Styles are removed when the plugin is uninstalled. Use specific selectors to avoid unintentionally overriding core UI.

Installation

Users — double-click install

Zip up the plugin folder and rename it to pluginname.mewsic. Double-clicking the file opens Mewsic and installs it automatically.

cd my-plugin.mewsic/
zip -r ../my-plugin.mewsic manifest.json plugin.js styles.css

Users — manual install

Copy the .mewsic folder to:

Platform Path
Linux ~/.config/mewsic/plugins/
macOS ~/Library/Application Support/dev.xeoniii.mewsic/plugins/
Windows %APPDATA%\dev.xeoniii.mewsic\plugins\

Developers — live editing

Drop the .mewsic folder in the plugin directory while Mewsic is running. Edit plugin.js and styles.css freely — changes apply on the next reload (Ctrl+Shift+R). No build step needed.

mkdir -p ~/.config/mewsic/plugins/my-plugin.mewsic
# edit files, reload Mewsic to see changes

The API is split into seven namespaces:

Namespace Purpose
Mewsic.player Playback control and queue management
Mewsic.library Track and playlist CRUD
Mewsic.audio DSP — EQ, reverb, speed, presets
Mewsic.ui Views, overlays, search providers, CSS injection
Mewsic.settings App settings — read and write
Mewsic.storage Isolated per-plugin key-value persistence
Mewsic.events Subscribe to system events and emit custom ones

Mewsic.player

Getters

Property Type Description
currentTrack Track | null The active track object
isPlaying boolean Whether audio is playing
volume number Current volume (0–1)
queue Track[] The active play queue
queueIndex number Index of the current track in the queue
currentTime number Playhead position in seconds
duration number Active track duration in seconds
shuffleEnabled boolean Whether shuffle is on
repeatMode "off" | "one" | "all" Current repeat mode
currentPlaylistName string | null Name of the active playlist, or null

Methods

Mewsic.player.play()
Mewsic.player.pause()
Mewsic.player.togglePlay()
Mewsic.player.next()
Mewsic.player.prev()
Mewsic.player.seek(seconds)
Mewsic.player.skipForward(seconds = 5)
Mewsic.player.skipBackward(seconds = 5)
Mewsic.player.setVolume(0.0 - 1.0)
Mewsic.player.toggleMute()
Mewsic.player.fadeVolume(targetVolume, durationMs)
Mewsic.player.setPlaybackRate(speed)   // 0.5 - 2.0
Mewsic.player.toggleShuffle()
Mewsic.player.setRepeatMode("off" | "one" | "all")
Mewsic.player.getState()               // snapshot of all player state

Queue management

JavaScript
// Play a local track by its ID
await Mewsic.player.playTrack("track-id");

// Play a virtual/remote track directly
await Mewsic.player.playVirtualTrack({
  id: "spotify:track:4PTG3Z6ehGkBF3zIqYQGSy",
  title: "Stay",
  artist: "The Kid LAROI",
  album: "F*CK LOVE 3",
  duration: 141,
  filePath: "https://open.spotify.com/track/4PTG3Z6ehGkBF3zIqYQGSy",
  isVirtual: true,
  provider: "virtual",
  coverArt: "https://i.scdn.co/image/..."
});

// Replace the whole queue
await Mewsic.player.setQueue(tracks, startIndex);

Mewsic.player.addToQueue(track);
Mewsic.player.removeFromQueue("track-id");
Mewsic.player.clearQueue();

Stream resolvers

Plugins can intercept playback of remote URLs (Spotify, SoundCloud, custom schemes, etc.) and resolve them to a playable direct audio URL before the audio engine loads them.

Mewsic.player.registerResolver(async (url) => {
  if (!url.startsWith("https://open.spotify.com/")) return null;

  const streamUrl = await mySpotifyProxy.resolve(url);
  return {
    url: streamUrl,           // required - direct playable audio URL
    title: "Stay",            // optional - overrides track metadata
    artist: "The Kid LAROI",
    duration: 141,
    coverArt: "https://..."
  };
});

Multiple resolvers can be registered. They are tried in order; the first non-null result wins.

Mewsic.library

Getters

Property Type Description
tracks Track[] All tracks (local + virtual)
virtualTracks Track[] Only virtual tracks
playlists Playlist[] All playlists
musicDir string Path to the user's music folder
isScanning boolean Whether a library scan is in progress

Track methods

Mewsic.library.getTrack("track-id");
Mewsic.library.search("query");              // searches title, artist, album
Mewsic.library.addTracks([...tracks]);
Mewsic.library.updateTrack(track);
Mewsic.library.addVirtualTrack(track);       // shows Virtual badge in UI
Mewsic.library.removeVirtualTrack("id");
Mewsic.library.purgeVirtualTracks();         // deletes all virtual tracks from the library

Playlist methods

Mewsic.library.getPlaylist("playlist-id");                    // Playlist | null
Mewsic.library.getPlaylistTracks("playlist-id");              // Track[]
Mewsic.library.addPlaylist(playlist);
Mewsic.library.updatePlaylist(playlist);
Mewsic.library.removePlaylist("playlist-id");
Mewsic.library.addTrackToPlaylist("track-id", "playlist-id");
Mewsic.library.removeTrackFromPlaylist(playlist, "track-id");
await Mewsic.library.rehydratePlaylist(playlist);
Mewsic.library.playPlaylist("playlist-id", startIndex = 0);

Search

const results = Mewsic.library.search("daft punk");
// searches title, artist, and album across all tracks

Download

Triggers Mewsic's built-in yt-dlp downloader to save a URL to the user's music folder.

await Mewsic.library.downloadTrack({
  title: "Get Lucky",
  artist: "Daft Punk",
  album: "Random Access Memories",
  coverArt: "https://...",
  url: "https://www.youtube.com/watch?v=5NV6Rdv1a3I",
  onProgress: (pct) => console.log(`${pct}% done`)
});

Mewsic.audio

Full read and write access to the DSP chain.

Getters

Property Type Description
reverbEnabled boolean Whether reverb is active
reverbStrength number Reverb wet mix (0–1)
bassBoost number Bass boost in dB
volumeBoost number Volume multiplier (0.5–3)
playbackSpeed number Playback rate (0.5–2.0)
eqGains number[] 10 EQ band gains in dB
activePresetId string | null Currently applied preset ID
presets AudioPreset[] All saved presets

Methods

Mewsic.audio.setReverb(enabled, strength?)    // strength 0.0 - 1.0
Mewsic.audio.setBassBoost(db)
Mewsic.audio.setVolumeBoost(multiplier)       // 0.5 - 3.0
Mewsic.audio.setPlaybackSpeed(speed)          // 0.5 - 2.0
Mewsic.audio.setEqGain(bandIndex, db)         // 0 - 9
Mewsic.audio.setEqGains([...10 values])       // set all bands at once
Mewsic.audio.resetEq()
Mewsic.audio.resetAll()                       // reset EQ + all effects to defaults
Mewsic.audio.applyPreset("preset-id")
Mewsic.audio.savePreset("My Preset")
Mewsic.audio.deletePreset("preset-id")
Mewsic.audio.getState()                       // snapshot of all DSP state

Mewsic.ui

Getters

Property Type
activeView string
theme "dark" | "light"
accentColor string
guiScale number
isFullscreen boolean

Navigation

Mewsic.ui.setView("library")         // any built-in view id
Mewsic.ui.openLibrary()
Mewsic.ui.openPlayer()
Mewsic.ui.openSettings()
Mewsic.ui.openPlaylist("playlist-id")
Mewsic.ui.setSearchQuery("query")    // pre-fill the search bar

Appearance

Mewsic.ui.setTheme("dark" | "light")
Mewsic.ui.setAccentColor("mint")     // any AccentPreset id
Mewsic.ui.setGuiScale(1.0)          // 0.75 - 1.5

Notifications (toast)

const id = Mewsic.ui.addNotification("Syncing...", "info", 0, true);
Mewsic.ui.updateNotification(id, { message: "Synced!", type: "success", loading: false });
Mewsic.ui.removeNotification(id);

type is "info" | "success" | "error". Duration is in ms; 0 means persistent until dismissed.

Sidebar components

Registers a clickable icon in the sidebar that routes to a custom plugin view.

Mewsic.ui.registerSidebarComponent("spotify", {
  name: "Spotify",
  icon: "<svg>...</svg>",   // raw SVG string
  viewId: "plugin:spotify"
});

Mewsic.ui.unregisterSidebarComponent("spotify");

Custom tabs / views

The viewId used in registerSidebarComponent must match the id you register here.

Mewsic.ui.registerTab("plugin:spotify", {
  render: (container) => {
    container.innerHTML = "<h1>Spotify</h1>";
  },
  cleanup: () => {
    // called when the tab is unregistered or the plugin is unloaded
  }
});

Mewsic.ui.unregisterTab("plugin:spotify");

Overlays

Appends a fixed DOM element over the entire app window. pointerEvents defaults to none so it does not block the UI unless you enable it explicitly.

const el = document.createElement("div");
el.textContent = "♫";
el.style.pointerEvents = "auto";

Mewsic.ui.registerOverlay("now-playing-badge", el);
Mewsic.ui.removeOverlay("now-playing-badge");

Search providers

Injects a custom search engine into the Harbour search dropdown.

JavaScript
Mewsic.ui.registerSearchProvider("spotify", {
  name: "Spotify",
  search: async (query) => {
    const results = await fetchSpotifyResults(query);
    return results.map(t => ({
      id: t.id,
      title: t.name,
      artist: t.artists[0].name,
      album: t.album.name,
      duration: Math.floor(t.duration_ms / 1000),
      coverArt: t.album.images[0]?.url,
      url: t.external_urls.spotify
    }));
  },
  download: async (track, musicDir, onProgress) => {
    const streamUrl = await resolveToYoutube(track.url);
    await Mewsic.library.downloadTrack({
      ...track,
      url: streamUrl,
      onProgress
    });
  }
});

Mewsic.ui.unregisterSearchProvider("spotify");

CSS injection

Mewsic.ui.injectCSS("my-plugin", `
  .sidebar { border-right: 2px solid hotpink; }
`);

Mewsic.ui.removeCSS("my-plugin");

Mewsic.settings

Read and write access to persistent app settings.

Getters

Property Type
theme "dark" | "light"
accentColor string
guiScale number
discordEnabled boolean
systemNotifications boolean
trayEnabled boolean
smoothScrollEnabled boolean
repeatMode "off" | "one" | "all"
shuffleEnabled boolean
libraryViewMode "grid" | "list"
homeViewMode "grid" | "list"
playlistViewMode "grid" | "list"
shortcuts ShortcutMap (copy)

Setters

JavaScript
Mewsic.settings.setTheme("dark" | "light")
Mewsic.settings.setAccentColor("violet")
Mewsic.settings.setGuiScale(1.0)               // 0.75 - 1.5
Mewsic.settings.setDiscordEnabled(true)
Mewsic.settings.setSystemNotifications(false)
Mewsic.settings.setTrayEnabled(true)           // also syncs with OS tray
Mewsic.settings.setSmoothScrollEnabled(true)
Mewsic.settings.setRepeatMode("all")
Mewsic.settings.setShuffle(true)
Mewsic.settings.setLibraryViewMode("grid")
Mewsic.settings.setHomeViewMode("list")
Mewsic.settings.setPlaylistViewMode("grid")

// Remap a keyboard shortcut
Mewsic.settings.setShortcut("togglePlay", "k")
Mewsic.settings.setShortcut("volumeUp", "=", { ctrl: true })
Mewsic.settings.resetShortcuts()

Valid shortcut action names: togglePlay, skipForward, skipBackward, playNext, playPrev, volumeUp, volumeDown.

Snapshot

const all = Mewsic.settings.get();
// returns a plain object copy of all settings listed above

Mewsic.storage

Namespaced, per-plugin localStorage wrapper. All keys are isolated under mewsic_plugin_<pluginId>_.

Mewsic.storage.set("my-plugin", "token", "ya29.a0...");
const token = Mewsic.storage.get("my-plugin", "token");
Mewsic.storage.remove("my-plugin", "token");

// Inspect what a plugin has stored
const keys = Mewsic.storage.keys("my-plugin");  // string[]

// Wipe everything a plugin stored
Mewsic.storage.clear("my-plugin");

Values are automatically JSON serialized on write and deserialized on read.

Mewsic.events

System events

Subscribe to changes happening inside the app.

const handler = (track) => console.log("Now playing:", track?.title);
Mewsic.events.on("track_changed", handler);
Mewsic.events.off("track_changed", handler);

// Auto-unsubscribe after the first call
Mewsic.events.once("track_changed", (track) => {
  console.log("First track loaded:", track?.title);
});
Event Payload
track_changed Track | null
playback_state_changed boolean (isPlaying)
time_changed number (seconds)
volume_changed number (0–1)
shuffle_changed boolean
repeat_changed "off" | "one" | "all"
queue_changed Track[]
view_changed string (view id)
playlist_changed string | null (playlist name)
library_changed Track[]
playlists_changed Playlist[]
theme_changed "dark" | "light"
accent_changed string
reverb_changed { enabled: boolean, strength: number }
bass_boost_changed number
volume_boost_changed number
playback_speed_changed number
eq_changed number[] (10 band gains)
track_loading { trackId: string }
track_error { trackId: string, error: string }

Custom inter-plugin events

Plugins can talk to each other through a separate custom event bus that doesn't interfere with system events.

// Plugin A emits
Mewsic.events.emit("my-plugin:sync-done", { count: 42 });

// Plugin B listens
Mewsic.events.onCustom("my-plugin:sync-done", (data) => {
  console.log("Synced", data.count, "tracks");
});

Mewsic.events.offCustom("my-plugin:sync-done", handler);

Track object reference

Typescript
interface Track {
  id: string;
  title: string;
  artist: string;
  album: string;
  duration: number;        // seconds
  filePath: string;        // local path or remote URL
  coverArt?: string;
  isVirtual?: boolean;     // true for remote/plugin-provided tracks
  provider?: string;       // e.g. "virtual", "spotify"
  genre?: string;
  year?: number;
  trackNumber?: number;
  fileSize?: number;
  bitrate?: number;
}

Complete plugin example

This example demonstrates every major pattern — sidebar view, event listening, storage, DSP control, virtual tracks, and search.

JavaScript
(() => {
  const M = window.Mewsic;
  const ID = "demo-plugin";

  // Persist a setting across sessions
  let callCount = parseInt(M.storage.get(ID, "callCount") || "0", 10);

  // React to track changes
  M.events.on("track_changed", (track) => {
    callCount++;
    M.storage.set(ID, "callCount", callCount);

    if (!track) return;

    // Auto-apply a warm EQ whenever a track starts
    M.audio.setEqGains([4, 3, 1, 0, 0, 0, 0, -1, -2, -3]);

    // Show a toast with the track title
    M.ui.addNotification(`Now playing: ${track.title}`, "info", 2000, "Demo Plugin");
  });

  // Add a virtual track to the library
  M.library.addVirtualTrack({
    id: `${ID}:example-001`,
    title: "Example Stream",
    artist: "Demo Artist",
    album: "Plugin Tracks",
    duration: 180,
    filePath: "https://example.com/stream.mp3",
    isVirtual: true,
    provider: ID,
    coverArt: ""
  });

  // Register a resolver — intercept plugin:// URLs
  M.player.registerResolver(async (url) => {
    if (!url.startsWith("plugin://demo/")) return null;
    const id = url.replace("plugin://demo/", "");
    return { url: `https://example.com/audio/${id}.mp3` };
  });

  // Register a search provider
  M.ui.registerSearchProvider(ID, {
    name: "Demo Source",
    search: async (query) => {
      // Replace with a real API call
      return [{
        id: `${ID}:search-result`,
        title: `Result for "${query}"`,
        artist: "Demo",
        album: "",
        duration: 120,
        coverArt: "",
        url: "plugin://demo/result"
      }];
    }
  });

  // Sidebar icon
  M.ui.registerSidebarComponent(ID, {
    name: "Demo Plugin",
    icon: `
      
    `,
    viewId: `plugin:${ID}`
  });

  // Custom view
  M.ui.registerTab(`plugin:${ID}`, {
    render: (container) => {
      container.innerHTML = "";
      container.style.cssText = "padding:24px;color:var(--text-primary);font-family:inherit;";

      const state = M.player.getState();
      const settings = M.settings.get();

      container.innerHTML = `
        

Demo Plugin

Showing all plugin API patterns

Now Playing

${state.currentTrack?.title ?? "Nothing"}

${state.currentTrack?.artist ?? "—"}

Stats

Tracks seen: ${callCount}

Theme: ${settings.theme}

`; container.querySelector("#dp-toggle-theme").onclick = () => { M.settings.setTheme(M.settings.theme === "dark" ? "light" : "dark"); }; container.querySelector("#dp-reset-eq").onclick = () => { M.audio.resetEq(); M.ui.addNotification("EQ reset to flat", "info", 2000); }; }, cleanup: () => {} }); // Cross-plugin messaging example M.events.emit(`${ID}:loaded`, { version: "1.0.0" }); M.events.onCustom(`${ID}:loaded`, (data) => { console.log("[Demo Plugin] Received own load event:", data); }); console.log("[Demo Plugin] Loaded. Track count:", callCount); })();

File associations

When installed via a built package (.deb, .rpm, .msi, .dmg), Mewsic registers as the default handler for:

Extension Action
.mewsic Install the plugin, show a reload prompt
.mp3, .flac, .wav, .ogg, .m4a, .aac, .opus, .aiff, .aif, .wma Play the file immediately