Community plugins for Parachord - the universal music player.
Plugins extend Parachord's capabilities by adding support for music services, scrobbling, AI features, and more. They're written as .axe files - JSON manifests with embedded JavaScript implementations.
| Plugin | Description | Auth Required |
|---|---|---|
| Apple Music | Stream via MusicKit with search, URL lookup, and album/playlist support | Yes |
| Bandcamp | Find and purchase music on Bandcamp | No |
| SoundCloud | Search and stream music from SoundCloud | Yes |
| Spotify | Spotify integration via Connect API | Yes |
| YouTube | Play music from YouTube | No |
| Local Files | Play local music files | No |
| Plugin | Description | Auth Required |
|---|---|---|
| Last.fm | Scrobble to Last.fm | Yes |
| ListenBrainz | Scrobble to ListenBrainz | Yes |
| Libre.fm | Scrobble to Libre.fm | Yes |
| Plugin | Description | Auth Required |
|---|---|---|
| Discogs | Album and artist metadata | No |
| Wikipedia | Artist biographies | No |
| Plugin | Description | Auth Required |
|---|---|---|
| ChatGPT | Generate playlists and chat using OpenAI | Yes |
| Claude | Anthropic's Claude AI assistant | Yes |
| Gemini | Generate playlists and chat using Google Gemini | Yes |
| Ollama | Run AI locally with Ollama - free, private, works offline | No |
See manifest.json for the full plugin marketplace catalog.
Plugins are .axe files with the following structure:
{
"manifest": {
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"author": "Your Name",
"description": "What this plugin does",
"icon": "🎵",
"color": "#ff6b6b",
"homepage": "https://example.com",
"email": "you@example.com"
},
"capabilities": {
"resolve": true,
"search": true,
"stream": false,
"browse": false,
"urlLookup": false
},
"urlPatterns": [
"*.example.com/track/*"
],
"settings": {
"requiresAuth": false,
"authType": "none",
"configurable": {}
},
"implementation": {
"search": "async function(query, config) { ... }",
"resolve": "async function(artist, track, album, config) { ... }",
"play": "async function(track, config) { ... }",
"init": "async function(config) { ... }",
"cleanup": "async function() { ... }"
}
}- resolve: Can find playable sources for a track
- search: Can search for tracks/albums/artists
- stream: Provides direct audio streams
- browse: Can browse catalogs/charts/playlists
- urlLookup: Can extract track info from URLs
- chat: Can handle conversational AI interactions
| Function | Purpose | Parameters |
|---|---|---|
init |
Initialize the plugin | config |
cleanup |
Clean up resources | none |
search |
Search for tracks | query, config |
resolve |
Find playable source | artist, track, album, config |
play |
Play a track | track, config |
lookupUrl |
Get track info from URL | url, config |
lookupAlbum |
Get album tracks from URL | url, config |
lookupPlaylist |
Get playlist tracks from URL | url, config |
| Function | Purpose | Parameters |
|---|---|---|
chat |
Conversational AI interaction | messages, tools, config |
generate |
Generate playlists from prompts | prompt, context, config |
search and resolve return result objects that get spread into track.sources[resolverId] via { ...result, confidence }. The shape is conventional rather than typed — fields a resolver populates show up as track.sources[resolverId].<field>. Required fields are obvious from the resolver implementations (id, title, artist, album, duration). The interesting set is the optional cross-cutting metadata that downstream consumers walk for, regardless of which resolver populated it:
| Field | Type | When to populate |
|---|---|---|
isrc |
string |
Whenever the upstream API exposes a recording ISRC. Normalize to upper-case + trimmed, format [A-Z]{2}[A-Z0-9]{3}\d{7}. Validate before storing — drop malformed values rather than emitting them. Currently captured by: Spotify (external_ids.isrc on /v1/search, /v1/tracks/{id}, /v1/playlists/{id}/tracks), Apple Music native MusicKit (song.isrc from catalog), local files (ID3 TSRC / Vorbis ISRC / MP4 atom via music-metadata's common.isrc). Bandcamp / SoundCloud / YouTube / iTunes Search don't expose ISRC; leave undefined. |
Resolver-specific identifiers (spotifyId, appleMusicId, bandcampUrl, youtubeId) stay on the source record for the resolver that owns them — they only make sense in that context. Cross-cutting metadata like ISRC identifies the underlying recording, not the service's view of it, so it belongs alongside the per-source fields and downstream consumers can walk track.sources[*].isrc to accept the first valid value from any resolver that captured it. The same principle generalizes to other recording-keyed metadata that might land later (BPM, key, recording-MBID, etc.).
Consumers in the Parachord desktop client use window.pickTrackIsrc(track) (defined in app.js) which checks top-level track.isrc first, then walks sources, skipping noMatch entries. Current consumers: the ISRC → recording-MBID fallback in resolveMbidForLove and enrichTrackWithMbid, and the Achordion track-links/submit ISRC-only fallback. New consumers should reuse pickTrackIsrc rather than reading individual fields directly.
- Fork this repository
- Create your
.axeplugin file in the repository root - Test it locally in Parachord
- Submit a pull request
- Follow the existing plugin structure
- Include clear error handling
- Don't hardcode API keys - use the config system
- Test with various edge cases
- Update this README with your plugin
MIT License - see LICENSE for details.
Plugins are community-contributed and may have their own licenses specified in their manifests.