Skill: grantmacnamara/mopidy (version 1.0.4)

--- name: mopidy description: Control a Mopidy music system via Mopidy JSON-RPC for everyday listening, queue management, and playback control. Use when the user wants to search for music, see what is playing, inspect the queue, add tracks, albums, or playlists, or control playback with actions like play, pause, next, previous, clear queue, or play-now behavior. Also use for requests like "top songs by" or "best tracks from" where external ranking knowledge should be matched against the local Mopidy library before queueing the best local matches. metadata: {"clawdbot":{"requires":{"bins":["curl","jq","python3"],"env":["MOPIDY_URL"]}}} --- # Mopidy Use this skill to control a Mopidy music system through Mopidy's JSON-RPC API in a predictable, scriptable way. ## Setup Configure the Mopidy JSON-RPC endpoint before use. Recommended environment variable: ```bash export MOPIDY_URL="https://your-mopidy-host.example.com/mopidy/rpc" ``` Typical endpoint shape: - `https://your-host.example.com/mopidy/rpc` Some installations serve Iris under `/iris/`, but this skill should control Mopidy through `/mopidy/rpc`. Helper scripts: - `scripts/mopidy.sh` - `scripts/match_top_tracks.py` ## Core Tasks ### See what is playing ```bash scripts/mopidy.sh current scripts/mopidy.sh state ``` ### See the queue ```bash scripts/mopidy.sh queue ``` ### Search for music ```bash scripts/mopidy.sh search "The Beths" scripts/mopidy.sh search "David Bowie Blackstar" ``` Use search results to identify the correct URI before adding something. ### Add a track or album to the queue ```bash scripts/mopidy.sh add-track backend:song:example scripts/mopidy.sh add-album backend:album:example ``` For natural requests like “queue some Beths”, search first, then choose the best matching track or album URI. ### List playlists ```bash scripts/mopidy.sh playlists ``` ### Add playlist contents to the queue ```bash scripts/mopidy.sh add-playlist-to-queue backend:playlist:example ``` ### Playback controls ```bash scripts/mopidy.sh play scripts/mopidy.sh play-track backend:song:example scripts/mopidy.sh pause scripts/mopidy.sh next scripts/mopidy.sh previous scripts/mopidy.sh clear ``` ## Working Style - Prefer Mopidy JSON-RPC over UI/browser automation. - Search first when the request is ambiguous. - Confirm the selected item if multiple similarly named results exist. - For queueing requests, default to adding to the queue rather than replacing playback unless the user explicitly asks to play immediately. - Be careful with destructive queue actions; do not clear or replace the queue unless asked. - For requests like **top**, **best**, **essential**, **greatest**, genre-based prompts, or other fuzzy ranking language, do not rely on raw library order alone. First use web search to identify likely canonical songs/albums, then match those items against the local Mopidy library before adding them. - When using external rankings, report any items that could not be matched locally instead of silently substituting random search results. - If requested music is not in the library, say so plainly and ask the user rather than substituting random results. ## Ranked / Canonical Requests For prompts like: - "add the top five The Fall songs" - "queue the best Bowie tracks" - "play the essential Brian Eno albums" - "add 5 of the top indie rock tracks to the queue" Use this workflow: 1. search the web for a credible ranked, canonical, or genre-representative list 2. extract the song or album names 3. match them against the local Mopidy library 4. add only the confirmed matches to the queue 5. tell the user what was added and what was not found Use `scripts/match_top_tracks.py` to help match externally sourced song names against the library. ## Playlist Handling Mopidy playlist APIs and backends can differ. Safe approach: 1. list playlists 2. identify the target playlist URI 3. look up playlist contents 4. add those track URIs to the queue Do not assume backend-specific shortcut behavior unless it has been tested on the target Mopidy server. ## References Read `references/api-notes.md` if you need endpoint details, common Mopidy methods, or URI guidance.

path: references/api-notes.md
content: # Mopidy-Iris / Mopidy API Notes

These notes are for adapting the skill to a specific Mopidy setup.

## Endpoint

Expected JSON-RPC endpoint shape:
- `https://your-host.example.com/mopidy/rpc`

Notes:
- Iris is the web UI, but control should use Mopidy's JSON-RPC API.
- The available library backend may vary by installation.
- Confirm core methods against the target server before assuming backend-specific behavior.

## Commonly Useful Methods

- `core.playback.get_current_track`
- `core.playback.get_state`
- `core.tracklist.get_tl_tracks`
- `core.tracklist.add`
- `core.tracklist.clear`
- `core.playlists.as_list`
- `core.playlists.lookup`
- `core.library.search`
- `core.playback.play`
- `core.playback.pause`
- `core.playback.next`
- `core.playback.previous`

## URI Notes

URI schemes vary by backend. Examples might look like:
- track example: `backend:song:...`
- album example: `backend:album:...`
- playlist example: `backend:playlist:...`

Do not hardcode a specific backend in the skill instructions unless the skill is intentionally backend-specific.

## Queueing Pattern

Typical add-to-queue pattern:
- call `core.tracklist.add` with `uris: [<track-or-album-uri>]`

## Playlist Note

To add playlist contents to queue:
1. look up the playlist with `core.playlists.lookup`
2. extract track URIs
3. add them via `core.tracklist.add`

Do not assume a backend-specific shortcut unless tested.


path: scripts/match_top_tracks.py
content: #!/usr/bin/env python3
import json
import subprocess
import sys
from pathlib import Path

if len(sys.argv) < 2:
    print("Usage: match_top_tracks.py <artist> [song1] [song2] ...", file=sys.stderr)
    sys.exit(1)

artist = sys.argv[1]
songs = sys.argv[2:]
script_dir = Path(__file__).resolve().parent
mopidy_script = script_dir / "mopidy.sh"
base_cmd = [str(mopidy_script), "search"]

matches = []
for song in songs:
    query = f"{artist} {song}"
    try:
        raw = subprocess.check_output(base_cmd + [query], text=True)
        data = json.loads(raw)
        tracks = data.get("result", [{}])[0].get("tracks", [])
    except Exception:
        tracks = []

    chosen = None
    song_l = song.lower()
    artist_l = artist.lower()

    for t in tracks:
        t_name = (t.get("name") or "").lower()
        artists = " ".join((a.get("name") or "") for a in t.get("artists") or []).lower()
        if song_l in t_name and artist_l in artists:
            chosen = {
                "requested": song,
                "uri": t.get("uri"),
                "name": t.get("name"),
                "artist": ", ".join((a.get("name") or "") for a in t.get("artists") or [])
            }
            break

    if chosen is None and tracks:
        t = tracks[0]
        chosen = {
            "requested": song,
            "uri": t.get("uri"),
            "name": t.get("name"),
            "artist": ", ".join((a.get("name") or "") for a in t.get("artists") or [])
        }

    matches.append(chosen or {"requested": song, "uri": None, "name": None, "artist": None})

print(json.dumps(matches, indent=2))


path: scripts/mopidy.sh
content: #!/usr/bin/env bash
set -euo pipefail
MOPIDY_URL="${MOPIDY_URL:-}"

if [ -z "$MOPIDY_URL" ]; then
  echo "MOPIDY_URL is not set. Example: export MOPIDY_URL='https://your-host.example.com/mopidy/rpc'" >&2
  exit 1
fi

rpc() {
  local payload="$1"
  curl -sS -H 'Content-Type: application/json' -d "$payload" "$MOPIDY_URL"
}

usage() {
  cat <<'EOF'
Usage:
  mopidy.sh state
  mopidy.sh current
  mopidy.sh queue
  mopidy.sh playlists
  mopidy.sh search <query>
  mopidy.sh add-track <track-uri>
  mopidy.sh add-album <album-uri>
  mopidy.sh add-playlist-to-queue <playlist-uri>
  mopidy.sh play
  mopidy.sh play-track <track-uri>
  mopidy.sh pause
  mopidy.sh next
  mopidy.sh previous
  mopidy.sh clear
EOF
}

cmd="${1:-}"
shift || true

case "$cmd" in
  state)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playback.get_state"}'
    ;;
  current)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playback.get_current_track"}'
    ;;
  queue)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.tracklist.get_tl_tracks"}'
    ;;
  playlists)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playlists.as_list"}'
    ;;
  search)
    q="${*:-}"
    [ -n "$q" ] || { echo "search query required" >&2; exit 1; }
    jq -nc --arg q "$q" '{jsonrpc:"2.0",id:1,method:"core.library.search",params:{query:{any:[$q]},uris:null,exact:false}}' | curl -ksS -H 'Content-Type: application/json' -d @- "$MOPIDY_URL"
    ;;
  add-track)
    uri="${1:-}"
    [ -n "$uri" ] || { echo "track uri required" >&2; exit 1; }
    jq -nc --arg uri "$uri" '{jsonrpc:"2.0",id:1,method:"core.tracklist.add",params:{uris:[$uri]}}' | curl -ksS -H 'Content-Type: application/json' -d @- "$MOPIDY_URL"
    ;;
  add-album)
    uri="${1:-}"
    [ -n "$uri" ] || { echo "album uri required" >&2; exit 1; }
    jq -nc --arg uri "$uri" '{jsonrpc:"2.0",id:1,method:"core.tracklist.add",params:{uris:[$uri]}}' | curl -ksS -H 'Content-Type: application/json' -d @- "$MOPIDY_URL"
    ;;
  add-playlist-to-queue)
    uri="${1:-}"
    [ -n "$uri" ] || { echo "playlist uri required" >&2; exit 1; }
    lookup=$(jq -nc --arg uri "$uri" '{jsonrpc:"2.0",id:1,method:"core.playlists.lookup",params:{uri:$uri}}' | curl -ksS -H 'Content-Type: application/json' -d @- "$MOPIDY_URL")
    uris=$(printf '%s' "$lookup" | jq '[.result.tracks[].uri]')
    jq -nc --argjson uris "$uris" '{jsonrpc:"2.0",id:1,method:"core.tracklist.add",params:{uris:$uris}}' | curl -ksS -H 'Content-Type: application/json' -d @- "$MOPIDY_URL"
    ;;
  play)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playback.play"}'
    ;;
  play-track)
    uri="${1:-}"
    [ -n "$uri" ] || { echo "track uri required" >&2; exit 1; }
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.tracklist.clear"}' >/dev/null
    jq -nc --arg uri "$uri" '{jsonrpc:"2.0",id:1,method:"core.tracklist.add",params:{uris:[$uri]}}' | curl -ksS -H 'Content-Type: application/json' -d @- "$MOPIDY_URL" >/dev/null
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playback.play"}'
    ;;
  pause)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playback.pause"}'
    ;;
  next)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playback.next"}'
    ;;
  previous)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.playback.previous"}'
    ;;
  clear)
    rpc '{"jsonrpc":"2.0","id":1,"method":"core.tracklist.clear"}'
    ;;
  *)
    usage
    exit 1
    ;;
esac


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)