Mewsic Plugin API
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 |